"use client"; import { useState, useEffect, useRef } from 'react'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import './style/create-to-video.css'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Skeleton } from "@/components/ui/skeleton"; // 添加自定义滚动条样式 const scrollbarStyles = ` .custom-scrollbar::-webkit-scrollbar { width: 4px; } .custom-scrollbar::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); border-radius: 2px; } .custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 2px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); } `; interface SceneVideo { id: number; video_url: string; script: any; } export function CreateToVideo() { const router = useRouter(); const [isUploading, setIsUploading] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [videoUrl, setVideoUrl] = useState(''); const [isLoading, setIsLoading] = useState(false); const [loadingText, setLoadingText] = useState('Generating...'); const [isPaused, setIsPaused] = useState(false); const [showMoreSettings, setShowMoreSettings] = useState(false); const [generateObj, setGenerateObj] = useState({ scripts: null, frame_urls: null, video_info: null, scene_videos: null, cut_video_url: null, audio_video_url: null, final_video_url: null }); const [showAllFrames, setShowAllFrames] = useState(false); const containerRef = useRef(null); const [showScrollNav, setShowScrollNav] = useState(false); const [selectedVideoIndex, setSelectedVideoIndex] = useState(null); const videosContainerRef = useRef(null); const scriptsContainerRef = useRef(null); const [activeTab, setActiveTab] = useState('clone'); const [editingField, setEditingField] = useState<{ type: 'shot' | 'frame' | 'atmosphere' | null; value: string; }>({ type: null, value: '' }); const [alternativeVideos, setAlternativeVideos] = useState<{ [key: number]: string[] }>({ 0: [ 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4', 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4', ], 1: [ 'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4', 'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4', ] }); const [currentVideoIndex, setCurrentVideoIndex] = useState<{ [key: number]: number }>({}); const [volume, setVolume] = useState<{ [key: number]: number }>({}); const [transition, setTransition] = useState<{ [key: number]: string }>({}); const [buttonPosition, setButtonPosition] = useState({ x: window.innerWidth - 100, y: window.innerHeight / 2 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const buttonRef = useRef(null); // 计算每行可以显示的图片数量(基于图片高度100px和容器宽度) const imagesPerRow = Math.floor(1080 / (100 * 16/9 + 8)); // 假设图片宽高比16:9,间距8px // 计算三行可以显示的最大图片数量 const maxVisibleImages = imagesPerRow * 3; // 生成分镜视频后默认选中第一个 useEffect(() => { if (generateObj.scene_videos?.length > 0 && selectedVideoIndex === null) { setSelectedVideoIndex(0); } }, [generateObj.scene_videos]); // 处理暂停/继续 const handlePauseResume = () => { setIsPaused(!isPaused); // TODO: 实现具体的暂停/继续逻辑 }; const handleUploadVideo = () => { console.log('upload video'); // 打开文件选择器 const input = document.createElement('input'); input.type = 'file'; input.accept = 'video/*'; input.onchange = (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) { setVideoUrl(URL.createObjectURL(file)); } } input.click(); } const generateSences = async () => { try { generateObj.scene_videos = []; const videoUrls = [ 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4', 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4', 'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4', 'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4', 'https://cdn.qikongjian.com/videos/1750303377_8c3c4ca6-c4ea-4376-8583-de3afa5681d8_text_to_video_0.mp4', 'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4' ]; setGenerateObj({...generateObj, scene_videos: []}); // 使用 Promise.all 和 Array.map 来处理异步操作 const promises = generateObj.scripts.map((element: any, index: number) => { return new Promise((resolveVideo) => { setTimeout(() => { generateObj.scene_videos.push({ id: index, video_url: videoUrls[index], script: element }); setGenerateObj({...generateObj}); setLoadingText(`生成第 ${index + 1} 个分镜视频...`); resolveVideo(); }, index * 3000); // 每个视频间隔3秒 }); }); // 等待所有视频生成完成 await Promise.all(promises); } catch (error) { console.error('生成分镜视频失败:', error); throw error; } } const handleCreateVideo = async () => { try { // 清空所有数据 setGenerateObj({ scripts: null, frame_urls: null, video_info: null, scene_videos: null, cut_video_url: null, audio_video_url: null, final_video_url: null }); console.log('create video'); setIsLoading(true); setIsExpanded(true); // 提取帧 await new Promise(resolve => setTimeout(resolve, 3000)); setLoadingText('提取帧...'); // 生成帧 await new Promise(resolve => setTimeout(resolve, 3000)); generateObj.frame_urls = [ "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000001.jpg/1750507511_tmphfb431oc_000001.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000002.jpg/1750507511_tmphfb431oc_000002.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000003.jpg/1750507511_tmphfb431oc_000003.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000004.jpg/1750507512_tmphfb431oc_000004.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000005.jpg/1750507512_tmphfb431oc_000005.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000006.jpg/1750507513_tmphfb431oc_000006.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000007.jpg/1750507514_tmphfb431oc_000007.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000008.jpg/1750507515_tmphfb431oc_000008.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000009.jpg/1750507515_tmphfb431oc_000009.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000010.jpg/1750507516_tmphfb431oc_000010.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000011.jpg/1750507516_tmphfb431oc_000011.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000012.jpg/1750507516_tmphfb431oc_000012.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000013.jpg/1750507517_tmphfb431oc_000013.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000014.jpg/1750507517_tmphfb431oc_000014.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000015.jpg/1750507517_tmphfb431oc_000015.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000016.jpg/1750507517_tmphfb431oc_000016.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000017.jpg/1750507517_tmphfb431oc_000017.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000018.jpg/1750507518_tmphfb431oc_000018.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000019.jpg/1750507520_tmphfb431oc_000019.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000020.jpg/1750507521_tmphfb431oc_000020.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000021.jpg/1750507523_tmphfb431oc_000021.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000022.jpg/1750507523_tmphfb431oc_000022.jpg", "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000023.jpg/1750507524_tmphfb431oc_000023.jpg", ]; setGenerateObj({...generateObj}); setLoadingText('分析视频...'); // 生成视频信息 await new Promise(resolve => setTimeout(resolve, 6000)); generateObj.video_info = { roles: [ { name: '雪 (YUKI)', core_identity: '一位接近二十岁或二十出头的年轻女性,东亚裔,拥有深色长发和刘海,五官柔和且富有表现力。', avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg', },{ name: '春 (HARU)', core_identity: '一位接近二十岁或二十出头的年轻男性,东亚裔,拥有深色、发型整洁的中长发和深思的气质。', avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg', } ], sence: '叙事在两个不同的时间段展开。现在时空设定在一个安静的乡下小镇,冬季时被大雪覆盖。记忆则设定在春夏两季,一个日本高中的校园内外,天气温暖而晴朗。', style: '电影感,照片般逼真,带有柔和、梦幻般的质感。其美学风格让人联想到日本的浪漫剧情片。现在时空的场景使用冷色、蓝色调的调色板,而记忆序列则沐浴在温暖的黄金时刻光晕中。大量使用浅景深、微妙的镜头光晕,以及平滑、富有情感的节奏。8K分辨率。' }; setGenerateObj({...generateObj}); setLoadingText('提取分镜脚本...'); // 生成分镜脚本 await new Promise(resolve => setTimeout(resolve, 6000)); generateObj.scripts = [ { shot: '面部特写,拉远至广角镜头。', frame: '序列以雪(YUKI)面部的特写开场,她闭着眼睛躺在一片纯净的雪地里。柔和的雪花轻轻飘落在她的黑发和苍白的皮肤上。摄像机缓慢拉远,形成一幅令人惊叹的广角画面,揭示出在黄昏时分,她是在一片广阔、寂静、白雪覆盖的景观中的一个渺小、孤独的身影。', atmosphere: '忧郁、宁静、寂静且寒冷。' }, { shot: '切至室内中景,随后是一个透过窗户的主观视角镜头。', frame: '我们切到雪(YUKI)在一个舒适、光线温暖的卧室里醒来。她穿着一件舒适的毛衣,从床上下来,走向一扇窗户。她的呼吸在冰冷的玻璃上凝成雾气。她的主观视角镜头揭示了外面的雪景,远处有一座红色的房子,以及一封信被放入邮箱的记忆,从而触发了一段闪回。', atmosphere: '怀旧、温暖、内省。' }, { shot: '跟踪镜头,随后是一系列切出镜头和特写。', frame: '记忆开始。一个跟踪镜头跟随着一群学生,包括雪(YUKI),他们在飘落的樱花花瓣构成的华盖下走路上学。场景切到一个阳光普照的教室。雪(YUKI)穿着校服,坐在她的课桌前,害羞地瞥了一眼坐在前几排的春(HARU)。他仿佛感觉到她的凝视,巧妙地转过头来。', atmosphere: '充满青春气息、怀旧、温暖,带有一种萌芽的、未言明的浪漫感。' }, { shot: '静态中景,切到另一个中景,营造出共享空间的感觉。', frame: '记忆转移到学校图书馆,充满了金色的光束。一个中景镜头显示春(HARU)靠在一个书架上,全神贯注地读一本书。然后摄像机切到雪(YUKI),她坐在附近的一张桌子旁,专注于画架上的一幅小画,当她感觉到他的存在时,嘴唇上泛起一丝淡淡的、秘密的微笑。', atmosphere: '平和、亲密、书卷气、温暖。' }, { shot: '雪(YUKI)的中景,过渡到通过相机镜头的特写主观视角。', frame: '在一个阳光明媚的运动日,雪(YUKI)站在运动场边缘,拿着一台老式相机。她举起相机,镜头推进到她透过取景器看的眼睛的特写。我们切到她的主观视角:除了站在运动场上、表情专注而坚定的春(HARU)之外,整个世界都是失焦的。', atmosphere: '充满活力、专注,一种投入的观察和遥远的钦佩感。' }, ]; setGenerateObj({...generateObj}); // 使用展开运算符创建新对象,确保触发更新 setLoadingText('生成分镜视频...'); // 生成分镜视频 await new Promise(resolve => setTimeout(resolve, 2000)); await generateSences(); setLoadingText('分镜剪辑...'); // 生成剪辑后的视频 await new Promise(resolve => setTimeout(resolve, 6000)); generateObj.cut_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; setGenerateObj({...generateObj}); setLoadingText('口型同步...'); // 口型同步后生成视频 await new Promise(resolve => setTimeout(resolve, 6000)); generateObj.audio_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; setGenerateObj({...generateObj}); setLoadingText('一致化处理...'); // 最终完成 await new Promise(resolve => setTimeout(resolve, 6000)); generateObj.final_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; setGenerateObj({...generateObj}); setLoadingText('完成'); setIsLoading(false); } catch (error) { console.error('视频生成过程出错:', error); setLoadingText('生成失败,请重试'); setIsLoading(false); } } // 处理视频选中 const handleVideoSelect = (index: number) => { setSelectedVideoIndex(index); // 滚动脚本到对应位置 const scriptElement = document.getElementById(`script-${index}`); scriptElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); }; // 处理脚本选中 const handleScriptSelect = (index: number) => { setSelectedVideoIndex(index); // 滚动视频到对应位置 const videoElement = document.getElementById(`video-${index}`); videoElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); }; // 处理脚本编辑 const handleEditScript = (type: 'shot' | 'frame' | 'atmosphere', value: string) => { setEditingField({ type, value }); }; // 处理脚本点击 const handleScriptClick = (type: 'shot' | 'frame' | 'atmosphere', value: string) => { if (editingField.type !== type) { handleEditScript(type, value); } }; // 自动保存脚本编辑 useEffect(() => { if (editingField.type && editingField.value && selectedVideoIndex !== null) { const timeoutId = setTimeout(() => { const updatedScripts = [...generateObj.scripts]; const fieldType = editingField.type as keyof typeof updatedScripts[number]; updatedScripts[selectedVideoIndex] = { ...updatedScripts[selectedVideoIndex], [fieldType]: editingField.value }; setGenerateObj({ ...generateObj, scripts: updatedScripts }); }, 500); // 500ms 防抖 return () => clearTimeout(timeoutId); } }, [editingField.value, editingField.type, selectedVideoIndex, generateObj]); // 刷新分镜脚本 const handleRefreshScript = () => { if (selectedVideoIndex === null) return; // TODO: 实现刷新脚本的逻辑 console.log('Refresh script for scene', selectedVideoIndex + 1); }; // 处理删除分镜视频 const handleDeleteScene = (index: number) => { if (generateObj.scene_videos) { const newSceneVideos = generateObj.scene_videos.filter((_: SceneVideo, i: number) => i !== index); setGenerateObj({ ...generateObj, scene_videos: newSceneVideos }); setSelectedVideoIndex(null); } }; // 处理重新生成分镜视频 const handleRegenerateScene = async (index: number) => { // TODO: 实现重新生成逻辑 console.log('Regenerate scene', index); }; // 处理切换其他生成的视频 const handleSwitchVideo = (sceneIndex: number, videoIndex: number) => { if (alternativeVideos[sceneIndex]?.[videoIndex]) { const newSceneVideos = [...generateObj.scene_videos]; newSceneVideos[sceneIndex] = { ...newSceneVideos[sceneIndex], video_url: alternativeVideos[sceneIndex][videoIndex] }; setGenerateObj({ ...generateObj, scene_videos: newSceneVideos }); setCurrentVideoIndex({ ...currentVideoIndex, [sceneIndex]: videoIndex }); } }; // 处理音量调节 const handleVolumeChange = (index: number, value: number) => { setVolume({ ...volume, [index]: value }); // TODO: 实现音量调节逻辑 }; // 处理转场设置 const handleTransitionChange = (index: number, value: string) => { setTransition({ ...transition, [index]: value }); // TODO: 实现转场效果逻辑 }; // 处理拖动开始 const handleDragStart = (e: React.MouseEvent) => { setIsDragging(true); setDragStart({ x: e.clientX - buttonPosition.x, y: e.clientY - buttonPosition.y }); }; // 处理拖动 const handleDrag = (e: MouseEvent) => { if (isDragging && buttonRef.current) { const newX = Math.min( Math.max(0, e.clientX - dragStart.x), window.innerWidth - buttonRef.current.offsetWidth ); const newY = Math.min( Math.max(0, e.clientY - dragStart.y), window.innerHeight - buttonRef.current.offsetHeight ); setButtonPosition({ x: newX, y: newY }); } }; // 处理拖动结束 const handleDragEnd = () => { setIsDragging(false); }; // 添加拖动事件监听 useEffect(() => { if (isDragging) { window.addEventListener('mousemove', handleDrag); window.addEventListener('mouseup', handleDragEnd); } return () => { window.removeEventListener('mousemove', handleDrag); window.removeEventListener('mouseup', handleDragEnd); }; }, [isDragging, dragStart]); return (
{/* 展示创作详细过程 */} {generateObj && (
{/* 第一行 - 占2/3高度 */} {/* 第一列 - 角色档案卡 */}

角色档案

{generateObj.video_info ? (
{generateObj.video_info.roles.map((role: any, index: number) => (
{role.name}
{role.name}
{role.core_identity}
))}
) : (
{[1, 2].map((i) => (
))}
)}
{/* 第二列 - 视频预览和加载状态 */}

视频预览

{isLoading ? (
{loadingText} . . .
) : generateObj.final_video_url ? (
{/* 第三列 - 概要信息 */}

概要信息

{generateObj.video_info ? (
场景

{generateObj.video_info.sence}

风格

{generateObj.video_info.style}

) : (
)}
{/* 第二行 - 占1/3高度 */} {/* 第一列 - 分镜脚本 */}

分镜脚本

{selectedVideoIndex !== null && ( Scene {selectedVideoIndex + 1} )}
{generateObj.scripts && selectedVideoIndex !== null ? (
镜头
{editingField.type === 'shot' ? (