diff --git a/api/DTO/movie_start_dto.ts b/api/DTO/movie_start_dto.ts index 0180ed8..220fc95 100644 --- a/api/DTO/movie_start_dto.ts +++ b/api/DTO/movie_start_dto.ts @@ -334,3 +334,47 @@ export interface TextToImageResponse { /** 是否成功 */ successful: boolean; } + +/** + * 创建电影V4接口请求数据 + */ +export interface CreateMovieProjectV4Request { + /** 剧本内容 */ + script: string; + /** 模式 */ + mode: "auto" | "manual"; + /** 分辨率 */ + resolution: "720p" | "1080p" | "4k"; + /** 语言 */ + language: string; + /** 画面比例(横/竖屏) */ + aspect_ratio: AspectRatioValue; + /** 扩展模式 */ + expansion_mode: boolean; + /** 视频时长 */ + video_duration: string; + /** 是否是图生 */ + is_image_to_video: boolean; + /** pcode编码 */ + pcode: string; + /** 角色简介数组 */ + character_briefs?: { + character_name:string; + character_description:string; + image_url:string; + }[]; + /** 场景简介数组 */ + scene_briefs?: { + scene_name:string; + scene_description:string; + image_url:string; + scene_type:string; + }[]; + /** 道具简介数组 */ + prop_briefs?: { + prop_name:string; + prop_description:string; + image_url:string; + prop_type:string; + }[]; +} \ No newline at end of file diff --git a/api/create_movie.ts b/api/create_movie.ts index 89db035..3801262 100644 --- a/api/create_movie.ts +++ b/api/create_movie.ts @@ -1,6 +1,7 @@ import { CreateMovieProjectV2Request, CreateMovieProjectV3Request, + CreateMovieProjectV4Request } from "./DTO/movie_start_dto"; import { post } from "./request"; import { getClientUserData } from './common'; @@ -40,4 +41,18 @@ export const createMovieProjectV3 = async ( post("/movie/create_movie_project_v3", params); const user_data = getClientUserData(); return withQueuePolling(apiCall, { ...(request as any), user_data } as any); +}; + +/** + * 创建电影项目V4 + * @param request - 创建项目请求参数 + * @returns Promise + */ +export const createMovieProjectV4 = async ( + request: CreateMovieProjectV4Request +): Promise => { + const apiCall = (params: CreateMovieProjectV4Request) => + post("/movie/create_movie_project_v4", params); + const user_data = getClientUserData(); + return withQueuePolling(apiCall, { ...(request as any), user_data } as any); }; \ No newline at end of file diff --git a/app/service/Interaction/MovieProjectService.ts b/app/service/Interaction/MovieProjectService.ts index 428bd44..6c7f1d7 100644 --- a/app/service/Interaction/MovieProjectService.ts +++ b/app/service/Interaction/MovieProjectService.ts @@ -1,5 +1,4 @@ - -import { createMovieProject, createMovieProjectV2, createMovieProjectV3 } from "@/api/create_movie"; +import { createMovieProject, createMovieProjectV2, createMovieProjectV3, createMovieProjectV4 } from "@/api/create_movie"; import { QueueResponse, withQueuePolling, QueueResponseData } from "@/api/movie_queue"; /** @@ -11,7 +10,9 @@ export enum MovieProjectMode { /** 照片生成模式 */ IMAGE = "image", /** 模板生成模式 */ - TEMPLATE = "template" + TEMPLATE = "template", + /** V4增强模式(支持角色、场景、道具等) */ + V4 = "v4" } /** 创建项目响应数据 */ @@ -43,6 +44,9 @@ export class MovieProjectService { case MovieProjectMode.TEMPLATE: apiCall = createMovieProjectV3 as (p: T) => Promise; break; + case MovieProjectMode.V4: + apiCall = createMovieProjectV4 as (p: T) => Promise; + break; default: throw new Error(`不支持的创建模式: ${mode}`); } diff --git a/components/pages/create-video/CreateInput/ConfigPanel.tsx b/components/pages/create-video/CreateInput/ConfigPanel.tsx index 0f8ceec..ea4d917 100644 --- a/components/pages/create-video/CreateInput/ConfigPanel.tsx +++ b/components/pages/create-video/CreateInput/ConfigPanel.tsx @@ -100,7 +100,14 @@ export const ConfigPanel = ({ onConfigChange('videoDuration', key as VideoDurationValue), + onClick: ({ key }) => { + onConfigChange('videoDuration', key as VideoDurationValue); + if (key === '8s') { + onConfigChange('expansion_mode', false); + } else { + onConfigChange('expansion_mode', true); + } + }, className: 'bg-[#1a1a1a] border border-white/10' }} trigger={['click']} diff --git a/components/pages/create-video/CreateInput/VideoCreationForm.tsx b/components/pages/create-video/CreateInput/VideoCreationForm.tsx index 42c52df..f01d29e 100644 --- a/components/pages/create-video/CreateInput/VideoCreationForm.tsx +++ b/components/pages/create-video/CreateInput/VideoCreationForm.tsx @@ -18,7 +18,10 @@ import { AddItemModal } from './AddItemModal'; import { defaultConfig } from './config-options'; import type { ConfigOptions } from './config-options'; import { useDeviceType } from '@/hooks/useDeviceType'; - +import { MovieProjectService, MovieProjectMode } from '@/app/service/Interaction/MovieProjectService'; +import type { CreateMovieProjectV4Request } from '@/api/DTO/movie_start_dto'; +import { getCurrentUser } from '@/lib/auth'; +import { useRouter } from 'next/navigation'; export default function VideoCreationForm() { const [photos, setPhotos] = useState([]); const [inputText, setInputText] = useState(''); @@ -28,9 +31,10 @@ export default function VideoCreationForm() { const [currentItemType, setCurrentItemType] = useState(null); const [editingIndex, setEditingIndex] = useState(null); const [replacingIndex, setReplacingIndex] = useState(null); + const [isCreating, setIsCreating] = useState(false); const { isMobile, isDesktop } = useDeviceType(); - + const router = useRouter(); const characterInputRef = useRef(null); const sceneInputRef = useRef(null); const propInputRef = useRef(null); @@ -74,13 +78,6 @@ export default function VideoCreationForm() { setConfigOptions(prev => ({ ...prev, [key]: value })); }; - /** Handle opening add item modal */ - const handleOpenAddItemModal = (type: PhotoType) => { - setCurrentItemType(type); - setEditingIndex(null); - setAddItemModalVisible(true); - }; - /** Handle editing a photo */ const handleEditPhoto = (index: number) => { const photo = photos[index]; @@ -165,13 +162,92 @@ export default function VideoCreationForm() { }; /** Handle video creation */ - const handleCreate = () => { - console.log({ - text: inputText, - photos, - config: configOptions, - }); - // TODO: Implement video creation logic + const handleCreate = async () => { + if (isCreating) return; + + if (!inputText.trim()) { + window.msg?.warning('Please enter your story description'); + return; + } + + try { + setIsCreating(true); + + const user = getCurrentUser(); + if (!user?.id) { + window.msg?.error('Please login first'); + return; + } + + /** Separate photos by type */ + const characterPhotos = photos.filter(p => p.type === 'character'); + const scenePhotos = photos.filter(p => p.type === 'scene'); + const propPhotos = photos.filter(p => p.type === 'prop'); + + /** Build request parameters */ + const requestParams: CreateMovieProjectV4Request = { + script: inputText, + mode: configOptions.mode, + resolution: '720p', + language: configOptions.language, + aspect_ratio: configOptions.aspect_ratio, + expansion_mode: configOptions.expansion_mode, + video_duration: configOptions.videoDuration, + is_image_to_video: photos.length > 0, + pcode: configOptions.pcode === 'portrait' ? '' : configOptions.pcode, + }; + + /** Add character briefs if exists */ + if (characterPhotos.length > 0) { + requestParams.character_briefs = characterPhotos.map(photo => ({ + character_name: photo.name || 'Character', + character_description: photo.description || '', + image_url: photo.url, + })); + } + + /** Add scene briefs if exists */ + if (scenePhotos.length > 0) { + requestParams.scene_briefs = scenePhotos.map(photo => ({ + scene_name: photo.name || 'Scene', + scene_description: photo.description || '', + image_url: photo.url, + scene_type: 'custom', + })); + } + + /** Add prop briefs if exists */ + if (propPhotos.length > 0) { + requestParams.prop_briefs = propPhotos.map(photo => ({ + prop_name: photo.name || 'Prop', + prop_description: photo.description || '', + image_url: photo.url, + prop_type: 'custom', + })); + } + + console.log('Creating video with params:', requestParams); + + /** Call MovieProjectService V4 API */ + const result = await MovieProjectService.createProject( + MovieProjectMode.V4, + requestParams + ); + + console.log('Video creation successful, project_id:', result.project_id); + window.msg?.success(`Video created successfully! Project ID: ${result.project_id}`); + router.push(`/movies/work-flow?episodeId=${result.project_id}`); + + /** TODO: Navigate to project detail page or next step */ + // window.location.href = `/movies/${result.project_id}`; + } catch (error) { + console.error('Failed to create video:', error); + if (error instanceof Error && error.message !== '操作已取消') { + window.msg?.error(error.message || 'Failed to create video'); + } + } finally { + setIsCreating(false); + } }; return ( @@ -298,10 +374,15 @@ export default function VideoCreationForm() { {/* Right Side - Create Button */} diff --git a/components/pages/create-video/CreateInput/config-options.ts b/components/pages/create-video/CreateInput/config-options.ts index fd94b1d..b8c6e8c 100644 --- a/components/pages/create-video/CreateInput/config-options.ts +++ b/components/pages/create-video/CreateInput/config-options.ts @@ -43,14 +43,16 @@ export interface ConfigOptions { videoDuration: VideoDurationValue; aspect_ratio: AspectRatioValue; pcode: PortraitAnimeValue; + mode: "auto" | "manual"; } /** Default configuration */ export const defaultConfig: ConfigOptions = { language: 'english', - expansion_mode: false, + expansion_mode: true, videoDuration: 'unlimited', aspect_ratio: 'VIDEO_ASPECT_RATIO_LANDSCAPE', pcode: 'portrait', + mode: 'auto', };