加入视频比例配置

This commit is contained in:
北枳 2025-09-20 16:33:51 +08:00
parent c9f9a0030b
commit 170a8b7a4f
10 changed files with 179 additions and 23 deletions

View File

@ -106,6 +106,8 @@ export interface CreateMovieProjectV2Request {
language: string;
/** 图片URL */
image_url: string;
/** 画面比例(横/竖屏) */
aspect_ratio?: "16:9" | "9:16";
}
/**
@ -249,6 +251,8 @@ export interface CreateMovieProjectV3Request {
/** 道具照片URL */
photo_url: string;
}[];
/** 画面比例(横/竖屏) */
aspect_ratio?: "16:9" | "9:16";
}
/**

View File

@ -57,7 +57,8 @@ interface UseImageStoryService {
user_id: string,
mode?: "auto" | "manual",
resolution?: "720p" | "1080p" | "4k",
language?: string
language?: string,
aspectRatio?: "16:9" | "9:16"
) => Promise<{ project_id: string } | undefined>;
/** 设置角色分析 */
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
@ -457,7 +458,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
user_id: string,
mode: "auto" | "manual" = "auto",
resolution: "720p" | "1080p" | "4k" = "720p",
language: string = "English"
language: string = "English",
aspectRatio?: "16:9" | "9:16"
) => {
try {
if (hasAnalyzed) {
@ -480,7 +482,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
character_briefs,
language,
image_url: activeImageUrl,
project_id:taskId
project_id:taskId,
...(aspectRatio ? { aspect_ratio: aspectRatio } : {})
};
// 调用create_movie_project_v2接口

View File

@ -28,7 +28,8 @@ interface UseTemplateStoryService {
user_id: string,
mode: "auto" | "manual",
resolution: "720p" | "1080p" | "4k",
language: string
language: string,
aspectRatio?: "16:9" | "9:16"
) => Promise<string | undefined>;
/** 设置选中的模板 */
setSelectedTemplate: (template: StoryTemplateEntity | null) => void;
@ -250,7 +251,8 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
user_id: string,
mode: "auto" | "manual" = "auto",
resolution: "720p" | "1080p" | "4k" = "720p",
language: string = "English"
language: string = "English",
aspectRatio?: "16:9" | "9:16"
) => {
console.log('selectedTemplate', selectedTemplate)
try {
@ -271,7 +273,8 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
storyItem: selectedTemplate?.storyItem || [],
freeInput: selectedTemplate?.freeInput || [],
language,
template_id: selectedTemplate?.template_id || ""
template_id: selectedTemplate?.template_id || "",
...(aspectRatio ? { aspect_ratio: aspectRatio } : {})
};
console.log("params", params);
const result = await MovieProjectService.createProject(

View File

@ -0,0 +1,86 @@
"use client";
import { Dropdown } from "antd";
import { RectangleHorizontal, RectangleVertical } from "lucide-react";
import { AspectRatioOptions } from "./types";
export type AspectRatioValue =
| "VIDEO_ASPECT_RATIO_LANDSCAPE"
| "VIDEO_ASPECT_RATIO_PORTRAIT";
interface AspectRatioSelectorProps {
/** Current selected aspect ratio value */
value: AspectRatioValue;
/** Change handler when an option is selected */
onChange: (value: AspectRatioValue) => void;
/** Optional className to customize the trigger button */
className?: string;
/** Optional dropdown placement, defaults to top */
placement?: "top" | "bottom" | "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
/** data-alt tag for analytics/testing */
dataAlt?: string;
}
/**
* A reusable aspect ratio selector (landscape/portrait) using Antd Dropdown.
* Shows an icon and label, and calls onChange when a new ratio is chosen.
* @param {AspectRatioValue} value - current selected value
* @param {(v: AspectRatioValue) => void} onChange - change handler
* @param {string} [className] - optional className for trigger button
* @param {string} [placement] - Dropdown placement, default is top
* @param {string} [dataAlt] - data-alt attribute for the trigger
* @returns {JSX.Element}
*/
export const AspectRatioSelector = ({
value,
onChange,
className,
placement = "top",
dataAlt = "config-aspect-ratio",
}: AspectRatioSelectorProps) => {
return (
<Dropdown
overlayClassName="aspect-dropdown"
menu={{
items: AspectRatioOptions.map((option) => ({
key: option.value,
label: (
<div
className={`flex items-center gap-2 px-2 py-2 ${
option.value === value ? "bg-white/[0.12] rounded-md" : ""
}`}
>
{option.value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? (
<RectangleHorizontal className="w-4 h-4" />
) : (
<RectangleVertical className="w-4 h-4" />
)}
<span className="text-sm text-white">{option.label}</span>
</div>
),
})),
onClick: ({ key }) => onChange(key as AspectRatioValue),
}}
trigger={["click"]}
placement={placement}
>
<button
data-alt={dataAlt}
className={`flex items-center gap-1 text-white/80 transition-all duration-200 px-2 py-2 ${className || ""}`}
>
{value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? (
<RectangleHorizontal className={"w-4 h-4"} />
) : (
<RectangleVertical className={"w-4 h-4"} />
)}
<span className="text-sm">
{value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? "16:9" : "9:16"}
</span>
</button>
</Dropdown>
);
};
export default AspectRatioSelector;

View File

@ -50,6 +50,7 @@ import { PcTemplateModal } from "./PcTemplateModal";
import { H5TemplateDrawer } from "./H5TemplateDrawer";
import { PcPhotoStoryModal } from "./PcPhotoStoryModal";
import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer";
import { AspectRatioSelector } from "./AspectRatioSelector";
const LauguageOptions = [
{ value: "english", label: "English", isVip: false, code:'EN' },
@ -75,6 +76,8 @@ const VideoDurationOptions = [
{ value: "unlimited", label: "unlimited" },
];
// aspect ratio options moved to reusable component
/**模板故事模式弹窗组件 */
/**
*
@ -128,6 +131,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
language: string;
videoDuration: string;
expansion_mode: boolean;
aspect_ratio: "VIDEO_ASPECT_RATIO_LANDSCAPE" | "VIDEO_ASPECT_RATIO_PORTRAIT";
};
const [configOptions, setConfigOptions] = useState<ConfigOptions>({
@ -136,6 +140,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
language: "english",
videoDuration: "unlimited",
expansion_mode: true,
aspect_ratio: "VIDEO_ASPECT_RATIO_LANDSCAPE",
});
// 从 localStorage 初始化配置
@ -150,6 +155,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
language: parsed.language || "english",
videoDuration: parsed.videoDuration || "1min",
expansion_mode: typeof parsed.expansion_mode === 'boolean' ? parsed.expansion_mode : false,
aspect_ratio: parsed.aspect_ratio || "VIDEO_ASPECT_RATIO_LANDSCAPE",
});
} catch (error) {
console.warn('解析保存的配置失败,使用默认配置:', error);
@ -205,6 +211,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
language: configOptions.language,
video_duration: configOptions.videoDuration,
expansion_mode: configOptions.expansion_mode,
aspect_ratio: configOptions.aspect_ratio,
};
// 调用创建剧集API
@ -301,7 +308,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
{/* 第二行功能按钮和Action按钮 - 同一行 */}
<div className="flex items-center justify-between">
{/* 左侧功能按钮区域 */}
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 flex-wrap sm:flex-nowrap">
{/*
<Tooltip
title="Get creative ideas for your story"
@ -335,7 +342,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
</Tooltip>
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
<div className="hidden sm:block w-px h-4 bg-white/[0.20]"></div>
{/* 图片故事按钮 */}
<Tooltip title="Create movie from image" placement="top" trigger={isDesktop ? "hover" : "contextMenu"}>
@ -372,7 +379,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
)}
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
<div className="hidden sm:block w-px h-4 bg-white/[0.20]"></div>
{/* 语言配置 */}
<Dropdown
@ -400,7 +407,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
>
<button
data-alt={`config-language`}
className={`flex items-center gap-1 text-white/80 transition-all duration-200 px-2 py-2`}
className={`flex items-center gap-1 text-white/80 transition-all duration-200 ${isMobile ? 'px-1' : 'px-2'} py-2`}
>
<Globe className={"w-4 h-4"} />
<span className="text-sm">{LauguageOptions.find((option) => option.value === configOptions.language)?.code}</span>
@ -408,7 +415,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
</Dropdown>
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
<div className="hidden sm:block w-px h-4 bg-white/[0.20]"></div>
{/* 剧本扩展开关 */}
<Tooltip title="Enable script expansion" placement="top" trigger={isDesktop ? "hover" : "click"}>
@ -422,14 +429,14 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
onChange={(checked) => onConfigChange('expansion_mode', checked)}
/>
</div>
<span className={`text-xs text-white`}>
<span className={`text-xs text-white hidden sm:inline`}>
{configOptions.expansion_mode ? 'On' : 'Off'}
</span>
</div>
</Tooltip>
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
<div className="hidden sm:block w-px h-4 bg-white/[0.20]"></div>
{/* 时长选择 */}
<Dropdown
@ -454,12 +461,23 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
>
<button
data-alt={`config-video-duration`}
className={`flex items-center gap-1 text-white/80 transition-all duration-200 px-2 py-2`}
className={`flex items-center gap-1 text-white/80 transition-all duration-200 ${isMobile ? 'px-1' : 'px-2'} py-2`}
>
<Clock className={"w-4 h-4"} />
<span className="text-sm">{configOptions.videoDuration}</span>
<span className="text-sm">{isMobile ? (configOptions.videoDuration === 'unlimited' ? '∞' : configOptions.videoDuration.replace('min', 'm')) : configOptions.videoDuration}</span>
</button>
</Dropdown>
{/* 分隔线(移动端隐藏,避免拥挤) */}
<div className="hidden sm:block w-px h-4 bg-white/[0.20]"></div>
{/* 横/竖屏选择 */}
<AspectRatioSelector
value={configOptions.aspect_ratio}
onChange={(v) => onConfigChange('aspect_ratio', v)}
placement="top"
className={`${isMobile ? '!px-1' : ''}`}
/>
</div>
{/* 右侧Action按钮 */}

View File

@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { Drawer, Popconfirm, Tooltip, Upload } from "antd";
import { Drawer, Popconfirm, Tooltip, Upload, Dropdown } from "antd";
import { ImagePlay, Sparkles, Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import GlobalLoad from "../common/GlobalLoad";
@ -9,6 +9,7 @@ import { ActionButton } from "../common/ActionButton";
import { HighlightEditor } from "../common/HighlightEditor";
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
import { useLoadScriptText } from "@/app/service/domain/service";
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
type ConfigOptions = {
mode: "auto" | "manual";
@ -79,6 +80,7 @@ export const H5PhotoStoryDrawer = ({
const { loadingText } = useLoadScriptText(isLoading);
const [localLoading, setLocalLoading] = useState(0);
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
const router = useRouter();
const taskProgressRef = useRef(taskProgress);
const [cursorPosition, setCursorPosition] = useState(0);
@ -117,7 +119,8 @@ export const H5PhotoStoryDrawer = ({
String(User.id),
configOptions.mode as "auto" | "manual",
configOptions.resolution as "720p" | "1080p" | "4k",
configOptions.language
configOptions.language,
aspectUI === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '16:9' : '9:16'
);
if (!episodeResponse) return;
const episodeId = episodeResponse.project_id;
@ -355,7 +358,13 @@ export const H5PhotoStoryDrawer = ({
</div>
<div data-alt="bottom-action-bar" className="sticky bottom-0 left-0 right-0 backdrop-blur border-t border-white/10 px-3 py-2">
<div className="flex items-center justify-end">
<div className="flex items-center justify-end gap-2">
{/* 横/竖屏选择 */}
<AspectRatioSelector
value={aspectUI}
onChange={setAspectUI}
placement="top"
/>
{!hasAnalyzed ? (
<Tooltip title={activeImageUrl ? "Analyze image content" : "Please upload an image first"} placement="top">
<div>

View File

@ -11,6 +11,8 @@ import { useUploadFile } from "@/app/service/domain/service";
import { ActionButton } from "../common/ActionButton";
import GlobalLoad from "../common/GlobalLoad";
import { motion, AnimatePresence } from "framer-motion";
import { Dropdown } from "antd";
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
interface H5TemplateDrawerProps {
isMobile: boolean;
@ -73,6 +75,7 @@ export const H5TemplateDrawer = ({
const [isDescExpanded, setIsDescExpanded] = useState(false);
// 自由输入框布局
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
// 自由输入框布局
useEffect(() => {
@ -119,7 +122,8 @@ export const H5TemplateDrawer = ({
String(User.id),
configOptions.mode,
configOptions.resolution,
configOptions.language
configOptions.language,
aspectUI === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '16:9' : '9:16'
);
if (projectId) {
@ -552,6 +556,12 @@ export const H5TemplateDrawer = ({
/>
</div>
)}
{/* 横/竖屏选择 */}
<AspectRatioSelector
value={aspectUI}
onChange={setAspectUI}
placement="top"
/>
<ActionButton
isCreating={isTemplateCreating || localLoading > 0}
handleCreateVideo={handleConfirm}

View File

@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { Modal, Tooltip, Popconfirm, Upload } from "antd";
import { Modal, Tooltip, Popconfirm, Upload, Dropdown } from "antd";
import { ImagePlay, Sparkles, Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import GlobalLoad from "../common/GlobalLoad";
@ -9,6 +9,7 @@ import { ActionButton } from "../common/ActionButton";
import { HighlightEditor } from "../common/HighlightEditor";
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
import { useLoadScriptText } from "@/app/service/domain/service";
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
type ConfigOptions = {
mode: "auto" | "manual";
@ -61,6 +62,7 @@ export const PcPhotoStoryModal = ({
} = useImageStoryServiceHook();
const { loadingText } = useLoadScriptText(isLoading);
const [localLoading, setLocalLoading] = useState(0);
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
const router = useRouter();
const taskProgressRef = useRef(taskProgress);
const [cursorPosition, setCursorPosition] = useState(0);
@ -103,7 +105,8 @@ export const PcPhotoStoryModal = ({
String(User.id),
configOptions.mode as "auto" | "manual",
configOptions.resolution as "720p" | "1080p" | "4k",
configOptions.language
configOptions.language,
aspectUI === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '16:9' : '9:16'
);
if (!episodeResponse) return;
const episodeId = episodeResponse.project_id;
@ -317,7 +320,13 @@ export const PcPhotoStoryModal = ({
type={"role"}
placeholder="Share your creative ideas about the image and let AI create a movie story for you..."
/>
<div className="absolute bottom-1 right-0 flex gap-2">
<div className="absolute bottom-1 right-0 flex gap-2 items-center">
{/* 横/竖屏选择 */}
<AspectRatioSelector
value={aspectUI}
onChange={setAspectUI}
placement="top"
/>
{!hasAnalyzed ? (
<Tooltip title={activeImageUrl ? "Analyze image content" : "Please upload an image first"} placement="top">
<ActionButton

View File

@ -11,6 +11,7 @@ import {
Tooltip,
Upload,
Image,
Dropdown,
} from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
@ -20,6 +21,7 @@ import { useRouter } from "next/navigation";
import { useUploadFile } from "@/app/service/domain/service";
import { ActionButton } from "../common/ActionButton";
import GlobalLoad from "../common/GlobalLoad";
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
/**
*
@ -102,6 +104,7 @@ export const PcTemplateModal = ({
// 自由输入框布局
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
const router = useRouter();
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
// 组件挂载时获取模板列表
useEffect(() => {
@ -179,7 +182,8 @@ export const PcTemplateModal = ({
String(User.id),
configOptions.mode,
configOptions.resolution,
configOptions.language
configOptions.language,
aspectUI === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '16:9' : '9:16'
);
if (projectId) {
@ -703,6 +707,12 @@ export const PcTemplateModal = ({
/>
</div>
)}
{/* 横/竖屏选择 */}
<AspectRatioSelector
value={aspectUI}
onChange={setAspectUI}
placement="top"
/>
<ActionButton
isCreating={isTemplateCreating || localLoading > 0}
handleCreateVideo={handleConfirm}

View File

@ -0,0 +1,4 @@
export const AspectRatioOptions = [
{ value: "VIDEO_ASPECT_RATIO_LANDSCAPE", label: "16:9" },
{ value: "VIDEO_ASPECT_RATIO_PORTRAIT", label: "9:16" },
];