文本自动生成视频系统详细设计方案.md 49 KB

文本自动生成视频系统 — 详细技术设计方案

版本: v1.0
日期: 2026-05-29
状态: 设计阶段


目录

  1. 概述与目标
  2. 核心链路
  3. 第一层:文本解析层
  4. 第二层:素材生成层
  5. 第三层:视频合成层
  6. 第四层:输出交付层
  7. 服务端架构
  8. 数据库设计
  9. API 设计
  10. 核心难点与解决方案
  11. 与现有项目融合
  12. 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 分镜脚本数据结构

// 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) 模块

实时进度设计

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. WebSocketws://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,无需额外处理。

长文本分段处理

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 的缩放/平移效果:

# 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 合成脚本生成器

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 字幕生成

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
复用缓存 同一图片多个分镜只生成一次

六、第四层:输出交付层

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 详细设计

// === 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 进度推送

@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 核心表

-- 视频任务主表
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 费用记录表

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

// === 视频任务 ===

// 创建视频任务
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 容错设计

// 多层容错机制
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 环境要求

# 新增依赖
# 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 开发完成后