"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 } from 'lucide-react'; import { useRouter } from 'next/navigation'; import './style/video-to-video.css'; // 添加自定义滚动条样式 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 VideoToVideo() { 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 [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); // 监听内容变化,自动滚动到底部 useEffect(() => { if (containerRef.current && (generateObj.scripts || generateObj.frame_urls || generateObj.video_info)) { setTimeout(() => { containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }); }, 100); // 给一个小延迟,确保内容已经渲染 } }, [generateObj.scripts, generateObj.frame_urls, generateObj.video_info, generateObj.scene_videos, generateObj.cut_video_url, generateObj.audio_video_url, generateObj.final_video_url]); // 计算每行可以显示的图片数量(基于图片高度100px和容器宽度) const imagesPerRow = Math.floor(1080 / (100 * 16/9 + 8)); // 假设图片宽高比16:9,间距8px // 计算三行可以显示的最大图片数量 const maxVisibleImages = imagesPerRow * 3; 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 * 2000); // 每个视频间隔2秒 }); }); // 等待所有视频生成完成 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, 1000)); setLoadingText('提取帧...'); // 生成帧 await new Promise(resolve => setTimeout(resolve, 1000)); 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, 2000)); 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, 2000)); 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, 2000)); 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, 2000)); 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, 2000)); 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' }); }; return (
{/* 展示创作详细过程 */} {generateObj && (
{generateObj.frame_urls && (
提取帧
{generateObj.frame_urls.map((frame: string, index: number) => (
{`frame
Frame {index + 1}
))}
{generateObj.frame_urls.length > maxVisibleImages && (
setShowAllFrames(!showAllFrames)} >
{showAllFrames ? ( <> 收起 ) : ( <> 展开全部 ({generateObj.frame_urls.length} 帧) )}
)}
)} {/* 视频信息 */} {generateObj.video_info && (
视频信息
{/* 展示:角色档案卡(头像、姓名、核心身份);场景;风格 */}
{/* 角色档案卡 */}
角色档案:
{generateObj.video_info.roles.map((role: any, index: number) => (
{role.name}
{role.name}
{role.core_identity}
))}
{/* 场景和风格 */}
场景

{generateObj.video_info.sence}

风格

{generateObj.video_info.style}

)} {/* 分镜脚本 */} {generateObj.scripts && (
分镜脚本
{generateObj.scripts.map((script: any, index: number) => (
{/* 序号 */}
Scene {index + 1}
#{String(index + 1).padStart(2, '0')}
{/* 滚动内容区域 */}
镜头

{script.shot}

场景

{script.frame}

氛围

{script.atmosphere}

))}
)} {/* 分镜视频 */} {generateObj.scene_videos && (
分镜视频
{/* 视频展示区 */}
{generateObj.scripts.map((script: any, index: number) => { const video = generateObj.scene_videos.find((v: any) => v.id === index); const isSelected = selectedVideoIndex === index; return (
handleVideoSelect(index)} > {video ? ( <>
); })}
{/* 脚本展示区 */}
{generateObj.scripts.map((script: any, index: number) => { const isSelected = selectedVideoIndex === index; return (
handleScriptSelect(index)} >
Scene {index + 1}
{script.frame}
); })}
)} {/* 剪辑后的视频 */} {generateObj.cut_video_url && (
剪辑后的视频
)} {/* 口型同步后的视频 */} {generateObj.audio_video_url && (
口型同步后的视频
)} {/* 最终视频 */} {generateObj.final_video_url && (
最终视频
)}
)} {/* 回滚条 */}
setShowScrollNav(true)} onMouseLeave={() => setShowScrollNav(false)} > {/* 悬浮按钮 */} {/* 展开的回滚导航 */}
{generateObj && ( <> {/* 进度条背景 */}
{/* 动态进度条 */}
{/* 步骤按钮 */}
{generateObj.frame_urls && ( )} {generateObj.video_info && ( )} {generateObj.scripts && ( )} {generateObj.scene_videos && ( )} {generateObj.cut_video_url && ( )} {generateObj.audio_video_url && ( )} {generateObj.final_video_url && ( )}
)}
{/* 工具栏 */}
{isExpanded ? (
setIsExpanded(false)}> {/* 图标 展开按钮 */} Click to create
) : (
setIsExpanded(true)}> {/* 图标 折叠按钮 */}
)}
{/* 图标 添加视频 */}
Add Video
{videoUrl && (
)}
Stop
Create
{/* Loading动画 */} {isLoading && (
{/* 外圈动画 */}
{/* 中圈动画 */}
{/* 内圈动画 */}
{/* Loading文字 */}
{loadingText} . . .
)}
); }