添加视频上传界面

This commit is contained in:
Kevin Wong
2026-01-19 18:22:18 +08:00
parent a467242041
commit 51f3ab67df
3 changed files with 158 additions and 16 deletions

34
Docs/DevLogs/Day5.md Normal file
View File

@@ -0,0 +1,34 @@
# Day 5: 前端视频上传功能
---
## 🆕 Web 视频上传功能 (15:32)
**需求**:用户需要通过 Web 界面上传原视频,而非手动放入目录
**实现**
- 后端 `POST /api/materials/` 已有完整上传接口
- 前端 `page.tsx` 新增上传 UI 组件
### 修改的文件
| 文件 | 修改内容 |
|------|----------|
| `frontend/src/app/page.tsx` | 添加上传按钮、进度条、handleUpload 函数 |
### 功能特性
1. **上传按钮**:紫色渐变样式,位于素材区块标题栏
2. **文件验证**:仅接受 MP4/MOV/AVI 格式
3. **进度显示**:使用 XMLHttpRequest 实时追踪上传进度
4. **自动刷新**:上传成功后自动刷新素材列表
5. **错误处理**:显示文件类型错误、网络错误等提示
---
## ✅ Day 5 完成事项
- [x] 添加视频上传 UI 组件
- [x] 实现上传进度显示
- [x] 上传成功后自动刷新素材列表
- [ ] 手动测试验证

View File

@@ -2,8 +2,8 @@
**项目**ViGent 数字人口播视频生成系统
**服务器**Dell R730 (2× RTX 3090 24GB)
**更新时间**2026-01-16
**整体进度**100%MuseTalk 口型同步完整修复,端到端验证通过
**更新时间**2026-01-19
**整体进度**100%Day 5 前端视频上传功能完成
## 📖 快速导航
@@ -16,7 +16,7 @@
| [时间线](#-时间线) | 开发历程 |
**相关文档**
- [Day 日志](file:///d:/CodingProjects/Antigravity/ViGent/Docs/DevLogs/) (Day1-4)
- [Day 日志](file:///d:/CodingProjects/Antigravity/ViGent/Docs/DevLogs/) (Day1-5)
- [部署指南](file:///d:/CodingProjects/Antigravity/ViGent/Docs/DEPLOY_MANUAL.md)
---
@@ -68,6 +68,11 @@
- [x] 视频合成 MP4 生成验证
- [x] 端到端流程完整测试
### 阶段八:前端功能增强 (Day 5)
- [x] Web 视频上传功能
- [x] 上传进度显示
- [x] 自动刷新素材列表
---
## 🛤️ 后续规划
@@ -163,5 +168,10 @@ Day 4: 口型同步完整修复 ✅ 完成
- audio_processor.py 音视频长度修复
- inference.py 错误日志增强
- MP4 视频合成验证通过
Day 5: 前端功能增强 ✅ 完成
- Web 视频上传功能
- 上传进度显示
- 自动刷新素材列表
```

View File

@@ -37,6 +37,9 @@ export default function Home() {
const [generatedVideo, setGeneratedVideo] = useState<string | null>(null);
const [fetchError, setFetchError] = useState<string | null>(null);
const [debugData, setDebugData] = useState<string>("");
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadError, setUploadError] = useState<string | null>(null);
// 可选音色
const voices = [
@@ -83,6 +86,58 @@ export default function Home() {
}
};
// 上传视频
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// 验证文件类型
const validTypes = ['.mp4', '.mov', '.avi'];
const ext = file.name.toLowerCase().slice(file.name.lastIndexOf('.'));
if (!validTypes.includes(ext)) {
setUploadError('仅支持 MP4、MOV、AVI 格式');
return;
}
setIsUploading(true);
setUploadProgress(0);
setUploadError(null);
const formData = new FormData();
formData.append('file', file);
// 使用 XMLHttpRequest 以获取上传进度
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
setUploadProgress(progress);
}
};
xhr.onload = () => {
setIsUploading(false);
if (xhr.status >= 200 && xhr.status < 300) {
fetchMaterials(); // 刷新素材列表
setUploadProgress(100);
} else {
setUploadError(`上传失败: ${xhr.statusText}`);
}
};
xhr.onerror = () => {
setIsUploading(false);
setUploadError('网络错误,上传失败');
};
xhr.open('POST', `${API_BASE}/api/materials/`);
xhr.send(formData);
// 清空 input 以便可以再次选择同一文件
e.target.value = '';
};
// 生成视频
const handleGenerate = async () => {
if (!selectedMaterial || !text.trim()) {
@@ -162,6 +217,24 @@ export default function Home() {
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
📹
</h2>
<div className="flex gap-2">
{/* 隐藏的文件输入 */}
<input
type="file"
id="video-upload"
accept=".mp4,.mov,.avi"
onChange={handleUpload}
className="hidden"
/>
<label
htmlFor="video-upload"
className={`px-3 py-1 text-xs rounded cursor-pointer transition-all ${isUploading
? "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"
}`}
>
📤
</label>
<button
onClick={fetchMaterials}
className="px-3 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300"
@@ -169,6 +242,36 @@ export default function Home() {
🔄
</button>
</div>
</div>
{/* 上传进度条 */}
{isUploading && (
<div className="mb-4 p-4 bg-purple-500/10 rounded-xl border border-purple-500/30">
<div className="flex justify-between text-sm text-purple-300 mb-2">
<span>📤 ...</span>
<span>{uploadProgress}%</span>
</div>
<div className="h-2 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: `${uploadProgress}%` }}
/>
</div>
</div>
)}
{/* 上传错误提示 */}
{uploadError && (
<div className="mb-4 p-4 bg-red-500/20 text-red-200 rounded-xl text-sm flex justify-between items-center">
<span> {uploadError}</span>
<button
onClick={() => setUploadError(null)}
className="text-red-300 hover:text-white"
>
</button>
</div>
)}
{fetchError ? (
<div className="p-4 bg-red-500/20 text-red-200 rounded-xl text-sm mb-4">
@@ -178,16 +281,11 @@ export default function Home() {
</div>
) : materials.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<div className="text-5xl mb-4">📁</div>
<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>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">