video-flow-b/components/pages/video-to-video.tsx
2025-06-24 20:12:30 +08:00

783 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<any>({
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<HTMLDivElement>(null);
const [showScrollNav, setShowScrollNav] = useState(false);
const [selectedVideoIndex, setSelectedVideoIndex] = useState<number | null>(null);
const videosContainerRef = useRef<HTMLDivElement>(null);
const scriptsContainerRef = useRef<HTMLDivElement>(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<void>((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' });
};
return (
<div
ref={containerRef}
className="container mx-auto overflow-auto custom-scrollbar"
style={isExpanded ? {height: 'calc(100vh - 12rem)'} : {height: 'calc(100vh - 20rem)'}}
>
{/* 展示创作详细过程 */}
{generateObj && (
<div className='video-creation-process-container mb-6'>
{generateObj.frame_urls && (
<div id='step-frame_urls' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='relative'>
<div className={`video-creation-process-item-content flex flex-wrap gap-2 ${!showAllFrames && 'max-h-[324px]'} overflow-hidden transition-all duration-300`}>
{generateObj.frame_urls.map((frame: string, index: number) => (
<div key={index} className="relative group">
<img
src={frame}
alt={`frame ${index + 1}`}
className='h-[100px] rounded-md object-cover transition-transform duration-200 group-hover:scale-105'
/>
<div className="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity duration-200 rounded-md flex items-center justify-center">
<span className="text-white/90 text-sm">Frame {index + 1}</span>
</div>
</div>
))}
</div>
{generateObj.frame_urls.length > maxVisibleImages && (
<div
className='absolute bottom-0 left-0 right-0 h-12 flex items-center justify-center bg-gradient-to-t from-black/20 to-transparent cursor-pointer'
onClick={() => setShowAllFrames(!showAllFrames)}
>
<div className='flex items-center gap-1 px-3 py-1 rounded-full bg-white/10 hover:bg-white/20 transition-colors'>
{showAllFrames ? (
<>
<ChevronUp className="w-4 h-4" />
<span className='text-sm'></span>
</>
) : (
<>
<ChevronDown className="w-4 h-4" />
<span className='text-sm'> ({generateObj.frame_urls.length} )</span>
</>
)}
</div>
</div>
)}
</div>
</div>
)}
{/* 视频信息 */}
{generateObj.video_info && (
<div id='step-video_info' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
{/* 展示:角色档案卡(头像、姓名、核心身份);场景;风格 */}
<div className='video-creation-process-item-content flex flex-col gap-6'>
{/* 角色档案卡 */}
<div className='space-y-4'>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<div className='flex flex-wrap gap-4'>
{generateObj.video_info.roles.map((role: any, index: number) => (
<div
key={index}
className='flex items-start gap-3 p-3 rounded-lg bg-white/[0.03] hover:bg-white/[0.05] transition-colors duration-200 min-w-[300px] max-w-[400px] group'
>
<div className='flex-shrink-0'>
<div className='w-[48px] h-[48px] rounded-full overflow-hidden border-2 border-white/10 group-hover:border-white/20 transition-colors duration-200'>
<img
src={role.avatar}
alt={role.name}
className='w-full h-full object-cover'
/>
</div>
</div>
<div className='flex-1 min-w-0'>
<div className='text-base font-medium mb-1 text-white/90'>{role.name}</div>
<div className='text-sm text-white/60 line-clamp-2'>{role.core_identity}</div>
</div>
</div>
))}
</div>
</div>
{/* 场景和风格 */}
<div className='space-y-4'>
<div className='video-creation-process-item-content-item-scene'>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed pl-1'>{generateObj.video_info.sence}</p>
</div>
<div className='video-creation-process-item-content-item-style'>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed pl-1'>{generateObj.video_info.style}</p>
</div>
</div>
</div>
</div>
)}
{/* 分镜脚本 */}
{generateObj.scripts && (
<div id='step-scripts' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='video-creation-process-item-content'>
<div className='flex flex-wrap gap-4'>
{generateObj.scripts.map((script: any, index: number) => (
<div
key={index}
className='flex-shrink-0 w-[360px] h-[400px] bg-white/[0.03] rounded-lg p-4 hover:bg-white/[0.05] transition-all duration-200 group'
>
{/* 序号 */}
<div className='flex items-center justify-between mb-3 pb-2 border-b border-white/10'>
<span className='text-lg font-medium text-blue-400/90'>Scene {index + 1}</span>
<div className='px-2 py-1 rounded-full bg-white/10 text-xs text-white/60'>
#{String(index + 1).padStart(2, '0')}
</div>
</div>
{/* 滚动内容区域 */}
<div className='h-[calc(100%-40px)] overflow-y-auto pr-2 space-y-4 custom-scrollbar'>
<div>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed'>{script.shot}</p>
</div>
<div>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed'>{script.frame}</p>
</div>
<div>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed'>{script.atmosphere}</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* 分镜视频 */}
{generateObj.scene_videos && (
<div id='step-scene_videos' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-sm font-medium'></span>
</div>
<div className='video-creation-process-item-content space-y-6'>
{/* 视频展示区 */}
<div
ref={videosContainerRef}
className='flex gap-4 overflow-x-auto pb-4 custom-scrollbar'
>
{generateObj.scripts.map((script: any, index: number) => {
const video = generateObj.scene_videos.find((v: any) => v.id === index);
const isSelected = selectedVideoIndex === index;
return (
<div
key={index}
id={`video-${index}`}
className={`flex-shrink-0 w-[320px] aspect-video rounded-lg overflow-hidden relative cursor-pointer transition-all duration-300
${isSelected ? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-[#191B1E]' : 'hover:ring-2 hover:ring-white/20'}`}
onClick={() => handleVideoSelect(index)}
>
{video ? (
<>
<video
src={video.video_url}
className="w-full h-full object-cover"
controls={isSelected}
/>
{!isSelected && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
<Play className="w-8 h-8 text-white/90" />
</div>
)}
</>
) : (
<div className="w-full h-full bg-white/5 flex items-center justify-center">
<Loader2 className="w-8 h-8 text-white/40 animate-spin" />
</div>
)}
<div className="absolute top-2 right-2 px-2 py-1 rounded-full bg-black/60 text-xs text-white/80">
Scene {index + 1}
</div>
</div>
);
})}
</div>
{/* 脚本展示区 */}
<div
ref={scriptsContainerRef}
className='flex gap-4 overflow-x-auto pb-4 custom-scrollbar'
>
{generateObj.scripts.map((script: any, index: number) => {
const isSelected = selectedVideoIndex === index;
return (
<div
key={index}
id={`script-${index}`}
className={`flex-shrink-0 w-[320px] p-4 rounded-lg cursor-pointer transition-all duration-300
${isSelected ? 'bg-blue-400/10 border border-blue-400/30' : 'bg-white/5 hover:bg-white/10'}`}
onClick={() => handleScriptSelect(index)}
>
<div className="text-xs text-white/40 mb-2">Scene {index + 1}</div>
<div className="text-sm text-white/80 line-clamp-4">{script.frame}</div>
</div>
);
})}
</div>
</div>
</div>
)}
{/* 剪辑后的视频 */}
{generateObj.cut_video_url && (
<div id='step-cut_video' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='video-creation-process-item-content'>
<div className='flex flex-wrap gap-4'>
<div className='flex-shrink-0 w-full bg-white/[0.03] rounded-lg p-4 hover:bg-white/[0.05] transition-all duration-200 group'>
<video src={generateObj.cut_video_url} className="w-full h-full object-cover" controls />
</div>
</div>
</div>
</div>
)}
{/* 口型同步后的视频 */}
{generateObj.audio_video_url && (
<div id='step-audio_video' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='video-creation-process-item-content'>
<div className='flex flex-wrap gap-4'>
<div className='flex-shrink-0 w-full bg-white/[0.03] rounded-lg p-4 hover:bg-white/[0.05] transition-all duration-200 group'>
<video src={generateObj.audio_video_url} className="w-full h-full object-cover" controls />
</div>
</div>
</div>
</div>
)}
{/* 最终视频 */}
{generateObj.final_video_url && (
<div id='step-final_video' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='video-creation-process-item-content'>
<div className='flex flex-wrap gap-4'>
<div className='flex-shrink-0 w-full bg-white/[0.03] rounded-lg p-4 hover:bg-white/[0.05] transition-all duration-200 group'>
<video src={generateObj.final_video_url} className="w-full h-full object-cover" controls />
</div>
</div>
</div>
</div>
)}
</div>
)}
{/* 回滚条 */}
<div
className="fixed right-8 top-1/2 -translate-y-1/2 z-50"
onMouseEnter={() => setShowScrollNav(true)}
onMouseLeave={() => setShowScrollNav(false)}
>
{/* 悬浮按钮 */}
<button
className={`flex items-center justify-center w-12 h-12 rounded-full bg-gradient-to-br from-blue-400/20 to-blue-600/20 backdrop-blur-lg hover:from-blue-400/30 hover:to-blue-600/30 transition-all duration-300 group
${showScrollNav ? 'opacity-0 scale-90' : 'opacity-100 scale-100'}`}
>
<ListOrdered className="w-5 h-5 text-white/70 group-hover:text-white/90 transition-colors" />
</button>
{/* 展开的回滚导航 */}
<div className={`absolute right-0 top-1/2 -translate-y-1/2 transition-all duration-300 ease-out
${showScrollNav ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-4 pointer-events-none'}`}
>
<div className="flex items-center gap-4 bg-gradient-to-b from-black/30 to-black/10 backdrop-blur-lg rounded-l-2xl pl-6 pr-6 py-6">
{generateObj && (
<>
{/* 进度条背景 */}
<div className="absolute w-[3px] h-[280px] bg-gradient-to-b from-white/5 to-white/0 rounded-full left-8" />
{/* 动态进度条 */}
<div className="absolute w-[3px] rounded-full transition-all duration-500 ease-out left-8 overflow-hidden"
style={{
height: generateObj.final_video_url ? '280px' :
generateObj.audio_video_url ? '240px' :
generateObj.cut_video_url ? '200px' :
generateObj.scene_videos ? '160px' :
generateObj.scripts ? '120px' :
generateObj.video_info ? '80px' :
generateObj.frame_urls ? '40px' : '0px',
top: '24px',
background: 'linear-gradient(180deg, rgba(96,165,250,0.7) 0%, rgba(96,165,250,0.3) 100%)',
boxShadow: '0 0 20px rgba(96,165,250,0.3)'
}}
/>
{/* 步骤按钮 */}
<div className="relative flex flex-col justify-between h-[280px] py-2">
{generateObj.frame_urls && (
<button
onClick={() => {
const element = document.getElementById('step-frame_urls');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.video_info && (
<button
onClick={() => {
const element = document.getElementById('step-video_info');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.scripts && (
<button
onClick={() => {
const element = document.getElementById('step-scripts');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.scene_videos && (
<button
onClick={() => {
const element = document.getElementById('step-scene_videos');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.cut_video_url && (
<button
onClick={() => {
const element = document.getElementById('step-cut_video');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.audio_video_url && (
<button
onClick={() => {
const element = document.getElementById('step-audio_video');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.final_video_url && (
<button
onClick={() => {
const element = document.getElementById('step-final_video');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
</div>
</>
)}
</div>
</div>
</div>
{/* 工具栏 */}
<div className='video-tool-component relative w-[1080px]'>
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#fff3] backdrop-blur-[15px]'>
{isExpanded ? (
<div className='absolute top-0 bottom-0 left-0 right-0 z-[9] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
{/* 图标 展开按钮 */}
<ChevronUp className='w-4 h-4' />
<span className='text-sm'>Click to create</span>
</div>
) : (
<div className='absolute top-[-8px] left-[50%] z-[10] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]' onClick={() => setIsExpanded(true)}>
{/* 图标 折叠按钮 */}
<ChevronDown className='w-4 h-4' />
</div>
)}
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 ${isExpanded ? 'h-[16px]' : 'h-[162px]'}`}>
<div className='video-creation-tool-container flex flex-col gap-4'>
<div className='relative flex items-center gap-4'>
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger' onClick={handleUploadVideo}>
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
{/* 图标 添加视频 */}
<Video className='w-4 h-4' />
</div>
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
<span className='text-xs cursor-[inherit]'>Add Video</span>
</div>
</div>
{videoUrl && (
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger'>
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
<video src={videoUrl} className='w-full h-full object-cover' />
</div>
</div>
)}
</div>
</div>
<div className='flex gap-3 justify-end'>
<div className='flex items-center gap-3'>
<div className='disabled tool-submit-button'>Stop</div>
<div className={`tool-submit-button ${videoUrl ? '' : 'disabled'}`} onClick={handleCreateVideo}>Create</div>
</div>
</div>
</div>
</div>
</div>
{/* Loading动画 */}
{isLoading && (
<div className='mt-8 flex justify-center'>
<div className='relative'>
{/* 外圈动画 */}
<div className='w-[40px] h-[40px] rounded-full bg-white/[0.05] flex items-center justify-center animate-bounce'>
{/* 中圈动画 */}
<div className='w-[20px] h-[20px] rounded-full bg-white/[0.05] flex items-center justify-center animate-pulse'>
{/* 内圈动画 */}
<div className='w-[10px] h-[10px] rounded-full bg-white/[0.05] flex items-center justify-center animate-ping'>
</div>
</div>
</div>
{/* Loading文字 */}
<div className='absolute top-[50px] left-1/2 -translate-x-1/2 whitespace-nowrap'>
<span className='text-white/70 text-sm animate-pulse inline-block'>{loadingText}</span>
<span className='inline-block ml-1 animate-bounce'>
<span className='inline-block animate-bounce delay-100'>.</span>
<span className='inline-block animate-bounce delay-200'>.</span>
<span className='inline-block animate-bounce delay-300'>.</span>
</span>
</div>
</div>
</div>
)}
</div>
);
}