|
|
@@ -0,0 +1,1471 @@
|
|
|
+# 文本自动生成视频系统 — 详细技术设计方案
|
|
|
+
|
|
|
+> 版本: v1.0
|
|
|
+> 日期: 2026-05-29
|
|
|
+> 状态: 设计阶段
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 目录
|
|
|
+
|
|
|
+1. [概述与目标](#一概述与目标)
|
|
|
+2. [核心链路](#二核心链路)
|
|
|
+3. [第一层:文本解析层](#三第一层文本解析层)
|
|
|
+4. [第二层:素材生成层](#四第二层素材生成层)
|
|
|
+5. [第三层:视频合成层](#五第三层视频合成层)
|
|
|
+6. [第四层:输出交付层](#六第四层输出交付层)
|
|
|
+7. [服务端架构](#七服务端架构)
|
|
|
+8. [数据库设计](#八数据库设计)
|
|
|
+9. [API 设计](#九api-设计)
|
|
|
+10. [核心难点与解决方案](#十核心难点与解决方案)
|
|
|
+11. [与现有项目融合](#十一与现有项目融合)
|
|
|
+12. [MVP 实施计划](#十二mvp-实施计划)
|
|
|
+13. [成本估算](#十三成本估算)
|
|
|
+14. [附录](#十四附录)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、概述与目标
|
|
|
+
|
|
|
+### 1.1 产品定义
|
|
|
+
|
|
|
+用户输入一段文本(故事、新闻、教程、文案),系统自动生成一段完整视频,包含:
|
|
|
+- 多个画面分镜
|
|
|
+- AI 配音旁白
|
|
|
+- 背景音乐
|
|
|
+- 字幕
|
|
|
+- 转场特效
|
|
|
+
|
|
|
+### 1.2 目标用户
|
|
|
+
|
|
|
+| 用户类型 | 典型场景 | 视频时长 | 频率 |
|
|
|
+|---------|---------|---------|------|
|
|
|
+| 内容创作者 | 小说转视频发抖音/快手 | 1-5分钟 | 高频 |
|
|
|
+| 自媒体运营 | 新闻/干货转短视频 | 30秒-2分钟 | 高频 |
|
|
|
+| 教育培训 | 教材/教程转教学视频 | 5-30分钟 | 中频 |
|
|
|
+| 企业 | 产品介绍/宣传视频 | 1-3分钟 | 低频 |
|
|
|
+
|
|
|
+### 1.3 设计原则
|
|
|
+
|
|
|
+1. **先出草稿,再精修** — 30 秒生成低分辨率预览,用户确认后再高清渲染
|
|
|
+2. **每个阶段可干预** — 分镜脚本、配音、画面、配乐全部支持用户调整
|
|
|
+3. **多模型热备** — 任何 AI 服务挂了,自动切换到备用方案
|
|
|
+4. **按需计费** — 按视频时长和分辨率阶梯定价
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、核心链路
|
|
|
+
|
|
|
+```
|
|
|
+输入文本
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────────────┐
|
|
|
+│ 第一层:文本解析(Scripting Layer) │
|
|
|
+│ LLM 理解文本 → 规划分镜 → 输出结构化脚本 JSON │
|
|
|
+│ 耗时: 3-10s │
|
|
|
+└──────────────────────────┬───────────────────────────────────┘
|
|
|
+ │ 分镜脚本 (JSON)
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────────────┐
|
|
|
+│ 第二层:素材生成(Asset Generation Layer) │
|
|
|
+│ 每个分镜并行生成:图片(T2I) + 配音(TTS) + 动态视频(I2V) │
|
|
|
+│ 耗时: 每个分镜 5-30s(并行后总耗时 ≈ 最慢的那个) │
|
|
|
+└──────────────────────────┬───────────────────────────────────┘
|
|
|
+ │ 素材文件 (图片/音频/视频片段)
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────────────┐
|
|
|
+│ 第三层:视频合成(Composition Layer) │
|
|
|
+│ FFmpeg 编排:拼接片段 + 叠加音频 + 添加字幕 + 转场 + BGM │
|
|
|
+│ 耗时: 视频时长的 0.5-2x │
|
|
|
+└──────────────────────────┬───────────────────────────────────┘
|
|
|
+ │ .mp4 文件
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────────────┐
|
|
|
+│ 第四层:输出交付(Delivery Layer) │
|
|
|
+│ 上传 OSS → 生成 CDN 链接 → 通知用户 │
|
|
|
+│ 耗时: 数秒 │
|
|
|
+└──────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+**端到端耗时预估**(3 分钟视频,10 个分镜):
|
|
|
+
|
|
|
+| 阶段 | 预览模式(512p) | 高清模式(1080p) |
|
|
|
+|------|:------------:|:-------------:|
|
|
|
+| 文本解析 | 5s | 10s |
|
|
|
+| 素材生成 | 30s | 120s |
|
|
|
+| 视频合成 | 60s | 180s |
|
|
|
+| 输出交付 | 5s | 10s |
|
|
|
+| **总计** | **~100s** | **~320s** |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、第一层:文本解析层
|
|
|
+
|
|
|
+### 3.1 处理流程
|
|
|
+
|
|
|
+```
|
|
|
+原始文本
|
|
|
+ │
|
|
|
+ ├── (1) 文本预处理
|
|
|
+ │ ├── 分段(按段落/话题自动切分)
|
|
|
+ │ ├── 字数统计、预估总时长
|
|
|
+ │ └── 语言检测
|
|
|
+ │
|
|
|
+ ├── (2) 核心 LLM 调用(Prompt 工程关键)
|
|
|
+ │ ├── 内容分析:主题、类型、情绪基调
|
|
|
+ │ ├── 角色提取:名称、性别、年龄、外貌、性格
|
|
|
+ │ ├── 场景提取:时间、地点、季节、氛围、色调
|
|
|
+ │ └── 分镜拆解:每个分镜的详细参数
|
|
|
+ │
|
|
|
+ └── (3) 结构化校验
|
|
|
+ ├── JSON Schema 校验
|
|
|
+ ├── 时长合理性检查(总时长 = Σ各分镜时长)
|
|
|
+ └── 兜底处理(LLM 输出异常时的降级方案)
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 分镜脚本数据结构
|
|
|
+
|
|
|
+```typescript
|
|
|
+// SceneScript - 完整分镜脚本
|
|
|
+interface VideoScript {
|
|
|
+ scriptId: string;
|
|
|
+ title: string;
|
|
|
+ textOriginal: string; // 用户原始文本
|
|
|
+ textCharCount: number;
|
|
|
+ estimatedDuration: number; // 预估总时长(秒)
|
|
|
+
|
|
|
+ // 全局设定
|
|
|
+ globalStyle: {
|
|
|
+ genre: 'story' | 'news' | 'tutorial' | 'marketing' | 'poetry';
|
|
|
+ visualStyle: string; // 如 "中国水墨画风"、"3D皮克斯风"
|
|
|
+ colorPalette: string[]; // 主色调
|
|
|
+ aspectRatio: '16:9' | '9:16' | '1:1'; // 横屏/竖屏/方屏
|
|
|
+ resolution: '720p' | '1080p' | '4K';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 角色设定(贯穿全片)
|
|
|
+ characters: Character[];
|
|
|
+
|
|
|
+ // 分镜列表
|
|
|
+ scenes: Scene[];
|
|
|
+
|
|
|
+ // TTS 配置
|
|
|
+ ttsConfig: {
|
|
|
+ provider: 'minimax' | 'aliyun' | 'volcano';
|
|
|
+ voiceId: string;
|
|
|
+ speed: number; // 0.5 - 2.0
|
|
|
+ volume: number; // 0 - 1
|
|
|
+ };
|
|
|
+
|
|
|
+ // BGM 配置
|
|
|
+ bgmConfig: {
|
|
|
+ style: string; // 如 "史诗管弦"、"轻快电子"、"中国古风"
|
|
|
+ tempo: 'slow' | 'medium' | 'fast';
|
|
|
+ volumeRatio: number; // 相对于旁白的音量,0.1 - 0.5
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+interface Character {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ gender: 'male' | 'female';
|
|
|
+ ageRange: string;
|
|
|
+ appearance: string; // 外貌描述(用于生成一致性画面)
|
|
|
+ personality: string;
|
|
|
+ referenceImageUrl?: string; // 用户上传的参考图
|
|
|
+}
|
|
|
+
|
|
|
+interface Scene {
|
|
|
+ id: number; // 分镜序号 1,2,3...
|
|
|
+ duration: number; // 时长(秒)
|
|
|
+
|
|
|
+ // 画面相关
|
|
|
+ imagePrompt: string; // 完整的 T2I prompt(英文)
|
|
|
+ imagePromptZh: string; // 中文版 prompt(供用户查看和编辑)
|
|
|
+ negativePrompt: string; // 反向 prompt
|
|
|
+ charactersInScene: string[]; // 此分镜出现的角色 ID
|
|
|
+ sceneDescription: string; // 场景描述
|
|
|
+
|
|
|
+ // 镜头相关
|
|
|
+ cameraMotion: CameraMotion; // 镜头运动
|
|
|
+ composition: string; // 构图(如 "三分法"、"中心对称")
|
|
|
+
|
|
|
+ // 配音相关
|
|
|
+ narration: string; // 旁白文本
|
|
|
+ narrationStyle: string; // 朗读风格描述
|
|
|
+
|
|
|
+ // 转场
|
|
|
+ transitionIn: Transition; // 入场转场
|
|
|
+ transitionOut: Transition; // 出场转场
|
|
|
+
|
|
|
+ // 情绪
|
|
|
+ mood: string; // 情绪标签
|
|
|
+ intensity: number; // 情绪强度 1-10
|
|
|
+}
|
|
|
+
|
|
|
+type CameraMotion =
|
|
|
+ | { type: 'static' }
|
|
|
+ | { type: 'zoom'; direction: 'in' | 'out'; speed: 'slow' | 'medium' | 'fast' }
|
|
|
+ | { type: 'pan'; direction: 'left' | 'right' | 'up' | 'down'; speed: 'slow' | 'medium' | 'fast' }
|
|
|
+ | { type: 'tilt'; direction: 'up' | 'down' }
|
|
|
+ | { type: 'track'; direction: 'left' | 'right' | 'forward' | 'backward' };
|
|
|
+
|
|
|
+type Transition =
|
|
|
+ | { type: 'cut' }
|
|
|
+ | { type: 'fade'; duration: number }
|
|
|
+ | { type: 'dissolve'; duration: number }
|
|
|
+ | { type: 'wipe'; direction: 'left' | 'right' | 'up' | 'down'; duration: number }
|
|
|
+ | { type: 'slide'; direction: 'left' | 'right' | 'up' | 'down'; duration: number };
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 LLM Prompt 模板
|
|
|
+
|
|
|
+```
|
|
|
+你是一个专业的视频导演和分镜师。请将以下文本转化为视频分镜脚本。
|
|
|
+
|
|
|
+## 全局设定
|
|
|
+- 视频类型:[根据文本自动判断:故事/新闻/教程/营销]
|
|
|
+- 视觉风格:[根据文本主题推荐]
|
|
|
+- 画面比例:16:9(横屏)
|
|
|
+- 目标总时长:[根据字数估算]
|
|
|
+
|
|
|
+## 角色设定
|
|
|
+从文本中提取所有角色,描述其外貌特征(用于保证画面一致性)。
|
|
|
+
|
|
|
+## 分镜要求
|
|
|
+1. 每个分镜时长 5-15 秒
|
|
|
+2. 画面描述用英文(供 AI 生图),要详细描述:主体、动作、背景、光线、色调、构图
|
|
|
+3. 旁白文本要口语化,适合朗读
|
|
|
+4. 镜头运动要配合情节节奏
|
|
|
+5. 转场要自然,高潮部分可以切换更快
|
|
|
+
|
|
|
+## 输出格式
|
|
|
+严格按以下 JSON 格式输出:
|
|
|
+{
|
|
|
+ "title": "视频标题",
|
|
|
+ "genre": "...",
|
|
|
+ "visualStyle": "...",
|
|
|
+ "colorPalette": ["#1a1a2e", "#e94560"],
|
|
|
+ "characters": [{ "id": "c1", "name": "...", ... }],
|
|
|
+ "scenes": [
|
|
|
+ {
|
|
|
+ "id": 1,
|
|
|
+ "duration": 8,
|
|
|
+ "imagePrompt": "cinematic shot of..., 8K, photorealistic",
|
|
|
+ "narration": "旁白文本...",
|
|
|
+ "mood": "神秘",
|
|
|
+ "cameraMotion": {"type":"slow_zoom","direction":"in"},
|
|
|
+ "transitionIn": {"type":"fade","duration":1}
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
+## 输入文本
|
|
|
+{text_content}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 技术选型
|
|
|
+
|
|
|
+| 组件 | 方案 | 原因 |
|
|
|
+|------|------|------|
|
|
|
+| LLM | Claude 3.5 Sonnet / GPT-4o | 结构化输出能力强 |
|
|
|
+| 备选 LLM | 阿里百炼 Qwen-Max | 成本低,中文理解好 |
|
|
|
+| Prompt 管理 | LangSmith / 自建模板系统 | 版本化 prompt,A/B 测试 |
|
|
|
+| 输出校验 | Zod Schema + 自定义规则 | 保证下游不收到脏数据 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、第二层:素材生成层
|
|
|
+
|
|
|
+### 4.1 并行流水线架构
|
|
|
+
|
|
|
+```
|
|
|
+ ┌─────────────────────┐
|
|
|
+ │ BullMQ 任务队列 │
|
|
|
+ │ (Redis-backed) │
|
|
|
+ └──────────┬──────────┘
|
|
|
+ │
|
|
|
+ ┌────────────────┼────────────────┐
|
|
|
+ ▼ ▼ ▼
|
|
|
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
|
+ │ Image Worker │ │ Audio Worker │ │ Video Worker │
|
|
|
+ │ (T2I Pool) │ │ (TTS Pool) │ │ (I2V Pool) │
|
|
|
+ │ concurrency=4│ │ concurrency=4│ │ concurrency=2│
|
|
|
+ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
|
|
+ │ │ │
|
|
|
+ ┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────┐
|
|
|
+ │ ProviderA │ │ ProviderA │ │ ProviderA │
|
|
|
+ │ (主) │ │ (主) │ │ (主) │
|
|
|
+ ├──────────────┤ ├──────────────┤ ├──────────────┤
|
|
|
+ │ ProviderB │ │ ProviderB │ │ ProviderB │
|
|
|
+ │ (备用/failover)│ │ (备用/failover)│ │ (备用/failover)│
|
|
|
+ └──────────────┘ └──────────────┘ └──────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 文生图 (T2I) 模块
|
|
|
+
|
|
|
+#### 实时进度设计
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface GenerationProgress {
|
|
|
+ taskId: string;
|
|
|
+ stage: 'scripting' | 'generating' | 'composing' | 'done' | 'failed';
|
|
|
+ progress: number; // 0-100
|
|
|
+ stageDetail: {
|
|
|
+ current: number;
|
|
|
+ total: number;
|
|
|
+ label: string; // "正在生成第 3/10 个画面..."
|
|
|
+ };
|
|
|
+ estimatedRemaining: number; // 秒
|
|
|
+ error?: string;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**进度推送方式**:
|
|
|
+1. **轮询**:`GET /api/video/:id/progress` — 每 2 秒请求一次
|
|
|
+2. **WebSocket**:`ws://host/ws/video/:id` — 实时推送(推荐)
|
|
|
+3. **SSE**:备选方案,兼容性好
|
|
|
+
|
|
|
+**前端效果**:
|
|
|
+```
|
|
|
+[████████░░░░░░░░] 45% 正在生成第 5/10 个画面...
|
|
|
+├── ✅ 分镜脚本已生成 (3s)
|
|
|
+├── ⏳ 画面生成中... (5/10)
|
|
|
+├── ⬜ 配音生成 (等待中)
|
|
|
+├── ⬜ 视频合成 (等待中)
|
|
|
+└── 预计剩余: 2 分 30 秒
|
|
|
+```
|
|
|
+
|
|
|
+#### 多方案对比
|
|
|
+
|
|
|
+| 方案 | API 服务 | 单张成本 | 速度 | 质量 | 中文理解 |
|
|
|
+|------|---------|:------:|:----:|:----:|:------:|
|
|
|
+| A1 | 通义万相 (阿里) | ¥0.06-0.12 | 3-8s | ⭐⭐⭐⭐ | ✅ 原生支持 |
|
|
|
+| A2 | Stable Diffusion (自部署) | ¥0.01 | 5-15s | ⭐⭐⭐ | 需英文 |
|
|
|
+| A3 | Midjourney API | ¥0.08-0.15 | 20-60s | ⭐⭐⭐⭐⭐ | 需英文 |
|
|
|
+| A4 | DALL-E 3 (OpenAI) | ¥0.30-0.60 | 5-15s | ⭐⭐⭐⭐⭐ | 支持 |
|
|
|
+| A5 | 即梦 (字节) | ¥0.04-0.08 | 3-5s | ⭐⭐⭐⭐ | ✅ 原生支持 |
|
|
|
+
|
|
|
+**推荐方案**:**通义万相 主 + 即梦 备用**(成本低、中文友好、速度快)
|
|
|
+
|
|
|
+#### 画面一致性保障
|
|
|
+
|
|
|
+这是最核心的难点。同一个角色在不同分镜里要保持脸一致。
|
|
|
+
|
|
|
+**方案一:IP-Adapter + FaceID (推荐)**
|
|
|
+
|
|
|
+```
|
|
|
+用户参考图 (或第一帧生成的角色图)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+IP-Adapter FaceID 提取面部特征向量
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+每个分镜的 T2I 请求附带特征向量作为 condition
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+生成的所有图片保持面部一致
|
|
|
+```
|
|
|
+
|
|
|
+实现路径:ComfyUI + IP-Adapter-FaceID 工作流(自部署)
|
|
|
+
|
|
|
+**方案二:GPT-4o 验证 + 重试**
|
|
|
+
|
|
|
+```
|
|
|
+生成第 N 个分镜图片
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+GPT-4o 对比:这张图里的角色和前面一致吗?
|
|
|
+ ├── 一致 → 通过
|
|
|
+ └── 不一致 → 调整 prompt → 重新生成(最多 3 次)
|
|
|
+```
|
|
|
+
|
|
|
+**方案三:Seed 锁定 + LoRA**
|
|
|
+
|
|
|
+预训练角色的 LoRA 模型,所有分镜共用同一个 seed 和 LoRA。
|
|
|
+
|
|
|
+### 4.3 语音合成 (TTS) 模块
|
|
|
+
|
|
|
+#### 现有能力复用
|
|
|
+
|
|
|
+当前项目已集成 3 家 TTS 服务,可直接复用:
|
|
|
+
|
|
|
+| 服务商 | 音色数 | 单字成本 | 中文质量 | 感情表达 |
|
|
|
+|--------|:----:|:------:|:------:|:------:|
|
|
|
+| MiniMax | 30+ | ¥0.015/字 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
|
|
+| 阿里百炼 | 50+ | ¥0.01/字 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
|
|
+| 火山引擎 | 20+ | ¥0.012/字 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
|
|
+
|
|
|
+**选择策略**:
|
|
|
+- 故事类 → MiniMax(感情表达最好)
|
|
|
+- 教程类 → 阿里百炼(最清晰)
|
|
|
+- 新闻类 → 火山引擎(语速可控)
|
|
|
+
|
|
|
+**音色一致性**:全片使用同一个 `voiceId`,无需额外处理。
|
|
|
+
|
|
|
+#### 长文本分段处理
|
|
|
+
|
|
|
+```typescript
|
|
|
+async function generateNarration(scenes: Scene[]): Promise<AudioSegment[]> {
|
|
|
+ const results: AudioSegment[] = [];
|
|
|
+
|
|
|
+ for (const scene of scenes) {
|
|
|
+ const audio = await ttsProvider.synthesize({
|
|
|
+ text: scene.narration,
|
|
|
+ voiceId: ttsConfig.voiceId,
|
|
|
+ speed: scene.mood === '紧张' ? 1.2 : 1.0,
|
|
|
+ format: 'mp3',
|
|
|
+ });
|
|
|
+
|
|
|
+ // 根据实际音频时长校准分镜时长
|
|
|
+ scene.duration = audio.duration;
|
|
|
+
|
|
|
+ results.push({
|
|
|
+ sceneId: scene.id,
|
|
|
+ audioUrl: await uploadToOss(audio.buffer),
|
|
|
+ duration: audio.duration,
|
|
|
+ text: scene.narration,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return results;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.4 图生视频 (I2V) 模块(可选,非 MVP 必须)
|
|
|
+
|
|
|
+如果预算允许,可以在 MVP 之后加入动态效果:
|
|
|
+
|
|
|
+| 方案 | API | 单秒成本 | 速度 | 效果 |
|
|
|
+|------|-----|:------:|:----:|:----:|
|
|
|
+| 可灵 (快手) | 国内 | ¥0.50-1.00 | 60s/秒 | ⭐⭐⭐⭐⭐ |
|
|
|
+| 即梦 (字节) | 国内 | ¥0.30-0.60 | 30s/秒 | ⭐⭐⭐⭐ |
|
|
|
+| Runway Gen-3 | 海外 | ¥0.80-1.50 | 60s/秒 | ⭐⭐⭐⭐⭐ |
|
|
|
+| SVD (开源) | 自部署 | ¥0.01 | 120s/秒 | ⭐⭐⭐ |
|
|
|
+
|
|
|
+**MVP 替代方案**:Ken Burns 效果
|
|
|
+
|
|
|
+不动态生成视频,只对静态图片应用 FFmpeg 的缩放/平移效果:
|
|
|
+
|
|
|
+```bash
|
|
|
+# Ken Burns: 缓慢放大 + 平移
|
|
|
+ffmpeg -loop 1 -i scene1.png \
|
|
|
+ -vf "zoompan=z='min(zoom+0.0015,1.5)':d=250:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1920x1080" \
|
|
|
+ -t 8 -c:v libx264 scene1_animated.mp4
|
|
|
+```
|
|
|
+
|
|
|
+效果类似纪录片风格,成本几乎为零,对于 MVP 完全够用。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、第三层:视频合成层
|
|
|
+
|
|
|
+### 5.1 FFmpeg 合成流水线
|
|
|
+
|
|
|
+```
|
|
|
+所有素材就绪
|
|
|
+ │
|
|
|
+ ├── (1) 图片转视频片段(Ken Burns 效果)
|
|
|
+ ├── (2) 视频片段 + 配音音频 对齐
|
|
|
+ ├── (3) 各分镜拼接 + 转场效果
|
|
|
+ ├── (4) 叠加字幕(SRT/ASS)
|
|
|
+ ├── (5) 混合背景音乐
|
|
|
+ ├── (6) 添加片头片尾(可选)
|
|
|
+ └── (7) 编码输出
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ output.mp4
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 完整的 FFmpeg 合成脚本生成器
|
|
|
+
|
|
|
+```typescript
|
|
|
+class FFmpegComposer {
|
|
|
+ /**
|
|
|
+ * 根据分镜脚本生成 FFmpeg 命令
|
|
|
+ */
|
|
|
+ buildCommand(params: ComposeParams): string {
|
|
|
+ const { scenes, bgmUrl, watermarkUrl, outputPath, resolution } = params;
|
|
|
+
|
|
|
+ // 构建 filter_complex
|
|
|
+ const filters: string[] = [];
|
|
|
+ const inputs: string[] = [];
|
|
|
+ let inputIndex = 0;
|
|
|
+ let videoStreams = '';
|
|
|
+ let audioStreams = '';
|
|
|
+
|
|
|
+ for (const scene of scenes) {
|
|
|
+ // 输入:图片 + 音频
|
|
|
+ inputs.push(`-loop 1 -t ${scene.duration} -i ${scene.imagePath}`);
|
|
|
+ inputs.push(`-i ${scene.audioPath}`);
|
|
|
+
|
|
|
+ const vIdx = inputIndex;
|
|
|
+ const aIdx = inputIndex + 1;
|
|
|
+ inputIndex += 2;
|
|
|
+
|
|
|
+ // Ken Burns 效果
|
|
|
+ filters.push(
|
|
|
+ `[${vIdx}:v]zoompan=z='min(zoom+0.0015,1.5)':d=1:` +
|
|
|
+ `s=${resolution.width}x${resolution.height}:` +
|
|
|
+ `x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)'[v${scene.id}]`
|
|
|
+ );
|
|
|
+
|
|
|
+ videoStreams += `[v${scene.id}]`;
|
|
|
+ audioStreams += `[${aIdx}:a]`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 视频拼接
|
|
|
+ const concatN = scenes.length;
|
|
|
+ filters.push(`${videoStreams}concat=n=${concatN}:v=1:a=0 [vout]`);
|
|
|
+ filters.push(`${audioStreams}concat=n=${concatN}:v=0:a=1 [aout]`);
|
|
|
+
|
|
|
+ // 字幕叠加
|
|
|
+ let currentTime = 0;
|
|
|
+ for (const scene of scenes) {
|
|
|
+ const subtitleText = this.escapeText(scene.narration);
|
|
|
+ filters.push(
|
|
|
+ `[vout]drawtext=text='${subtitleText}':` +
|
|
|
+ `fontfile=/usr/share/fonts/NotoSansSC-Regular.ttf:` +
|
|
|
+ `fontsize=24:fontcolor=white:borderw=2:bordercolor=black@0.5:` +
|
|
|
+ `x=(w-text_w)/2:y=h-120:` +
|
|
|
+ `enable='between(t,${currentTime},${currentTime + scene.duration})'[vsub]`
|
|
|
+ );
|
|
|
+ currentTime += scene.duration;
|
|
|
+ }
|
|
|
+
|
|
|
+ // BGM 混合
|
|
|
+ inputs.push(`-i ${bgmUrl}`);
|
|
|
+ const bgmIdx = inputIndex;
|
|
|
+ const totalDuration = scenes.reduce((sum, s) => sum + s.duration, 0);
|
|
|
+ filters.push(
|
|
|
+ `[aout][${bgmIdx}:a]amix=inputs=2:duration=first:weights='1 0.3' [afinal]`
|
|
|
+ );
|
|
|
+
|
|
|
+ // 构建命令
|
|
|
+ const filterComplex = filters.join('; ');
|
|
|
+ return [
|
|
|
+ ...inputs,
|
|
|
+ `-filter_complex "${filterComplex}"`,
|
|
|
+ `-map "[vsub]" -map "[afinal]"`,
|
|
|
+ `-c:v libx264 -preset medium -crf 20`,
|
|
|
+ `-c:a aac -b:a 192k`,
|
|
|
+ `-pix_fmt yuv420p`,
|
|
|
+ `-movflags +faststart`,
|
|
|
+ `-y ${outputPath}`,
|
|
|
+ ].join(' ');
|
|
|
+ }
|
|
|
+
|
|
|
+ private escapeText(text: string): string {
|
|
|
+ return text
|
|
|
+ .replace(/\\/g, '\\\\')
|
|
|
+ .replace(/'/g, "\\'")
|
|
|
+ .replace(/:/g, '\\:')
|
|
|
+ .replace(/,/g, '\\,');
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.3 字幕生成
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface SubtitleLine {
|
|
|
+ index: number;
|
|
|
+ startTime: number; // 秒
|
|
|
+ endTime: number; // 秒
|
|
|
+ text: string;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从分镜脚本生成 SRT 字幕文件
|
|
|
+ */
|
|
|
+function generateSRT(scenes: Scene[], audioSegments: AudioSegment[]): string {
|
|
|
+ let srt = '';
|
|
|
+ let index = 1;
|
|
|
+ let currentTime = 0;
|
|
|
+
|
|
|
+ for (const scene of scenes) {
|
|
|
+ const words = scene.narration.split('');
|
|
|
+ const charsPerSecond = words.length / scene.duration;
|
|
|
+
|
|
|
+ // 每 15-20 字一行
|
|
|
+ const chunkSize = 18;
|
|
|
+ let pos = 0;
|
|
|
+
|
|
|
+ while (pos < words.length) {
|
|
|
+ const chunk = words.slice(pos, pos + chunkSize).join('');
|
|
|
+ const chunkDuration = chunk.length / charsPerSecond;
|
|
|
+
|
|
|
+ srt += `${index}\n`;
|
|
|
+ srt += `${this.formatSrtTime(currentTime)} --> ${this.formatSrtTime(currentTime + chunkDuration)}\n`;
|
|
|
+ srt += `${chunk}\n\n`;
|
|
|
+
|
|
|
+ currentTime += chunkDuration;
|
|
|
+ index++;
|
|
|
+ pos += chunkSize;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return srt;
|
|
|
+}
|
|
|
+
|
|
|
+function formatSrtTime(seconds: number): string {
|
|
|
+ const h = Math.floor(seconds / 3600);
|
|
|
+ const m = Math.floor((seconds % 3600) / 60);
|
|
|
+ const s = Math.floor(seconds % 60);
|
|
|
+ const ms = Math.floor((seconds % 1) * 1000);
|
|
|
+ return `${pad(h)}:${pad(m)}:${pad(s)},${String(ms).padEnd(3, '0')}`;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.4 性能优化
|
|
|
+
|
|
|
+| 优化策略 | 效果 |
|
|
|
+|---------|------|
|
|
|
+| GPU 编码 (NVENC) | 合成速度 3-5x 提升 |
|
|
|
+| 分段合成 + 最后拼接 | 内存可控,支持长视频 |
|
|
|
+| 预览模式 512p | 比 1080p 快 4x |
|
|
|
+| 复用缓存 | 同一图片多个分镜只生成一次 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、第四层:输出交付层
|
|
|
+
|
|
|
+```typescript
|
|
|
+class VideoDeliveryService {
|
|
|
+ async deliver(taskId: string, videoPath: string) {
|
|
|
+ // 1. 上传到 OSS
|
|
|
+ const ossKey = `videos/${taskId}/output_${Date.now()}.mp4`;
|
|
|
+ await this.oss.upload(videoPath, ossKey, {
|
|
|
+ headers: { 'Content-Type': 'video/mp4' },
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. 生成 CDN 签名 URL
|
|
|
+ const cdnUrl = this.oss.generateSignedUrl(ossKey, {
|
|
|
+ expires: 7 * 24 * 3600, // 7 天有效期
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3. 生成封面图
|
|
|
+ const coverKey = await this.generateCover(videoPath);
|
|
|
+
|
|
|
+ // 4. 更新数据库
|
|
|
+ await this.db.videoTask.update(taskId, {
|
|
|
+ status: 'done',
|
|
|
+ videoUrl: cdnUrl,
|
|
|
+ coverUrl: coverKey,
|
|
|
+ fileSize: fs.statSync(videoPath).size,
|
|
|
+ completedAt: new Date(),
|
|
|
+ });
|
|
|
+
|
|
|
+ // 5. 推送通知
|
|
|
+ await this.notification.send(taskId, {
|
|
|
+ type: 'video_completed',
|
|
|
+ title: '视频生成完成',
|
|
|
+ videoUrl: cdnUrl,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 6. 清理本地文件(保留 1 小时后删除)
|
|
|
+ setTimeout(() => fs.unlinkSync(videoPath), 3600000);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、服务端架构
|
|
|
+
|
|
|
+### 7.1 整体架构图
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────────────────┐
|
|
|
+│ Nginx (反向代理 + HTTPS) │
|
|
|
+└──────────────┬──────────────────────────────┬───────────────┘
|
|
|
+ │ │
|
|
|
+┌──────────────▼──────────┐ ┌────────────▼──────────────┐
|
|
|
+│ Nest.js API Server │ │ Frontend (Vue/UniApp) │
|
|
|
+│ (主要业务逻辑) │ │ │
|
|
|
+│ - 用户认证 │ │ - 文本输入页 │
|
|
|
+│ - 任务创建/查询 │ │ - 分镜编辑页 │
|
|
|
+│ - 支付/订阅 │ │ - 进度展示页 │
|
|
|
+│ - 素材管理 │ │ - 视频播放页 │
|
|
|
+└──────────────┬──────────┘ └──────────────────────────────┘
|
|
|
+ │
|
|
|
+┌──────────────▼─────────────────────────────────────────────┐
|
|
|
+│ BullMQ Task Queue (Redis) │
|
|
|
+│ │
|
|
|
+│ ┌───────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
|
|
+│ │ Scripting │ │ Asset Gen │ │ Composition │ │
|
|
|
+│ │ Workers (x2) │ │ Workers (x4) │ │ Workers (x2) │ │
|
|
|
+│ │ │ │ │ │ (需 GPU 加速) │ │
|
|
|
+│ │ CPU: 2核 │ │ CPU: 2核 │ │ GPU: T4 x1 │ │
|
|
|
+│ │ Mem: 4GB │ │ Mem: 8GB │ │ Mem: 16GB │ │
|
|
|
+│ └───────────────┘ └──────────────┘ └─────────────────┘ │
|
|
|
+└──────────────┬──────────────────────────────────────────────┘
|
|
|
+ │
|
|
|
+┌──────────────▼──────────────────────────────────────────────┐
|
|
|
+│ Storage Layer │
|
|
|
+│ ┌──────────────────┐ ┌──────────────┐ ┌───────────────┐ │
|
|
|
+│ │ MySQL (任务/用户) │ │ Redis (队列 │ │ OSS (素材/视频)│ │
|
|
|
+│ │ │ │ /缓存/进度) │ │ │ │
|
|
|
+│ └──────────────────┘ └──────────────┘ └───────────────┘ │
|
|
|
+└─────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 7.2 Worker 详细设计
|
|
|
+
|
|
|
+```typescript
|
|
|
+// === Scripting Worker ===
|
|
|
+@Processor('scripting')
|
|
|
+class ScriptingWorker {
|
|
|
+ @Process('generate')
|
|
|
+ async generateScript(job: Job<{ text: string; config: ScriptConfig }>) {
|
|
|
+ const { text, config } = job.data;
|
|
|
+
|
|
|
+ // 更新进度
|
|
|
+ await job.updateProgress({ stage: 'scripting', progress: 0 });
|
|
|
+
|
|
|
+ // 调用 LLM 生成分镜
|
|
|
+ const script = await this.llmService.generateScript(text, config);
|
|
|
+
|
|
|
+ await job.updateProgress({ stage: 'scripting', progress: 100 });
|
|
|
+
|
|
|
+ // 创建素材生成子任务
|
|
|
+ for (const scene of script.scenes) {
|
|
|
+ await this.assetQueue.add('generateImage', { scene, scriptId: script.id });
|
|
|
+ await this.assetQueue.add('generateAudio', { scene, scriptId: script.id });
|
|
|
+ }
|
|
|
+
|
|
|
+ return script;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// === Asset Worker ===
|
|
|
+@Processor('asset-generation')
|
|
|
+class AssetWorker {
|
|
|
+ @Process('generateImage')
|
|
|
+ async generateImage(job: Job<{ scene: Scene; scriptId: string }>) {
|
|
|
+ const { scene } = job.data;
|
|
|
+
|
|
|
+ // 重试机制 + provider failover
|
|
|
+ let image: Buffer;
|
|
|
+ const providers = ['tongyi', 'jimeng', 'openai'];
|
|
|
+
|
|
|
+ for (const provider of providers) {
|
|
|
+ try {
|
|
|
+ image = await this.imageProviders[provider].generate(scene.imagePrompt, {
|
|
|
+ negativePrompt: scene.negativePrompt,
|
|
|
+ size: '1024x1024',
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ } catch (e) {
|
|
|
+ this.logger.warn(`Provider ${provider} failed, trying next...`, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!image) throw new Error('All image providers failed');
|
|
|
+
|
|
|
+ // 上传到 OSS
|
|
|
+ const url = await this.oss.upload(
|
|
|
+ `scripts/${job.data.scriptId}/scene_${scene.id}.png`,
|
|
|
+ image
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新场景状态
|
|
|
+ await this.db.scene.update(scene.id, { imageUrl: url, imageStatus: 'done' });
|
|
|
+
|
|
|
+ return { sceneId: scene.id, imageUrl: url };
|
|
|
+ }
|
|
|
+
|
|
|
+ @Process('generateAudio')
|
|
|
+ async generateAudio(job: Job<{ scene: Scene; scriptId: string }>) {
|
|
|
+ const { scene } = job.data;
|
|
|
+
|
|
|
+ const providers = ['minimax', 'aliyun', 'volcano'];
|
|
|
+ let audio: Buffer;
|
|
|
+
|
|
|
+ for (const provider of providers) {
|
|
|
+ try {
|
|
|
+ audio = await this.ttsProviders[provider].synthesize({
|
|
|
+ text: scene.narration,
|
|
|
+ voiceId: job.data.script.ttsConfig.voiceId,
|
|
|
+ speed: job.data.script.ttsConfig.speed,
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ } catch (e) {
|
|
|
+ this.logger.warn(`TTS provider ${provider} failed, trying next...`, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!audio) throw new Error('All TTS providers failed');
|
|
|
+
|
|
|
+ const url = await this.oss.upload(
|
|
|
+ `scripts/${job.data.scriptId}/scene_${scene.id}_audio.mp3`,
|
|
|
+ audio
|
|
|
+ );
|
|
|
+
|
|
|
+ await this.db.scene.update(scene.id, { audioUrl: url, audioStatus: 'done' });
|
|
|
+
|
|
|
+ return { sceneId: scene.id, audioUrl: url };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// === Composition Worker ===
|
|
|
+@Processor('composition')
|
|
|
+class CompositionWorker {
|
|
|
+ @Process('compose')
|
|
|
+ async composeVideo(job: Job<{ scriptId: string; options: ComposeOptions }>) {
|
|
|
+ const { scriptId, options } = job.data;
|
|
|
+
|
|
|
+ await job.updateProgress({ stage: 'composing', progress: 0 });
|
|
|
+
|
|
|
+ // 加载所有素材
|
|
|
+ const scenes = await this.db.scene.findByScriptId(scriptId);
|
|
|
+ const script = await this.db.script.findById(scriptId);
|
|
|
+
|
|
|
+ // 确保所有素材就绪
|
|
|
+ const allReady = scenes.every(s => s.imageStatus === 'done' && s.audioStatus === 'done');
|
|
|
+ if (!allReady) throw new Error('Not all assets are ready');
|
|
|
+
|
|
|
+ // 下载素材到本地(OSS → tmp dir)
|
|
|
+ const tmpDir = `/tmp/video-${scriptId}`;
|
|
|
+ await this.downloadAssets(scenes, tmpDir);
|
|
|
+
|
|
|
+ // 生成 FFmpeg 命令
|
|
|
+ const composer = new FFmpegComposer();
|
|
|
+ const cmd = composer.buildCommand({
|
|
|
+ scenes: scenes.map(s => ({
|
|
|
+ ...s,
|
|
|
+ imagePath: `${tmpDir}/scene_${s.id}.png`,
|
|
|
+ audioPath: `${tmpDir}/scene_${s.id}_audio.mp3`,
|
|
|
+ })),
|
|
|
+ bgmUrl: script.bgmConfig.audioUrl,
|
|
|
+ outputPath: `${tmpDir}/output.mp4`,
|
|
|
+ resolution: { width: 1920, height: 1080 },
|
|
|
+ });
|
|
|
+
|
|
|
+ await job.updateProgress({ progress: 20 });
|
|
|
+
|
|
|
+ // 执行 FFmpeg
|
|
|
+ await this.execFFmpeg(cmd, (progress) => {
|
|
|
+ job.updateProgress({ progress: 20 + Math.round(progress * 0.7) });
|
|
|
+ });
|
|
|
+
|
|
|
+ await job.updateProgress({ progress: 90 });
|
|
|
+
|
|
|
+ // 交付
|
|
|
+ await this.deliveryService.deliver(scriptId, `${tmpDir}/output.mp4`);
|
|
|
+
|
|
|
+ await job.updateProgress({ progress: 100 });
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.3 WebSocket 进度推送
|
|
|
+
|
|
|
+```typescript
|
|
|
+@WebSocketGateway({ namespace: '/ws/video' })
|
|
|
+class VideoProgressGateway {
|
|
|
+ @WebSocketServer()
|
|
|
+ server: Server;
|
|
|
+
|
|
|
+ // 客户端订阅任务进度
|
|
|
+ @SubscribeMessage('subscribe')
|
|
|
+ async handleSubscribe(
|
|
|
+ @MessageBody() data: { taskId: string },
|
|
|
+ @ConnectedSocket() client: Socket
|
|
|
+ ) {
|
|
|
+ client.join(`task:${data.taskId}`);
|
|
|
+
|
|
|
+ // 发送当前进度
|
|
|
+ const task = await this.videoService.getTask(data.taskId);
|
|
|
+ client.emit('progress', task);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Worker 端推送进度更新
|
|
|
+ pushProgress(taskId: string, progress: VideoProgress) {
|
|
|
+ this.server.to(`task:${taskId}`).emit('progress', progress);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、数据库设计
|
|
|
+
|
|
|
+### 8.1 核心表
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 视频任务主表
|
|
|
+CREATE TABLE video_tasks (
|
|
|
+ id VARCHAR(36) PRIMARY KEY,
|
|
|
+ user_id INT NOT NULL,
|
|
|
+ title VARCHAR(200),
|
|
|
+ text_original TEXT NOT NULL,
|
|
|
+ text_char_count INT DEFAULT 0,
|
|
|
+ status ENUM('pending','scripting','generating','composing','done','failed') DEFAULT 'pending',
|
|
|
+ progress JSON, -- {stage, progress, detail}
|
|
|
+ video_url VARCHAR(500),
|
|
|
+ cover_url VARCHAR(500),
|
|
|
+ duration INT DEFAULT 0, -- 实际时长(秒)
|
|
|
+ file_size BIGINT DEFAULT 0, -- 字节
|
|
|
+ resolution VARCHAR(10) DEFAULT '1080p',
|
|
|
+ estimated_cost DECIMAL(10,4), -- 预估费用
|
|
|
+ actual_cost DECIMAL(10,4), -- 实际费用
|
|
|
+ error_message TEXT,
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ completed_at TIMESTAMP NULL,
|
|
|
+ INDEX idx_user_status (user_id, status),
|
|
|
+ INDEX idx_created (created_at)
|
|
|
+);
|
|
|
+
|
|
|
+-- 分镜脚本表
|
|
|
+CREATE TABLE video_scripts (
|
|
|
+ id VARCHAR(36) PRIMARY KEY,
|
|
|
+ task_id VARCHAR(36) NOT NULL,
|
|
|
+ title VARCHAR(200),
|
|
|
+ genre VARCHAR(20),
|
|
|
+ visual_style VARCHAR(100),
|
|
|
+ color_palette JSON,
|
|
|
+ aspect_ratio VARCHAR(5) DEFAULT '16:9',
|
|
|
+ tts_config JSON, -- {provider, voiceId, speed, volume}
|
|
|
+ bgm_config JSON, -- {style, tempo, volumeRatio}
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ FOREIGN KEY (task_id) REFERENCES video_tasks(id)
|
|
|
+);
|
|
|
+
|
|
|
+-- 角色表
|
|
|
+CREATE TABLE video_characters (
|
|
|
+ id VARCHAR(36) PRIMARY KEY,
|
|
|
+ script_id VARCHAR(36) NOT NULL,
|
|
|
+ name VARCHAR(100),
|
|
|
+ gender ENUM('male','female'),
|
|
|
+ age_range VARCHAR(20),
|
|
|
+ appearance TEXT,
|
|
|
+ personality TEXT,
|
|
|
+ reference_image_url VARCHAR(500),
|
|
|
+ face_feature_vector BLOB, -- IP-Adapter 提取的特征向量
|
|
|
+ FOREIGN KEY (script_id) REFERENCES video_scripts(id)
|
|
|
+);
|
|
|
+
|
|
|
+-- 分镜表
|
|
|
+CREATE TABLE video_scenes (
|
|
|
+ id VARCHAR(36) PRIMARY KEY,
|
|
|
+ script_id VARCHAR(36) NOT NULL,
|
|
|
+ scene_order INT NOT NULL,
|
|
|
+ duration DECIMAL(5,1) DEFAULT 8.0,
|
|
|
+ image_prompt TEXT,
|
|
|
+ image_prompt_zh TEXT,
|
|
|
+ negative_prompt TEXT,
|
|
|
+ narration TEXT,
|
|
|
+ narration_style VARCHAR(100),
|
|
|
+ camera_motion JSON,
|
|
|
+ transition_in JSON,
|
|
|
+ transition_out JSON,
|
|
|
+ mood VARCHAR(50),
|
|
|
+ intensity TINYINT DEFAULT 5,
|
|
|
+
|
|
|
+ -- 生成结果
|
|
|
+ image_url VARCHAR(500),
|
|
|
+ image_status ENUM('pending','generating','done','failed') DEFAULT 'pending',
|
|
|
+ audio_url VARCHAR(500),
|
|
|
+ audio_status ENUM('pending','generating','done','failed') DEFAULT 'pending',
|
|
|
+ video_url VARCHAR(500), -- I2V 扩展
|
|
|
+ video_status ENUM('pending','generating','done','failed') DEFAULT 'pending',
|
|
|
+
|
|
|
+ retry_count INT DEFAULT 0,
|
|
|
+ error_message TEXT,
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ FOREIGN KEY (script_id) REFERENCES video_scripts(id),
|
|
|
+ INDEX idx_script_order (script_id, scene_order)
|
|
|
+);
|
|
|
+
|
|
|
+-- 场景中出现的角色关联
|
|
|
+CREATE TABLE video_scene_characters (
|
|
|
+ scene_id VARCHAR(36) NOT NULL,
|
|
|
+ character_id VARCHAR(36) NOT NULL,
|
|
|
+ PRIMARY KEY (scene_id, character_id)
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### 8.2 费用记录表
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE video_costs (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ task_id VARCHAR(36) NOT NULL,
|
|
|
+ scene_id VARCHAR(36), -- NULL 表示整任务费用
|
|
|
+ cost_type ENUM('scripting','image','audio','video_gen','composition','delivery'),
|
|
|
+ provider VARCHAR(50), -- 如 'tongyi', 'minimax', 'ffmpeg'
|
|
|
+ units DECIMAL(10,2), -- 使用量(张数/字数/秒数)
|
|
|
+ unit_price DECIMAL(10,6),
|
|
|
+ total_cost DECIMAL(10,4),
|
|
|
+ detail JSON, -- 附加信息
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ FOREIGN KEY (task_id) REFERENCES video_tasks(id),
|
|
|
+ INDEX idx_task (task_id)
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 九、API 设计
|
|
|
+
|
|
|
+### 9.1 RESTful API
|
|
|
+
|
|
|
+```typescript
|
|
|
+// === 视频任务 ===
|
|
|
+
|
|
|
+// 创建视频任务
|
|
|
+POST /api/video/create
|
|
|
+Body: {
|
|
|
+ text: "输入文本...",
|
|
|
+ config?: {
|
|
|
+ style?: "水墨画风" | "3D卡通" | "写实" | "二次元",
|
|
|
+ resolution?: "720p" | "1080p" | "4K",
|
|
|
+ aspectRatio?: "16:9" | "9:16" | "1:1",
|
|
|
+ voice?: "male_young" | "female_warm" | "male_deep",
|
|
|
+ bgm?: "epic" | "light" | "none",
|
|
|
+ subtitleEnabled?: true
|
|
|
+ }
|
|
|
+}
|
|
|
+Response: {
|
|
|
+ code: 0,
|
|
|
+ data: {
|
|
|
+ taskId: "uuid",
|
|
|
+ estimatedCost: 0.85, // 预估费用
|
|
|
+ estimatedDuration: 180, // 预估耗时(秒)
|
|
|
+ status: "pending"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 查询任务状态
|
|
|
+GET /api/video/:id
|
|
|
+Response: {
|
|
|
+ code: 0,
|
|
|
+ data: {
|
|
|
+ taskId: "uuid",
|
|
|
+ status: "generating",
|
|
|
+ progress: { stage: "generating", progress: 45, detail: "正在生成第5/10个画面..." },
|
|
|
+ script: { ... }, // status >= scripting 时返回
|
|
|
+ scenes: [ ... ], // 含素材 URL
|
|
|
+ videoUrl: "https://...", // status === done 时返回
|
|
|
+ coverUrl: "https://...",
|
|
|
+ duration: 180,
|
|
|
+ actualCost: 0.82
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 查询任务列表
|
|
|
+GET /api/video/list?page=1&pageSize=20&status=done
|
|
|
+Response: {
|
|
|
+ code: 0,
|
|
|
+ data: {
|
|
|
+ list: [{ taskId, title, coverUrl, duration, status, createdAt }],
|
|
|
+ total: 42
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 重试失败任务
|
|
|
+POST /api/video/:id/retry
|
|
|
+
|
|
|
+// 删除任务
|
|
|
+DELETE /api/video/:id
|
|
|
+
|
|
|
+// === 分镜编辑(草稿阶段干预) ===
|
|
|
+
|
|
|
+// 更新分镜脚本
|
|
|
+PUT /api/video/:id/script
|
|
|
+Body: { scenes: [{ id: 1, imagePrompt: "修改后的prompt", narration: "修改后的旁白" }] }
|
|
|
+
|
|
|
+// 重新生成单个分镜
|
|
|
+POST /api/video/:id/scene/:sceneId/regenerate
|
|
|
+Body: { type: "image" | "audio" | "all" }
|
|
|
+
|
|
|
+// === 费用 ===
|
|
|
+
|
|
|
+// 预估费用
|
|
|
+POST /api/video/estimate
|
|
|
+Body: { text: "...", config: { resolution: "1080p" } }
|
|
|
+Response: { code: 0, data: { estimatedCost: 0.85 } }
|
|
|
+
|
|
|
+// 查询费用明细
|
|
|
+GET /api/video/:id/costs
|
|
|
+```
|
|
|
+
|
|
|
+### 9.2 WebSocket 事件
|
|
|
+
|
|
|
+```
|
|
|
+Client → Server:
|
|
|
+ subscribe { taskId: "uuid" } // 订阅任务进度
|
|
|
+ unsubscribe { taskId: "uuid" } // 取消订阅
|
|
|
+
|
|
|
+Server → Client:
|
|
|
+ progress { // 进度更新
|
|
|
+ taskId: "uuid",
|
|
|
+ stage: "generating",
|
|
|
+ progress: 67,
|
|
|
+ detail: { current: 7, total: 10, label: "正在生成第7/10个画面..." },
|
|
|
+ estimatedRemaining: 45
|
|
|
+ }
|
|
|
+ completed { // 任务完成
|
|
|
+ taskId: "uuid",
|
|
|
+ videoUrl: "https://...",
|
|
|
+ duration: 180
|
|
|
+ }
|
|
|
+ failed { // 任务失败
|
|
|
+ taskId: "uuid",
|
|
|
+ error: "视频合成失败:内存不足"
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十、核心难点与解决方案
|
|
|
+
|
|
|
+### 10.1 画面一致性
|
|
|
+
|
|
|
+**问题**:同一个角色在不同分镜里脸不一样,看起来像不同的人。
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────────────────┐
|
|
|
+│ 三层保障机制 │
|
|
|
+│ │
|
|
|
+│ Layer 1: Prompt 工程 │
|
|
|
+│ 所有分镜的 imagePrompt 前缀统一角色描述 │
|
|
|
+│ 例: "A young Chinese woman, long black hair, round face, │
|
|
|
+│ wearing a red hanfu, consistent character..." │
|
|
|
+│ │
|
|
|
+│ Layer 2: IP-Adapter + FaceID (推荐) │
|
|
|
+│ - 第一帧生成角色 → 提取 FaceID embedding │
|
|
|
+│ - 后续分镜附带 embedding 作为条件注入 │
|
|
|
+│ - 技术栈: ComfyUI + IP-Adapter-FaceID-Plus │
|
|
|
+│ │
|
|
|
+│ Layer 3: GPT-4o 质量验证 │
|
|
|
+│ - 每个分镜生成后 → GPT-4o 对比角色一致性 │
|
|
|
+│ - 不一致 → 自动调整 prompt → 重新生成(最多 3 次) │
|
|
|
+│ - Cost: ~$0.01/次验证 │
|
|
|
+└─────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 10.2 长文本处理
|
|
|
+
|
|
|
+**问题**:用户输入 5000 字,需要 20+ 个分镜,LLM 输出可能失控。
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+
|
|
|
+1. **分段处理**:超过 2000 字自动切分成 N 段,每段独立生成分镜
|
|
|
+2. **全局上下文注入**:每段生成时携带角色设定的摘要
|
|
|
+3. **分镜合并**:所有段的分镜按序拼接,保证连贯性
|
|
|
+
|
|
|
+### 10.3 成本控制
|
|
|
+
|
|
|
+```
|
|
|
+预估成本公式:
|
|
|
+ Cost = N_scenes × (T2I_cost + TTS_cost) + BGM_cost + Compute_cost
|
|
|
+
|
|
|
+以 3 分钟视频(10 个分镜)为例:
|
|
|
+ 通义万相 T2I: 10 × ¥0.10 = ¥1.00
|
|
|
+ MiniMax TTS: 500字 × ¥0.015 = ¥7.50
|
|
|
+ BGM: ¥0.50
|
|
|
+ FFmpeg 合成: ¥0.20 (GPU 租用分摊)
|
|
|
+ LLM 分镜: ¥0.05
|
|
|
+ ─────────────────────────
|
|
|
+ 总计: ¥9.25
|
|
|
+
|
|
|
+用户定价建议:
|
|
|
+ 1080p: ¥2.99/次 (约 3x 成本覆盖)
|
|
|
+ 4K: ¥5.99/次
|
|
|
+ VIP 月度: ¥29.99/月(不限次数,限 1080p)
|
|
|
+```
|
|
|
+
|
|
|
+### 10.4 容错设计
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 多层容错机制
|
|
|
+class ResilienceManager {
|
|
|
+ // 1. Provider Failover
|
|
|
+ async generateWithRetry<T>(
|
|
|
+ providers: string[],
|
|
|
+ generateFn: (provider: string) => Promise<T>,
|
|
|
+ maxRetries = 3
|
|
|
+ ): Promise<T> {
|
|
|
+ for (const provider of providers) {
|
|
|
+ for (let retry = 0; retry < maxRetries; retry++) {
|
|
|
+ try {
|
|
|
+ return await generateFn(provider);
|
|
|
+ } catch (e) {
|
|
|
+ if (retry < maxRetries - 1) {
|
|
|
+ await this.sleep(Math.pow(2, retry) * 1000); // 指数退避
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new Error('All providers exhausted');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 单个分镜失败不阻塞全部
|
|
|
+ async generateAllScenes(scenes: Scene[]) {
|
|
|
+ const results = await Promise.allSettled(
|
|
|
+ scenes.map(s => this.generateScene(s))
|
|
|
+ );
|
|
|
+
|
|
|
+ // 统计失败率
|
|
|
+ const failed = results.filter(r => r.status === 'rejected');
|
|
|
+ if (failed.length / scenes.length > 0.5) {
|
|
|
+ throw new Error('Too many scenes failed');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对失败的用 placeholder 替代
|
|
|
+ return results.map((r, i) => {
|
|
|
+ if (r.status === 'fulfilled') return r.value;
|
|
|
+ return this.generatePlaceholder(scenes[i]);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 任务幂等性
|
|
|
+ async createTask(text: string): Promise<string> {
|
|
|
+ const hash = crypto.createHash('md5').update(text).digest('hex');
|
|
|
+ const existing = await this.db.videoTask.findByHash(hash);
|
|
|
+
|
|
|
+ if (existing && existing.status === 'done') {
|
|
|
+ return existing.id; // 相同文本直接返回缓存结果
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.createNewTask(text, hash);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 10.5 用户交互设计
|
|
|
+
|
|
|
+```
|
|
|
+用户编辑流程(保证可控性):
|
|
|
+
|
|
|
+ 输入文本 → [生成分镜预览] → 用户确认/编辑分镜 → [生成素材] → 用户预览 →
|
|
|
+ [调整/重生成单个分镜] → [确认合成] → 等待 → 播放/下载
|
|
|
+
|
|
|
+关键:分镜草稿阶段就允许用户干预,避免生成完才发现不满意
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十一、与现有项目融合
|
|
|
+
|
|
|
+### 11.1 现有能力映射
|
|
|
+
|
|
|
+| 现有模块 | 路径 | 复用方式 |
|
|
|
+|---------|------|---------|
|
|
|
+| Nest.js 服务 | `server/src/` | 新增 `modules/video/` 目录 |
|
|
|
+| TTS 多模型 | `server/src/modules/tts/` | 封装为统一 TTS Provider 接口 |
|
|
|
+| 阿里云 OSS | 现有 OSS 配置 | 视频素材 + 成品存储 |
|
|
|
+| 支付系统 | `server/src/modules/payment/` | 按视频收费,复用支付宝/微信 |
|
|
|
+| 用户认证 | `server/src/modules/auth/` | 复用 JWT 认证 + VIP 体系 |
|
|
|
+| 任务队列 | 需要新增 | 引入 BullMQ + Redis |
|
|
|
+| FFmpeg | 需要新增 | 服务器安装 + 封装 |
|
|
|
+| 前端 UniApp | `my-uniapp-vue3/` | 新增视频生成页面 |
|
|
|
+
|
|
|
+### 11.2 新模块目录结构
|
|
|
+
|
|
|
+```
|
|
|
+server/src/modules/video/
|
|
|
+├── video.module.ts # Nest.js 模块定义
|
|
|
+├── video.controller.ts # API 控制器
|
|
|
+├── video.service.ts # 业务逻辑
|
|
|
+├── video.gateway.ts # WebSocket 进度推送
|
|
|
+├── scripting/
|
|
|
+│ ├── scripting.worker.ts # 分镜生成 Worker
|
|
|
+│ ├── scripting.service.ts # LLM 调用封装
|
|
|
+│ └── prompts/
|
|
|
+│ ├── story.ts # 故事类 prompt 模板
|
|
|
+│ ├── news.ts # 新闻类 prompt 模板
|
|
|
+│ └── tutorial.ts # 教程类 prompt 模板
|
|
|
+├── assets/
|
|
|
+│ ├── image.worker.ts # T2I Worker
|
|
|
+│ ├── image.service.ts # 多 Provider 封装
|
|
|
+│ ├── audio.worker.ts # TTS Worker (复用现有)
|
|
|
+│ ├── audio.service.ts # TTS Provider 统一接口
|
|
|
+│ └── providers/
|
|
|
+│ ├── tongyi.provider.ts
|
|
|
+│ ├── jimeng.provider.ts
|
|
|
+│ ├── minimax.provider.ts
|
|
|
+│ └── aliyun-tts.provider.ts
|
|
|
+├── composition/
|
|
|
+│ ├── composition.worker.ts # 视频合成 Worker
|
|
|
+│ ├── composition.service.ts
|
|
|
+│ ├── ffmpeg-builder.ts # FFmpeg 命令构建器
|
|
|
+│ └── subtitle.service.ts # 字幕生成
|
|
|
+├── delivery/
|
|
|
+│ └── delivery.service.ts # OSS 上传 + CDN
|
|
|
+├── entities/
|
|
|
+│ ├── video-task.entity.ts
|
|
|
+│ ├── video-script.entity.ts
|
|
|
+│ ├── video-character.entity.ts
|
|
|
+│ └── video-scene.entity.ts
|
|
|
+└── dto/
|
|
|
+ ├── create-video.dto.ts
|
|
|
+ ├── update-script.dto.ts
|
|
|
+ └── video-progress.dto.ts
|
|
|
+```
|
|
|
+
|
|
|
+### 11.3 前端新页面
|
|
|
+
|
|
|
+```
|
|
|
+my-uniapp-vue3/src/pages/
|
|
|
+├── video-create/ # 视频创建页
|
|
|
+│ └── index.vue # 输入文本 + 参数选择
|
|
|
+├── video-script/ # 分镜编辑页
|
|
|
+│ └── index.vue # 分镜预览 + 调整
|
|
|
+├── video-progress/ # 进度页
|
|
|
+│ └── index.vue # 实时进度 + 素材预览
|
|
|
+├── video-player/ # 视频播放页
|
|
|
+│ └── index.vue # 播放 + 分享 + 下载
|
|
|
+└── video-list/ # 我的视频列表
|
|
|
+ └── index.vue # 历史记录
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十二、MVP 实施计划
|
|
|
+
|
|
|
+### 12.1 MVP 范围
|
|
|
+
|
|
|
+**包含**:
|
|
|
+- 输入文本 → 自动生成分镜脚本
|
|
|
+- 通义万相 T2I 生成画面
|
|
|
+- 现有 TTS 生成配音
|
|
|
+- Ken Burns 效果(静态图缓慢运动)
|
|
|
+- FFmpeg 合成 + 字幕 + BGM
|
|
|
+- 1080p 输出
|
|
|
+- 基本进度展示
|
|
|
+
|
|
|
+**不包含**:
|
|
|
+- IP-Adapter 角色一致性(用 prompt 工程替代)
|
|
|
+- I2V 动态视频生成
|
|
|
+- 复杂转场(仅淡入淡出+硬切)
|
|
|
+- 分镜编辑功能(MVP 不支持调整,直接生成)
|
|
|
+- 4K 输出
|
|
|
+- BGM 智能匹配(手动选风格)
|
|
|
+
|
|
|
+### 12.2 7 天开发计划
|
|
|
+
|
|
|
+| 天数 | 任务 | 产出 |
|
|
|
+|:--:|------|------|
|
|
|
+| **Day 1** | LLM 分镜脚本生成 | Prompt 模板 + JSON Schema 校验 + API 端点 |
|
|
|
+| **Day 2** | T2I 接入(通义万相) | Provider 封装 + 批量生成 + OSS 上传 |
|
|
|
+| **Day 3** | TTS 模块统一封装 | Provider 接口 + 现有三家的适配层 |
|
|
|
+| **Day 4** | FFmpeg 合成引擎 | Ken Burns + 拼接 + 字幕 + BGM + 编码 |
|
|
|
+| **Day 5** | 任务编排 + 进度推送 | BullMQ 队列 + WebSocket Gateway + Worker 串联 |
|
|
|
+| **Day 6** | 前端页面 | 创建页 + 进度页 + 播放页 |
|
|
|
+| **Day 7** | 联调 + 测试 + 上线 | 端到端测试 + 性能调优 + 部署 |
|
|
|
+
|
|
|
+### 12.3 MVP 环境要求
|
|
|
+
|
|
|
+```bash
|
|
|
+# 新增依赖
|
|
|
+# server/package.json
|
|
|
+{
|
|
|
+ "dependencies": {
|
|
|
+ "bullmq": "^5.0",
|
|
|
+ "ioredis": "^5.3",
|
|
|
+ "@nestjs/bullmq": "^10.0",
|
|
|
+ "@nestjs/websockets": "^10.0",
|
|
|
+ "@nestjs/platform-socket.io": "^10.0",
|
|
|
+ "@alicloud/openapi-client": "^0.4",
|
|
|
+ "@alicloud/wanxiang-20240614": "^1.0",
|
|
|
+ "fluent-ffmpeg": "^2.1",
|
|
|
+ "zod": "^3.22"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# 服务端新增环境变量
|
|
|
+# server/.env
|
|
|
+# === Video Generation ===
|
|
|
+VIDEO_T2I_PROVIDER=tongyi
|
|
|
+VIDEO_T2I_BACKUP=jimeng
|
|
|
+VIDEO_LLM_PROVIDER=claude
|
|
|
+VIDEO_LLM_BACKUP=qwen-max
|
|
|
+VIDEO_MAX_DURATION=600 # 最长支持 10 分钟
|
|
|
+VIDEO_MAX_CHARS=10000 # 最多 1 万字
|
|
|
+VIDEO_PREVIEW_RESOLUTION=512p # 预览分辨率
|
|
|
+
|
|
|
+# Redis (BullMQ 需要)
|
|
|
+REDIS_HOST=localhost
|
|
|
+REDIS_PORT=6379
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十三、成本估算
|
|
|
+
|
|
|
+### 13.1 单次生成成本(1080p, 3 分钟视频)
|
|
|
+
|
|
|
+| 环节 | 调用次数 | 单价 | 小计 |
|
|
|
+|------|:------:|:----:|:----:|
|
|
|
+| LLM 分镜 | 1 次 | ¥0.05 | ¥0.05 |
|
|
|
+| T2I 图片 | 10 张 | ¥0.10 | ¥1.00 |
|
|
|
+| TTS 配音 | 500 字 | ¥0.015 | ¥7.50 |
|
|
|
+| BGM | 1 首 | ¥0.50 | ¥0.50 |
|
|
|
+| FFmpeg 合成 | 1 次 | ¥0.20 | ¥0.20 |
|
|
|
+| **总计** | | | **¥9.25** |
|
|
|
+
|
|
|
+### 13.2 月度运营预估
|
|
|
+
|
|
|
+| 方案 | 月生成量 | 单次成本 | 月成本 | 定价 | 月利润 |
|
|
|
+|------|:------:|:------:|:------:|:----:|:-----:|
|
|
|
+| 保守 | 100 次 | ¥9.25 | ¥925 | ¥1,790 | ¥865 |
|
|
|
+| 中等 | 500 次 | ¥9.25 | ¥4,625 | ¥8,950 | ¥4,325 |
|
|
|
+| 乐观 | 2000 次 | ¥9.25 | ¥18,500 | ¥35,800 | ¥17,300 |
|
|
|
+
|
|
|
+### 13.3 服务器成本
|
|
|
+
|
|
|
+| 配置 | 用途 | 月费 |
|
|
|
+|------|------|:----:|
|
|
|
+| 4C8G 应用服务器 | Nest.js API | ¥200 |
|
|
|
+| 2C4G Redis | BullMQ 队列 | ¥100 |
|
|
|
+| 2C4G Worker x2 | 分镜 + 素材 | ¥200 |
|
|
|
+| GPU T4 Worker | FFmpeg 加速 | ¥500 |
|
|
|
+| OSS + CDN | 存储 + 分发 | ¥100 |
|
|
|
+| **总计** | | **¥1,100/月** |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十四、附录
|
|
|
+
|
|
|
+### A. 参考项目
|
|
|
+
|
|
|
+| 项目 | 特点 | 启发 |
|
|
|
+|------|------|------|
|
|
|
+| HeyGen | 数字人视频 | 用户可编辑分镜 |
|
|
|
+| Sora (OpenAI) | 文本 → 视频 | 端到端生成思路 |
|
|
|
+| 剪映 AI | 模板化视频 | Ken Burns + 转场的实用效果 |
|
|
|
+| RunwayML | AI 视频编辑 | 多模态视频生成 |
|
|
|
+| Invideo AI | 文本 → 营销视频 | 分镜 Pipeline 架构 |
|
|
|
+
|
|
|
+### B. 技术风险
|
|
|
+
|
|
|
+| 风险 | 概率 | 影响 | 应对 |
|
|
|
+|------|:----:|:----:|------|
|
|
|
+| AI 服务不稳定 | 高 | 中 | 多 Provider failover |
|
|
|
+| 画面质量差 | 中 | 高 | 分镜编辑 + 重新生成 |
|
|
|
+| 成本超预期 | 中 | 中 | 按用量计费 + 成本监控 |
|
|
|
+| 生成速度慢 | 低 | 中 | 并行 + GPU 加速 + 缓存 |
|
|
|
+| 用户生成违规内容 | 中 | 高 | 内容审核 + 敏感词过滤 |
|
|
|
+
|
|
|
+### C. 关键术语
|
|
|
+
|
|
|
+| 术语 | 英文 | 解释 |
|
|
|
+|------|------|------|
|
|
|
+| T2I | Text-to-Image | 文生图 |
|
|
|
+| TTS | Text-to-Speech | 文本转语音 |
|
|
|
+| I2V | Image-to-Video | 图生视频 |
|
|
|
+| LLM | Large Language Model | 大语言模型 |
|
|
|
+| Ken Burns | - | 静态图片缓慢缩放/平移的效果 |
|
|
|
+| IP-Adapter | - | 图像提示适配器,用于控制图像一致性 |
|
|
|
+| SRT | SubRip Text | 字幕文件格式 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+> **文档维护者**: AI Assistant
|
|
|
+> **下次评审**: MVP 开发完成后
|