4e06f4abe2
- 后端: 空镜素材迁移到 config/materials.json,duration从文件名_{N}s_自动解析
- 后端: 新增 POST /api/v1/materials/match 接口,后端做关键词匹配
- 前端: VideoGeneration 空镜匹配改为调用后端接口
- 前端: 人物出镜素材改为本地文件选择器直接选取,不走素材库
- 前端: 视频生成流程简化,移除Vidu对口型和七牛云上传
- Rust: 视频合成支持从随机起始时间截取人物素材片段
- Rust: 修复ffprobe参数错误(添加-show_entries format=duration)
11 KiB
11 KiB
视频生成实现方案 v3(Vidu 对口型版)
状态:待审阅
一、流程概述
| Step | 页面 | 产出 |
|---|---|---|
| 1 | ScriptCreation | 分镜列表(含 scene + duration) |
| 2 | VoiceDubbing | 配音音频(已存七牛云) |
| 3 | VideoGeneration | 最终视频(Vidu 对口型后) |
| 4 | SubtitleBurning | 字幕压制 |
| 5 | CoverDesign | 封面 |
| 6 | VideoComposite | 最终成品(如需要额外合成) |
二、Step 3 完整流程
┌─────────────────────────────────────────────────────────────┐
│ Step 3: 视频生成 │
│ │
│ 1. 选择形象素材(本地 avatar_materials) │
│ └─ 上传 / 选择已有素材 │
│ │
│ 2. 匹配空镜素材(七牛云) │
│ └─ 根据 scene 关键词匹配 │
│ └─ 检查时长 >= 分镜 duration │
│ │
│ 3. FFmpeg 裁剪 │
│ └─ segment:从形象素材中截取 duration 秒 │
│ └─ empty_shot:从空镜素材中截取 duration 秒 │
│ └─ 输出:clip_001.mp4, clip_002.mp4, ... │
│ │
│ 4. FFmpeg 拼接(移除音频) │
│ └─ concat 所有 clip → raw_video.mp4(无声) │
│ │
│ 5. 上传临时视频 │
│ └─ raw_video.mp4 → 七牛云临时 URL │
│ │
│ 6. Vidu 对口型 │
│ └─ POST /ent/v2/lip-sync │
│ { video_url: 临时URL, audio_url: Step2音频URL } │
│ └─ 返回 task_id │
│ │
│ 7. 轮询等待 │
│ └─ GET /ent/v2/tasks/{task_id}/creations │
│ └─ state = "success" 时获取 video_url │
│ │
│ 8. 下载最终视频 │
│ └─ 下载到本地 products/{project_id}/ │
│ │
│ 9. 上传永久保存 │
│ └─ 上传七牛云 videos/{project_id}/final.mp4 │
│ │
│ 10. 更新项目状态 │
│ └─ segment.videoPath = 本地路径 │
│ └─ segment.videoUrl = 七牛云永久 URL │
└─────────────────────────────────────────────────────────────┘
三、素材体系
3.1 人物出镜素材(本地)
存储位置:~/Documents/Meijiaka-zj/avatar_materials/
索引:~/Documents/Meijiaka-zj/avatar_materials_index.json
{
"materials": [
{
"id": "avt_001",
"name": "男主播正面",
"filename": "host_male.mp4",
"duration": 15.2,
"uploadedAt": "2026-04-22T10:00:00Z"
}
]
}
时长获取:上传后用 ffprobe 提取。
3.2 空镜素材(七牛云)
URL 格式:https://media.liche.cn/meijiaka-zj/material/{slug}/{slug}_{序号}_{时长}s.mp4
映射表:tauri-app/src/constants/materialUrls.ts
export interface MaterialInfo {
url: string;
duration: number;
}
export const MATERIAL_URLS: Record<string, MaterialInfo[]> = {
"ceiling": [
{ url: "https://media.liche.cn/meijiaka-zj/material/ceiling/ceiling_001_5s.mp4", duration: 5 },
],
"plumbing": [
{ url: "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_153_4s.mp4", duration: 4 },
],
"paint": [
{ url: "https://media.liche.cn/meijiaka-zj/material/paint/paint_001_5s.mp4", duration: 5 },
],
"final": [
{ url: "https://media.liche.cn/meijiaka-zj/material/final/final_001_7s.mp4", duration: 7 },
],
};
export const KEYWORD_MAP: Record<string, string> = {
"吊顶": "ceiling",
"天花": "ceiling",
"水电": "plumbing",
"水管": "plumbing",
"油漆": "paint",
"涂料": "paint",
"刷漆": "paint",
"完工": "final",
"竣工": "final",
"交付": "final",
};
四、匹配规则(含时长检查)
4.1 人物出镜素材选择
用户手动选择一个本地素材作为"当前形象"。
系统检查:
let maxSegmentDuration = max(所有 segment 分镜的 duration);
if 形象素材.duration < maxSegmentDuration:
警告:形象素材时长不足
4.2 空镜素材匹配
function matchMaterial(scene: string, requiredDuration: number): MaterialInfo | null {
for (const [keyword, slug] of Object.entries(KEYWORD_MAP)) {
if (scene.includes(keyword)) {
const candidates = MATERIAL_URLS[slug]?.filter(m => m.duration >= requiredDuration);
if (candidates && candidates.length > 0) {
return candidates[Math.floor(Math.random() * candidates.length)];
}
}
}
return null;
}
五、裁剪与拼接
5.1 裁剪
每个分镜从素材中截取 duration 秒:
| 场景 | 素材时长 | 截取方式 |
|---|---|---|
| 等于 | 5s | 从头截取 0-5s |
| 大于 | 8s | 随机从 [0, 3s] 开始截取 |
ffmpeg -ss {start} -t {duration} -i {input} -c:v libx264 -preset fast -an -y {output}
-an 确保输出无音频。
5.2 拼接(无声)
# clips.txt
file 'clip_001.mp4'
file 'clip_002.mp4'
...
# 拼接,无音频
ffmpeg -f concat -safe 0 -i clips.txt -c copy -an raw_video.mp4
5.3 上传临时视频
使用七牛云 SDK 上传 raw_video.mp4,获取临时访问 URL。
5.4 Vidu 对口型
POST https://api.vidu.cn/ent/v2/lip-sync
{
"video_url": "https://media.liche.cn/tmp/raw_video_xxx.mp4",
"audio_url": "https://media.liche.cn/audios/project_xxx.mp3"
}
响应:{ task_id: "xxx", state: "created" }
轮询:
GET https://api.vidu.cn/ent/v2/tasks/{task_id}/creations
成功响应:
{
"state": "success",
"creations": [{ "url": "https://.../final.mp4" }]
}
5.5 下载与保存
- 下载 Vidu 返回的
url到本地 - 本地路径:
~/Documents/Meijiaka-zj/products/{project_id}/final.mp4 - 上传七牛云:
videos/{project_id}/final.mp4 - 更新项目数据
六、Step 3 页面交互
┌─────────────────────────────────────────┐
│ Step 3: 视频生成 │
│ │
│ ┌─ 人物形象 ──────────────────────────┐ │
│ │ [上传] 或 选择已有 │ │
│ │ ● 男主播正面 (15.2s) │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌─ 空镜匹配 ──────────────────────────┐ │
│ │ 分镜2:瓦工贴墙砖 (5s) │ │
│ │ ✓ ceiling_001 (5s) │ │
│ │ │ │
│ │ 分镜4:防水施工 (4s) │ │
│ │ ✗ 素材不足 (plumbing 只有 4s?) │ │
│ │ [手动选择本地文件] │ │
│ └──────────────────────────────────────┘ │
│ │
│ [生成视频] │
│ │
│ 进度: │
│ 裁剪中... │
│ 拼接中... │
│ 上传中... │
│ Vidu 处理中... (task_id: xxx) │
│ 下载中... │
│ 完成 ✓ │
└─────────────────────────────────────────┘
七、实施清单
Phase 1:人物出镜素材库
| # | 文件 | 内容 |
|---|---|---|
| 1 | src-tauri/src/commands/avatar_material.rs |
IPC:上传、列表、删除 |
| 2 | src-tauri/src/storage/avatar_material.rs |
索引读写、ffprobe 提取时长 |
| 3 | src/api/modules/avatarMaterial.ts |
前端 API 封装 |
| 4 | VideoGeneration.tsx |
形象上传 + 选择组件 |
Phase 2:空镜素材匹配
| # | 文件 | 内容 |
|---|---|---|
| 5 | src/constants/materialUrls.ts |
映射表(按实际 URL 格式) |
| 6 | VideoGeneration.tsx |
自动匹配逻辑 |
Phase 3:Vidu 合成流水线
| # | 文件 | 内容 |
|---|---|---|
| 7 | src-tauri/src/ffmpeg_cmd.rs |
新增:批量裁剪 + 无声拼接 |
| 8 | python-api/app/services/vidu_service.py |
Vidu API 封装(对口型 + 轮询) |
| 9 | python-api/app/api/v1/vidu.py |
后端路由:提交对口型、查询状态 |
| 10 | src-tauri/src/commands/video_generate.rs |
IPC:生成视频主流程 |
| 11 | VideoGeneration.tsx |
[生成视频] 按钮 + 进度显示 |
八、确认事项
- 人物出镜素材本地管理(上传/选择/时长检查)
- 空镜素材七牛云映射(关键词 → slug → URL)
- FFmpeg 裁剪 + 无声拼接
- 拼接后视频上传七牛云临时 URL
- Vidu 对口型 API(提交 + 轮询)
- 最终视频下载到本地 products/ + 上传七牛云永久保存
- Step 2 音频已在七牛云,直接取 URL
确认后我开始写。