Init: 初版代码

This commit is contained in:
Kevin Wong
2026-01-14 14:39:02 +08:00
parent 41c2e3f9d3
commit 302a43a22f
44 changed files with 9999 additions and 316 deletions

348
frontend/src/app/page.tsx Normal file
View File

@@ -0,0 +1,348 @@
"use client";
import { useState, useEffect } from "react";
const API_BASE = "http://127.0.0.1:8000";
// 类型定义
interface Material {
id: string;
name: string;
scene: string;
size_mb: number;
path: string;
}
interface Task {
task_id: string;
status: string;
progress: number;
message: string;
download_url?: string;
}
export default function Home() {
const [materials, setMaterials] = useState<Material[]>([]);
const [selectedMaterial, setSelectedMaterial] = useState<string>("");
const [text, setText] = useState<string>(
"大家好,欢迎来到我的频道,今天给大家分享一些有趣的内容。"
);
const [voice, setVoice] = useState<string>("zh-CN-YunxiNeural");
const [isGenerating, setIsGenerating] = useState(false);
const [currentTask, setCurrentTask] = useState<Task | null>(null);
const [generatedVideo, setGeneratedVideo] = useState<string | null>(null);
const [fetchError, setFetchError] = useState<string | null>(null);
const [debugData, setDebugData] = useState<string>("");
// 可选音色
const voices = [
{ id: "zh-CN-YunxiNeural", name: "云溪 (男声-年轻)" },
{ id: "zh-CN-YunjianNeural", name: "云健 (男声-新闻)" },
{ id: "zh-CN-YunyangNeural", name: "云扬 (男声-专业)" },
{ id: "zh-CN-XiaoxiaoNeural", name: "晓晓 (女声-活泼)" },
{ id: "zh-CN-XiaoyiNeural", name: "晓伊 (女声-温柔)" },
];
// 加载素材列表
useEffect(() => {
fetchMaterials();
}, []);
const fetchMaterials = async () => {
try {
setFetchError(null);
setDebugData("Loading...");
// Add timestamp to prevent caching
const url = `${API_BASE}/api/materials/?t=${new Date().getTime()}`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
const text = await res.text(); // Get raw text first
setDebugData(text.substring(0, 200) + (text.length > 200 ? "..." : "")); // Show preview
const data = JSON.parse(text);
setMaterials(data.materials || []);
if (data.materials?.length > 0) {
if (!selectedMaterial) {
setSelectedMaterial(data.materials[0].id);
}
}
} catch (error) {
console.error("获取素材失败:", error);
setFetchError(String(error));
setDebugData(`Error: ${String(error)}`);
}
};
// 生成视频
const handleGenerate = async () => {
if (!selectedMaterial || !text.trim()) {
alert("请选择素材并输入文案");
return;
}
setIsGenerating(true);
setGeneratedVideo(null);
try {
// 查找选中的素材对象以获取路径
const materialObj = materials.find(m => m.id === selectedMaterial);
if (!materialObj) {
alert("素材数据异常");
return;
}
// 创建生成任务
const res = await fetch(`${API_BASE}/api/videos/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
material_path: materialObj.path,
text: text,
voice: voice,
add_subtitle: true,
}),
});
const data = await res.json();
const taskId = data.task_id;
// 轮询任务状态
const pollTask = async () => {
const taskRes = await fetch(`${API_BASE}/api/videos/tasks/${taskId}`);
const taskData: Task = await taskRes.json();
setCurrentTask(taskData);
if (taskData.status === "completed") {
setGeneratedVideo(`${API_BASE}${taskData.download_url}`);
setIsGenerating(false);
} else if (taskData.status === "failed") {
alert("视频生成失败: " + taskData.message);
setIsGenerating(false);
} else {
setTimeout(pollTask, 1000);
}
};
pollTask();
} catch (error) {
console.error("生成失败:", error);
setIsGenerating(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
{/* Header */}
<header className="border-b border-white/10 bg-black/20 backdrop-blur-sm">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
<span className="text-3xl">🎬</span>
ViGent
</h1>
</div>
</header>
<main className="max-w-6xl mx-auto px-6 py-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 左侧: 输入区域 */}
<div className="space-y-6">
{/* 素材选择 */}
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
📹
</h2>
<button
onClick={fetchMaterials}
className="px-3 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300"
>
🔄
</button>
</div>
{fetchError ? (
<div className="p-4 bg-red-500/20 text-red-200 rounded-xl text-sm mb-4">
: {fetchError}
<br />
API: {API_BASE}/api/materials/
</div>
) : materials.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<p></p>
<p className="text-sm mt-2">
backend/uploads/materials/
</p>
<div className="mt-4 p-4 bg-black/40 rounded text-left text-xs font-mono text-gray-500 overflow-auto whitespace-pre-wrap break-all">
<p className="font-bold text-purple-400">Debug Info:</p>
<p>Time: {new Date().toLocaleTimeString()}</p>
<p>Items: {materials.length}</p>
<p className="mt-2 text-gray-400 border-t border-gray-700 pt-2">Raw Response:</p>
<p>{debugData}</p>
</div>
</div>
) : (
<div className="grid grid-cols-2 gap-3">
{materials.map((m) => (
<button
key={m.id}
onClick={() => setSelectedMaterial(m.id)}
className={`p-4 rounded-xl border-2 transition-all text-left ${selectedMaterial === m.id
? "border-purple-500 bg-purple-500/20"
: "border-white/10 bg-white/5 hover:border-white/30"
}`}
>
<div className="text-white font-medium truncate">
{m.scene || m.name}
</div>
<div className="text-gray-400 text-sm mt-1">
{m.size_mb.toFixed(1)} MB
</div>
</button>
))}
</div>
)}
</div>
{/* 文案输入 */}
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
</h2>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="请输入你想说的话..."
className="w-full h-40 bg-black/30 border border-white/10 rounded-xl p-4 text-white placeholder-gray-500 resize-none focus:outline-none focus:border-purple-500 transition-colors"
/>
<div className="flex justify-between mt-2 text-sm text-gray-400">
<span>{text.length} </span>
<span>: ~{Math.ceil(text.length / 4)} </span>
</div>
</div>
{/* 音色选择 */}
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
🎙
</h2>
<div className="grid grid-cols-2 gap-3">
{voices.map((v) => (
<button
key={v.id}
onClick={() => setVoice(v.id)}
className={`p-3 rounded-xl border-2 transition-all text-left ${voice === v.id
? "border-purple-500 bg-purple-500/20"
: "border-white/10 bg-white/5 hover:border-white/30"
}`}
>
<span className="text-white text-sm">{v.name}</span>
</button>
))}
</div>
</div>
{/* 生成按钮 */}
<button
onClick={handleGenerate}
disabled={isGenerating || !selectedMaterial}
className={`w-full py-4 rounded-xl font-bold text-lg transition-all ${isGenerating || !selectedMaterial
? "bg-gray-600 cursor-not-allowed text-gray-400"
: "bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white shadow-lg hover:shadow-purple-500/25"
}`}
>
{isGenerating ? (
<span className="flex items-center justify-center gap-3">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
... {currentTask?.progress || 0}%
</span>
) : (
"🚀 生成视频"
)}
</button>
</div>
{/* 右侧: 预览区域 */}
<div className="space-y-6">
{/* 进度显示 */}
{currentTask && isGenerating && (
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4">
</h2>
<div className="space-y-3">
<div className="h-3 bg-black/30 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-purple-500 to-pink-500 transition-all duration-300"
style={{ width: `${currentTask.progress}%` }}
/>
</div>
<p className="text-gray-300">{currentTask.message}</p>
</div>
</div>
)}
{/* 视频预览 */}
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
🎥
</h2>
<div className="aspect-video bg-black/50 rounded-xl overflow-hidden flex items-center justify-center">
{generatedVideo ? (
<video
src={generatedVideo}
controls
className="w-full h-full object-contain"
/>
) : (
<div className="text-gray-500 text-center">
<div className="text-5xl mb-4">📹</div>
<p></p>
</div>
)}
</div>
{generatedVideo && (
<a
href={generatedVideo}
download
className="mt-4 w-full py-3 rounded-xl bg-green-600 hover:bg-green-700 text-white font-medium flex items-center justify-center gap-2 transition-colors"
>
</a>
)}
</div>
</div>
</div>
</main>
{/* Footer */}
<footer className="border-t border-white/10 mt-12">
<div className="max-w-6xl mx-auto px-6 py-4 text-center text-gray-500 text-sm">
ViGent - MuseTalk + EdgeTTS
</div>
</footer>
</div>
);
}