You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

20 KiB

ADR 03: 分镜关键帧管理架构

状态:提议中 日期:2026-02-07 决策者:架构团队


目录


背景

业务需求

在 AI 视频生成流程中,分镜(storyboard)需要生成多张关键帧图片,用于指导 AI 生成流畅的视频。

核心需求

  1. 关键帧序列:一个分镜需要生成多张连续的关键帧图片(如:坐下 → 站起的动作序列)
  2. 多版本管理:用户可以为同一个分镜创建多个关键帧组(如"初稿"、"修改版"、"最终版")
  3. 生成类型:支持不同的生成方式(单图9宫格、单镜4图、单镜9图)
  4. AI 参考:生成的关键帧图片用于 AI 视频生成的参考,确保视频连贯性

业务场景示例

分镜 #1:孙悟空从坐下到站起来
├─ 场景:咖啡厅(白天)← 场景变体标签决定
├─ 角色:孙悟空(少年)← 角色变体标签决定
├─ 动作:从坐下到站起来
└─ 关键帧需求:
    ├─ 关键帧1:坐下
    ├─ 关键帧2:准备站起
    ├─ 关键帧3:半站起
    └─ 关键帧4:完全站起

用户期望

  • 可以为一个分镜生成多组关键帧(如"初稿"、"最终版")
  • 每组关键帧包含多张连续的图片(4张或9张)
  • 可以选择不同的生成类型(单镜4图、单镜9图等)
  • 关键帧图片用于 AI 视频生成的参考

当前架构

现有表结构

  1. storyboards 表:存储分镜信息

    • 包含:title、description、shot_size、camera_movement 等
    • 通过 storyboard_items 关联角色、场景、道具
  2. storyboard_images 表:存储分镜图片

    • 包含:url、ai_prompt、ai_prompt_id、ai_params、status 等
    • 已有 element_tag_id 字段(但未使用)
  3. project_element_tags 表:存储资源变体标签

    • 当前支持:角色变体、场景变体、道具变体
    • element_type:1=角色, 2=场景, 3=道具

当前问题

  • 无法为分镜创建多个关键帧组(如"初稿" vs "最终版")
  • 无法标识关键帧的顺序(第1帧、第2帧...)
  • 无法区分不同的生成类型(单镜4图 vs 单镜9图)
  • storyboard_images.element_tag_id 字段未使用

决策

核心方案

复用标签系统管理分镜关键帧组

将分镜的关键帧组视为"分镜的变体标签",复用现有的 project_element_tags 表:

  • 扩展 element_type 枚举,新增:4 = 分镜(storyboard)
  • 标签的 tag_label 表示关键帧组名称(如"初稿"、"最终版")
  • 标签的 meta_data 存储生成类型(generation_type)
  • storyboard_images 通过 element_tag_id 关联到关键帧组
  • 新增 frame_index 字段标识关键帧顺序

设计原则

  1. 架构统一:所有变体都用标签管理(角色变体、场景变体、道具变体、分镜关键帧组)
  2. 复用现有表:无需新建表,扩展现有的 project_element_tags
  3. 字段复用storyboard_images.element_tag_id 字段已存在,直接使用
  4. 语义清晰:分镜的"初稿"、"最终版"确实是变体的概念
  5. 扩展性好meta_data 可以存储生成类型等参数
  6. 向后兼容:不影响现有的角色/场景/道具变体标签

架构设计

1. 数据库表结构

1.1 扩展 project_element_tags 表

调整:扩展 element_type 枚举,新增分镜类型

-- 更新表注释
COMMENT ON COLUMN project_element_tags.element_type IS '元素类型:1=角色(character), 2=场景(location), 3=道具(prop), 4=分镜(storyboard)';

-- 确认索引存在
CREATE INDEX IF NOT EXISTS idx_project_element_tags_element 
    ON project_element_tags(element_type, element_id);

关键设计说明

  • 无需修改表结构:只需扩展枚举值
  • element_id 指向分镜:当 element_type = 4 时,element_id 指向 storyboards.storyboard_id
  • tag_label 是关键帧组名称:如"初稿"、"修改版"、"最终版"
  • meta_data 存储生成参数:如 {"generation_type": 2}

数据示例

-- 为分镜创建关键帧组标签
INSERT INTO project_element_tags (
    tag_id, element_type, element_id, tag_label, meta_data
) VALUES (
    '01936d8a-7b2c-7890-abcd-ef1234567890',
    4,  -- 分镜
    'storyboard-001',  -- 分镜ID
    '初稿',
    '{"generation_type": 2}'  -- 单镜4图
);

1.2 扩展 storyboard_images 表

调整:新增 frame_index 字段

-- 添加关键帧索引字段
ALTER TABLE storyboard_images 
ADD COLUMN frame_index INTEGER DEFAULT 0;

-- 添加索引
CREATE INDEX idx_storyboard_images_frame_index 
    ON storyboard_images(element_tag_id, frame_index);

-- 添加注释
COMMENT ON COLUMN storyboard_images.frame_index IS '关键帧索引(0=第1帧, 1=第2帧, 2=第3帧...)';

-- 更新 element_tag_id 注释
COMMENT ON COLUMN storyboard_images.element_tag_id IS '关联的关键帧组标签ID(指向 project_element_tags,element_type=4)';

关键设计说明

  • element_tag_id 字段已存在:直接使用,无需新增
  • frame_index 标识顺序:0=第1帧, 1=第2帧, 2=第3帧...
  • 保留现有字段:ai_prompt_id、ai_prompt、ai_params、status、version 等

1.3 Python 枚举定义

from enum import IntEnum

class ElementType(IntEnum):
    """元素类型"""
    CHARACTER = 1   # 角色
    LOCATION = 2    # 场景
    PROP = 3        # 道具
    STORYBOARD = 4  # 分镜(新增)

class StoryboardImageGenerationType(IntEnum):
    """分镜图片生成类型"""
    GRID_9 = 1          # 单图9宫格(一次生成9张不同的图)
    QUAD_4 = 2          # 单镜4图(一个分镜生成4张关键帧)
    GRID_9_SINGLE = 3   # 单镜9图(一个分镜生成9张关键帧)

2. 数据关系

storyboards(分镜表)
  ↓ storyboard_id
project_element_tags(标签表 - 关键帧组)
  ├─ tag_id: tag-001
  ├─ element_type: 4(分镜)
  ├─ element_id: storyboard-001
  ├─ tag_label: "初稿"
  └─ meta_data: {"generation_type": 2}
      ↓ element_tag_id
storyboard_images(图片表 - 关键帧)
  ├─ image_id: img-001, frame_index: 0, url: "..."
  ├─ image_id: img-002, frame_index: 1, url: "..."
  ├─ image_id: img-003, frame_index: 2, url: "..."
  └─ image_id: img-004, frame_index: 3, url: "..."

完整示例

分镜 #1(storyboard_id = 'sb-001')
  ↓
project_element_tags(分镜的关键帧组标签)
├─ tag_id: tag-001
│  ├─ element_type: 4(分镜)
│  ├─ element_id: 'sb-001'
│  ├─ tag_label: "初稿"
│  └─ meta_data: {"generation_type": 2}  // 单镜4图
│      ↓
│  storyboard_images("初稿"的4个关键帧)
│  ├─ image_id: img-001, frame_index: 0, url: "..."(坐下)
│  ├─ image_id: img-002, frame_index: 1, url: "..."(准备站起)
│  ├─ image_id: img-003, frame_index: 2, url: "..."(半站起)
│  └─ image_id: img-004, frame_index: 3, url: "..."(完全站起)
│
└─ tag_id: tag-002
   ├─ element_type: 4(分镜)
   ├─ element_id: 'sb-001'
   ├─ tag_label: "最终版"
   └─ meta_data: {"generation_type": 3}  // 单镜9图
       ↓
   storyboard_images("最终版"的9个关键帧)
   ├─ image_id: img-005, frame_index: 0, url: "..."
   ├─ image_id: img-006, frame_index: 1, url: "..."
   ├─ ...
   └─ image_id: img-013, frame_index: 8, url: "..."

3. API 设计

3.1 创建关键帧组

POST /api/v1/storyboards/{storyboard_id}/keyframe-groups

# 请求体
{
  "tagLabel": "初稿",
  "generationType": 2,  // 单镜4图
  "aiPromptId": "prompt-001",  // 可选,使用系统模板
  "aiPrompt": "smooth transition from sitting to standing",  // 可选,自定义 prompt
  "aiParams": {
    "seed": 42,
    "steps": 30,
    "cfgScale": 7.5
  }
}

# 响应
{
  "success": true,
  "code": 200,
  "message": "关键帧组创建成功",
  "data": {
    "tagId": "01936d8a-7b2c-7890-abcd-ef1234567890",
    "storyboardId": "01936d8a-1234-7890-abcd-ef1234567891",
    "tagLabel": "初稿",
    "generationType": 2,
    "generationTypeLabel": "单镜4图",
    "frameCount": 4,
    "status": "pending",
    "createdAt": "2026-02-07T10:30:00Z"
  },
  "timestamp": "2026-02-07T10:30:00Z"
}

后端处理逻辑

async def create_keyframe_group(
    storyboard_id: UUID,
    data: CreateKeyframeGroupRequest
) -> Dict[str, Any]:
    """创建关键帧组"""
    # 1. 创建标签(关键帧组)
    tag_id = generate_uuid_v7()
    await db.execute(
        """
        INSERT INTO project_element_tags (
            tag_id, element_type, element_id, tag_label, meta_data
        ) VALUES ($1, 4, $2, $3, $4)
        """,
        tag_id, storyboard_id, data.tag_label,
        {"generation_type": data.generation_type}
    )
    
    # 2. 根据 generation_type 确定关键帧数量
    frame_count = {
        1: 9,  # 单图9宫格
        2: 4,  # 单镜4图
        3: 9   # 单镜9图
    }[data.generation_type]
    
    # 3. 创建关键帧图片记录(status = 1 待生成)
    for i in range(frame_count):
        image_id = generate_uuid_v7()
        await db.execute(
            """
            INSERT INTO storyboard_images (
                image_id, storyboard_id, element_tag_id, frame_index,
                ai_prompt_id, ai_prompt, ai_params, status
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, 1)
            """,
            image_id, storyboard_id, tag_id, i,
            data.ai_prompt_id, data.ai_prompt, data.ai_params
        )
    
    return {"tag_id": tag_id, "frame_count": frame_count}

3.2 生成关键帧图片

POST /api/v1/storyboards/keyframe-groups/{tag_id}/generate

# 响应
{
  "success": true,
  "code": 200,
  "message": "关键帧生成任务已创建",
  "data": {
    "tagId": "01936d8a-7b2c-7890-abcd-ef1234567890",
    "status": "generating",
    "estimatedTime": 120,  // 预计生成时间(秒)
    "aiJobId": "01936d8a-7b2c-7890-abcd-ef1234567892"
  },
  "timestamp": "2026-02-07T10:30:00Z"
}

后端处理逻辑

async def generate_keyframes(tag_id: UUID) -> Dict[str, Any]:
    """生成关键帧图片"""
    # 1. 查询该标签下的所有图片记录
    images = await db.fetch_all(
        """
        SELECT * FROM storyboard_images
        WHERE element_tag_id = $1
        ORDER BY frame_index
        """,
        tag_id
    )
    
    # 2. 更新状态为生成中
    await db.execute(
        """
        UPDATE storyboard_images
        SET status = 2  -- 生成中
        WHERE element_tag_id = $1
        """,
        tag_id
    )
    
    # 3. 调用 Celery 异步任务生成图片
    job_id = generate_uuid_v7()
    celery_app.send_task(
        'tasks.generate_storyboard_keyframes',
        args=[tag_id, images, job_id]
    )
    
    return {"job_id": job_id, "frame_count": len(images)}

3.3 查询分镜的关键帧组

GET /api/v1/storyboards/{storyboard_id}/keyframe-groups

# 响应
{
  "success": true,
  "code": 200,
  "message": "查询成功",
  "data": {
    "items": [
      {
        "tagId": "01936d8a-7b2c-7890-abcd-ef1234567890",
        "tagLabel": "初稿",
        "generationType": 2,
        "generationTypeLabel": "单镜4图",
        "frameCount": 4,
        "completedCount": 4,
        "status": "completed",
        "frames": [
          {
            "imageId": "01936d8a-7b2c-7890-abcd-ef1234567893",
            "frameIndex": 0,
            "url": "https://...",
            "thumbnailUrl": "https://...",
            "width": 1024,
            "height": 768,
            "status": "completed"
          },
          {
            "imageId": "01936d8a-7b2c-7890-abcd-ef1234567894",
            "frameIndex": 1,
            "url": "https://...",
            "thumbnailUrl": "https://...",
            "width": 1024,
            "height": 768,
            "status": "completed"
          }
          // ... 另外2个关键帧
        ],
        "createdAt": "2026-02-07T10:30:00Z"
      },
      {
        "tagId": "01936d8a-7b2c-7890-abcd-ef1234567895",
        "tagLabel": "最终版",
        "generationType": 3,
        "generationTypeLabel": "单镜9图",
        "frameCount": 9,
        "completedCount": 0,
        "status": "pending",
        "frames": [],
        "createdAt": "2026-02-07T11:00:00Z"
      }
    ],
    "total": 2
  },
  "timestamp": "2026-02-07T11:00:00Z"
}

查询 SQL

-- 查询分镜的所有关键帧组
SELECT 
    t.tag_id,
    t.tag_label,
    t.meta_data->>'generation_type' as generation_type,
    COUNT(i.image_id) as frame_count,
    COUNT(CASE WHEN i.status = 3 THEN 1 END) as completed_count,
    MAX(i.status) as max_status,
    t.created_at
FROM project_element_tags t
LEFT JOIN storyboard_images i ON i.element_tag_id = t.tag_id
WHERE t.element_type = 4 
  AND t.element_id = 'storyboard-001'
GROUP BY t.tag_id, t.tag_label, t.meta_data, t.created_at
ORDER BY t.created_at DESC;

-- 查询某个关键帧组的所有图片
SELECT * FROM storyboard_images
WHERE element_tag_id = 'tag-001'
ORDER BY frame_index;

3.4 删除关键帧组

DELETE /api/v1/storyboards/keyframe-groups/{tag_id}

# 响应
{
  "success": true,
  "code": 200,
  "message": "关键帧组已删除",
  "timestamp": "2026-02-07T11:00:00Z"
}

后端处理逻辑

async def delete_keyframe_group(tag_id: UUID) -> None:
    """删除关键帧组"""
    # 1. 删除关键帧图片
    await db.execute(
        "DELETE FROM storyboard_images WHERE element_tag_id = $1",
        tag_id
    )
    
    # 2. 删除标签
    await db.execute(
        "DELETE FROM project_element_tags WHERE tag_id = $1",
        tag_id
    )

4. 用户工作流

1. 用户在分镜详情页,点击"生成关键帧"
   ↓
2. 弹出对话框,填写:
   - 关键帧组名称:如"初稿"
   - 生成类型:单镜4图(4个关键帧)
   - Prompt:自定义描述(可选)
   ↓
3. 点击"创建"
   → 调用 POST /api/v1/storyboards/{id}/keyframe-groups
   → 创建标签记录(element_type = 4)
   → 创建 4 条图片记录(status = 1 待生成)
   ↓
4. 点击"开始生成"
   → 调用 POST /api/v1/storyboards/keyframe-groups/{tag_id}/generate
   → 更新图片 status = 2(生成中)
   → 触发 Celery 异步任务
   ↓
5. Celery Worker 生成关键帧
   → 调用 AI 服务生成 4 张连续的关键帧图片
   → 更新图片 URL、status = 3(已完成)
   ↓
6. 前端轮询或 WebSocket 通知
   → 显示 4 张关键帧图片
   ↓
7. AI 视频生成时使用
   → 读取关键帧图片(按 frame_index 排序)
   → 根据关键帧生成中间的过渡帧
   → 生成流畅的视频

影响分析

正面影响

  1. 架构统一

    • 所有变体都用标签管理(角色、场景、道具、分镜)
    • 代码复用度高,维护成本低
  2. 实现简单

    • 无需新建表,只需扩展枚举
    • 复用现有的 element_tag_id 字段
    • 只需添加 1 个字段(frame_index)
  3. 扩展性好

    • meta_data 可以存储更多参数(如 seed、steps、cfg_scale)
    • 支持未来新增的生成类型
  4. 用户体验

    • 可以创建多个关键帧组对比效果
    • 关键帧顺序清晰(frame_index)
    • 支持重新生成
  5. AI 视频生成

    • 关键帧图片为 AI 提供精准参考
    • 确保视频连贯性和一致性

负面影响

  1. 实现成本

    • 需要修改 element_type 枚举
    • 需要添加 frame_index 字段
    • 需要更新前后端代码
    • 预计工作量:8-10人天
  2. 数据迁移

    • 需要执行数据库迁移脚本
    • 需要更新 Python 枚举定义
    • 风险:低(只是添加字段和枚举值)
  3. 查询复杂度

    • 需要 JOIN project_element_tags 和 storyboard_images
    • 需要过滤 element_type = 4
    • 缓解:添加索引,使用 Repository 层封装

缓解措施

  1. 性能优化

    • 为 (element_type, element_id) 创建复合索引
    • 为 (element_tag_id, frame_index) 创建复合索引
    • 对高频查询结果进行缓存
  2. 风险控制

    • 在测试环境充分测试
    • 保留旧数据,支持快速回滚
    • 分阶段发布(先后端,再前端)
  3. 用户体验

    • 提供清晰的关键帧组管理界面
    • 显示生成进度和状态
    • 支持预览和重新生成

实现计划

Phase 1:数据库迁移(1-2天)

任务

  • 编写数据库迁移脚本
    • 扩展 project_element_tags.element_type 注释(新增 4=分镜)
    • 添加 storyboard_images.frame_index 字段
    • 创建索引
  • 在测试环境执行迁移
  • 数据一致性校验

负责人:后端团队 风险:低

Phase 2:后端服务层(3-4天)

任务

  • 更新 Python 枚举
    • ElementType 新增 STORYBOARD = 4
    • 新增 StoryboardImageGenerationType 枚举
  • 创建 Service 层
    • StoryboardKeyframeService.create_keyframe_group()
    • StoryboardKeyframeService.generate_keyframes()
    • StoryboardKeyframeService.get_keyframe_groups()
    • StoryboardKeyframeService.delete_keyframe_group()
  • 更新 Repository 层
    • ProjectElementTagRepository 支持 element_type = 4
    • StoryboardImageRepository 支持 frame_index 查询
  • 编写单元测试

负责人:后端团队 风险:中等

Phase 3:API 接口开发(2-3天)

任务

  • 实现新增接口
    • POST /api/v1/storyboards/{id}/keyframe-groups
    • POST /api/v1/storyboards/keyframe-groups/{tag_id}/generate
    • GET /api/v1/storyboards/{id}/keyframe-groups
    • DELETE /api/v1/storyboards/keyframe-groups/{tag_id}
  • 编写 API 文档
  • 集成测试

负责人:后端团队 风险:低

Phase 4:Celery 任务开发(2天)

任务

  • 实现 Celery 任务
    • tasks.generate_storyboard_keyframes
    • 调用 AI 服务生成关键帧图片
    • 更新图片 URL 和状态
  • 错误处理和重试机制
  • 任务监控和日志

负责人:后端团队 风险:中等

Phase 5:前端适配(2-3天)

任务

  • 更新 API Service
    • client/src/services/api/storyboard-keyframes.ts(新增)
  • 更新 Hooks
    • client/src/hooks/api/useStoryboardKeyframes.ts(新增)
  • 更新组件
    • 分镜详情页添加"关键帧管理"面板
    • 实现关键帧组创建对话框
    • 实现关键帧图片展示
  • 类型定义更新
    • client/src/types/storyboard.ts
  • 前端测试

负责人:前端团队 风险:低

Phase 6:监控与优化(持续)

任务

  • 监控新接口的调用情况
  • 监控 AI 生成任务的成功率
  • 收集用户反馈
  • 优化查询性能

负责人:全团队 风险:低


相关文档

架构决策记录

技术栈规范(jointo-tech-stack)


变更记录

v1.0 (2026-02-07)

  • 初始版本
    • 提出分镜关键帧管理方案
    • 复用标签系统(project_element_tags)
    • 扩展 element_type 枚举,新增 4=分镜
    • 添加 storyboard_images.frame_index 字段
    • 支持多个关键帧组(如"初稿"、"最终版")
    • 支持不同的生成类型(单镜4图、单镜9图)
    • 完整的 API 设计和用户工作流
    • 完整的实现计划(Phase 1-6)

状态:提议中 决策日期:2026-02-07 最后更新:2026-02-07 预计工作量:8-10人天 预计完成时间:1-2周