# 文本自动生成视频系统 — 详细技术设计方案 > 版本: 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 { 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( providers: string[], generateFn: (provider: string) => Promise, maxRetries = 3 ): Promise { 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 { 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 开发完成后