forked from 77media/video-flow
416 lines
15 KiB
TypeScript
416 lines
15 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Separator } from '@/components/ui/separator';
|
||
import { ArrowRight, Sparkles, Users, FileText, Play, Pause, RefreshCw, Palette, Volume2 } from 'lucide-react';
|
||
import { Progress } from '@/components/ui/progress';
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||
import { motion } from 'framer-motion';
|
||
|
||
interface InputScriptStepProps {
|
||
onNext: () => void;
|
||
}
|
||
|
||
interface Character {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
personality: string;
|
||
appearance: string;
|
||
voice: string;
|
||
avatar: string;
|
||
fullBodyImage: string;
|
||
audioSample: string;
|
||
styles: string[];
|
||
currentStyle: number;
|
||
}
|
||
|
||
const aiModels = [
|
||
{ id: 'gpt-4', name: 'GPT-4 Turbo', description: 'Most advanced model with superior creativity' },
|
||
{ id: 'gpt-3.5', name: 'GPT-3.5 Turbo', description: 'Fast and efficient for most tasks' },
|
||
{ id: 'claude-3', name: 'Claude 3 Opus', description: 'Excellent for narrative and storytelling' },
|
||
];
|
||
|
||
const loadingSteps = [
|
||
{ text: "分析脚本内容...", progress: 20 },
|
||
{ text: "提取角色信息...", progress: 40 },
|
||
{ text: "生成角色形象...", progress: 60 },
|
||
{ text: "匹配音色特征...", progress: 80 },
|
||
{ text: "完成角色创建...", progress: 100 },
|
||
];
|
||
|
||
const mockCharacters: Character[] = [
|
||
{
|
||
id: "1",
|
||
name: "凤青楗",
|
||
description: "重生的凤凰,拥有强大的意志力,决心改变自己的命运",
|
||
personality: "坚强、勇敢、充满希望",
|
||
appearance: "优雅的凤凰形象,金色羽毛,炯炯有神的眼睛",
|
||
voice: "温暖而坚定的女声",
|
||
avatar: "https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300",
|
||
fullBodyImage: "https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=400",
|
||
audioSample: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav",
|
||
styles: [
|
||
"https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300",
|
||
"https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
|
||
"https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=300"
|
||
],
|
||
currentStyle: 0
|
||
},
|
||
{
|
||
id: "2",
|
||
name: "星光使者",
|
||
description: "掌控星辰力量的神秘角色,与凤青楗一同战斗",
|
||
personality: "智慧、冷静、神秘",
|
||
appearance: "星光环绕的身影,深邃的蓝色长袍",
|
||
voice: "低沉磁性的男声",
|
||
avatar: "https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
|
||
fullBodyImage: "https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=400",
|
||
audioSample: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav",
|
||
styles: [
|
||
"https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
|
||
"https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=300",
|
||
"https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300"
|
||
],
|
||
currentStyle: 0
|
||
}
|
||
];
|
||
|
||
// 新的Loading组件
|
||
const CharacterLoading = ({ step }: { step: string }) => {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center min-h-[300px] bg-gradient-to-b from-gray-900 to-black text-white relative overflow-hidden rounded-xl">
|
||
{/* 旋转粒子环 */}
|
||
<motion.div
|
||
className="absolute w-40 h-40 border-2 border-cyan-400 rounded-full opacity-30"
|
||
animate={{ rotate: 360 }}
|
||
transition={{ repeat: Infinity, duration: 3, ease: 'linear' }}
|
||
/>
|
||
|
||
{/* 中心波动光圈 */}
|
||
<motion.div
|
||
className="absolute w-20 h-20 bg-cyan-500/10 rounded-full blur-xl"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
opacity: [0.3, 0.6, 0.3]
|
||
}}
|
||
transition={{ repeat: Infinity, duration: 2 }}
|
||
/>
|
||
|
||
{/* 扫光线条 */}
|
||
<motion.div
|
||
className="absolute bottom-0 w-full h-1 bg-gradient-to-r from-transparent via-cyan-400 to-transparent blur"
|
||
animate={{ y: [-30, 300] }}
|
||
transition={{ repeat: Infinity, duration: 2, ease: 'easeInOut' }}
|
||
/>
|
||
|
||
{/* 核心文本 */}
|
||
<motion.div
|
||
className="relative z-10 mt-12 text-lg font-semibold text-cyan-300"
|
||
animate={{ opacity: [1, 0.4, 1] }}
|
||
transition={{ repeat: Infinity, duration: 1.5 }}
|
||
>
|
||
{step}
|
||
</motion.div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 角色卡片组件
|
||
const CharacterCard = ({
|
||
character,
|
||
onStyleChange,
|
||
onPlayAudio,
|
||
isPlaying
|
||
}: {
|
||
character: Character;
|
||
onStyleChange: (id: string, styleIndex: number) => void;
|
||
onPlayAudio: (id: string) => void;
|
||
isPlaying: string | null;
|
||
}) => (
|
||
<Card className="bg-gradient-to-br from-gray-800 to-gray-900 border-gray-600 overflow-hidden group hover:shadow-2xl transition-all duration-300 hover:scale-105">
|
||
<CardContent className="p-0">
|
||
{/* 角色头像区域 */}
|
||
<div className="relative">
|
||
<div className="aspect-[3/4] overflow-hidden bg-gradient-to-b from-blue-500/20 to-purple-500/20">
|
||
<img
|
||
src={character.fullBodyImage}
|
||
alt={character.name}
|
||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||
/>
|
||
{/* 渐变遮罩 */}
|
||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
||
|
||
{/* 角色名字 */}
|
||
<div className="absolute bottom-4 left-4 right-4">
|
||
<h3 className="text-white text-xl font-bold mb-2">{character.name}</h3>
|
||
<Badge variant="secondary" className="bg-blue-600/80 text-white">
|
||
{character.voice}
|
||
</Badge>
|
||
</div>
|
||
|
||
{/* 音频播放按钮 */}
|
||
<Button
|
||
size="sm"
|
||
variant="ghost"
|
||
className="absolute top-4 right-4 bg-black/50 hover:bg-black/70 text-white"
|
||
onClick={() => onPlayAudio(character.id)}
|
||
>
|
||
{isPlaying === character.id ? (
|
||
<Pause className="h-4 w-4" />
|
||
) : (
|
||
<Play className="h-4 w-4" />
|
||
)}
|
||
<Volume2 className="h-4 w-4 ml-1" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 详细信息 */}
|
||
<div className="p-4 space-y-3">
|
||
<div>
|
||
<Label className="text-sm font-medium text-gray-300">角色描述</Label>
|
||
<p className="text-sm text-gray-400 mt-1">{character.description}</p>
|
||
</div>
|
||
|
||
<div>
|
||
<Label className="text-sm font-medium text-gray-300">性格特征</Label>
|
||
<p className="text-sm text-gray-400 mt-1">{character.personality}</p>
|
||
</div>
|
||
|
||
<div>
|
||
<Label className="text-sm font-medium text-gray-300">外观特征</Label>
|
||
<p className="text-sm text-gray-400 mt-1">{character.appearance}</p>
|
||
</div>
|
||
|
||
{/* 样式切换 */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-sm font-medium text-gray-300">形象样式</Label>
|
||
<Button
|
||
size="sm"
|
||
variant="ghost"
|
||
className="text-blue-400 hover:text-blue-300"
|
||
onClick={() => onStyleChange(character.id, (character.currentStyle + 1) % character.styles.length)}
|
||
>
|
||
<Palette className="h-4 w-4 mr-1" />
|
||
切换
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="flex space-x-2">
|
||
{character.styles.map((style, index) => (
|
||
<button
|
||
key={index}
|
||
className={`w-12 h-12 rounded-lg overflow-hidden border-2 transition-all ${
|
||
character.currentStyle === index
|
||
? 'border-blue-500 shadow-lg'
|
||
: 'border-gray-600 hover:border-gray-500'
|
||
}`}
|
||
onClick={() => onStyleChange(character.id, index)}
|
||
>
|
||
<img src={style} alt={`Style ${index + 1}`} className="w-full h-full object-cover" />
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
|
||
// 角色生成结果组件
|
||
const CharacterGenerationResult = ({
|
||
characters,
|
||
onStyleChange,
|
||
onPlayAudio,
|
||
isPlaying,
|
||
onContinue
|
||
}: {
|
||
characters: Character[];
|
||
onStyleChange: (id: string, styleIndex: number) => void;
|
||
onPlayAudio: (id: string) => void;
|
||
isPlaying: string | null;
|
||
onContinue: () => void;
|
||
}) => (
|
||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-purple-900 p-6">
|
||
<div className="max-w-7xl mx-auto space-y-8">
|
||
{/* 标题区域 */}
|
||
<div className="text-center space-y-4">
|
||
<div className="inline-flex items-center space-x-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white px-6 py-3 rounded-full">
|
||
<Users className="h-5 w-5" />
|
||
<span className="font-medium">角色生成完成</span>
|
||
<Sparkles className="h-5 w-5" />
|
||
</div>
|
||
<h1 className="text-3xl font-bold text-white">您的故事角色</h1>
|
||
<p className="text-gray-300 max-w-2xl mx-auto">
|
||
AI已经根据您的脚本生成了{characters.length}个独特的角色,每个角色都有专属的形象和音色。
|
||
您可以试听音色、切换形象样式,满意后继续下一步。
|
||
</p>
|
||
</div>
|
||
|
||
{/* 角色网格 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||
{characters.map((character) => (
|
||
<div key={character.id} className="transform transition-all duration-500 hover:-translate-y-2">
|
||
<CharacterCard
|
||
character={character}
|
||
onStyleChange={onStyleChange}
|
||
onPlayAudio={onPlayAudio}
|
||
isPlaying={isPlaying}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex justify-center space-x-4 pt-8">
|
||
<Button
|
||
variant="outline"
|
||
size="lg"
|
||
className="border-gray-600 text-gray-300 hover:bg-gray-800"
|
||
>
|
||
<RefreshCw className="mr-2 h-5 w-5" />
|
||
重新生成
|
||
</Button>
|
||
<Button
|
||
size="lg"
|
||
onClick={onContinue}
|
||
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||
>
|
||
继续创作
|
||
<ArrowRight className="ml-2 h-5 w-5" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
export function InputScriptStep({ onNext }: InputScriptStepProps) {
|
||
const [script, setScript] = useState('');
|
||
const [chapters, setChapters] = useState('4');
|
||
const [shots, setShots] = useState('8');
|
||
const [showActorsPanel, setShowActorsPanel] = useState(false);
|
||
const [isGenerating, setIsGenerating] = useState(true);
|
||
const [showCharacters, setShowCharacters] = useState(false);
|
||
const [loadingStep, setLoadingStep] = useState(0);
|
||
const [characters, setCharacters] = useState<Character[]>(mockCharacters);
|
||
const [playingAudio, setPlayingAudio] = useState<string | null>(null);
|
||
|
||
// 模拟生成过程
|
||
useEffect(() => {
|
||
if (isGenerating) {
|
||
const timer = setInterval(() => {
|
||
setLoadingStep((prev) => {
|
||
if (prev >= loadingSteps.length - 1) {
|
||
clearInterval(timer);
|
||
setTimeout(() => {
|
||
setIsGenerating(false);
|
||
setShowCharacters(true);
|
||
}, 500);
|
||
return prev;
|
||
}
|
||
return prev + 1;
|
||
});
|
||
}, 1500);
|
||
|
||
return () => clearInterval(timer);
|
||
}
|
||
}, [isGenerating]);
|
||
|
||
const handleSubmit = () => {
|
||
if (script.trim() && chapters) {
|
||
setIsGenerating(true);
|
||
setLoadingStep(0);
|
||
}
|
||
};
|
||
|
||
const handleStyleChange = (characterId: string, styleIndex: number) => {
|
||
setCharacters(prev =>
|
||
prev.map(char =>
|
||
char.id === characterId
|
||
? { ...char, currentStyle: styleIndex, fullBodyImage: char.styles[styleIndex] }
|
||
: char
|
||
)
|
||
);
|
||
};
|
||
|
||
const handlePlayAudio = (characterId: string) => {
|
||
if (playingAudio === characterId) {
|
||
setPlayingAudio(null);
|
||
} else {
|
||
setPlayingAudio(characterId);
|
||
// 模拟音频播放,3秒后自动停止
|
||
setTimeout(() => setPlayingAudio(null), 3000);
|
||
}
|
||
};
|
||
|
||
const handleContinue = () => {
|
||
setShowCharacters(false);
|
||
onNext();
|
||
};
|
||
|
||
// 显示角色生成结果
|
||
if (showCharacters) {
|
||
return (
|
||
<CharacterGenerationResult
|
||
characters={characters}
|
||
onStyleChange={handleStyleChange}
|
||
onPlayAudio={handlePlayAudio}
|
||
isPlaying={playingAudio}
|
||
onContinue={handleContinue}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// 原始的脚本输入界面
|
||
return (
|
||
<div className="space-y-6">
|
||
<Card>
|
||
<CardContent className="space-y-6">
|
||
{/* Script Input */}
|
||
<div className="space-y-3">
|
||
<Label htmlFor="script" className="text-base font-medium">
|
||
Your Script
|
||
</Label>
|
||
<Textarea
|
||
id="script"
|
||
placeholder="Paste your script here... The AI will analyze it and break it into chapters with suggested actors and scenes."
|
||
value={script}
|
||
onChange={(e) => setScript(e.target.value)}
|
||
className="min-h-[200px] resize-none"
|
||
/>
|
||
<div className="flex justify-between text-sm text-muted-foreground">
|
||
<span>{script.length} characters</span>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Loading Animation - 显示在输入框下方 */}
|
||
{isGenerating && (
|
||
<CharacterLoading step={loadingSteps[loadingStep].text} />
|
||
)}
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex justify-end">
|
||
<Button
|
||
onClick={handleSubmit}
|
||
disabled={!script.trim() || isGenerating}
|
||
size="lg"
|
||
>
|
||
<Sparkles className="mr-2 h-4 w-4" />
|
||
Generate
|
||
<ArrowRight className="ml-2 h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |