# ADR 03: 分镜关键帧管理架构 > **状态**:提议中 > **日期**:2026-02-07 > **决策者**:架构团队 --- ## 目录 - [背景](#背景) - [业务需求](#业务需求) - [当前架构](#当前架构) - [决策](#决策) - [核心方案](#核心方案) - [设计原则](#设计原则) - [架构设计](#架构设计) - [1. 数据库表结构](#1-数据库表结构) - [2. 数据关系](#2-数据关系) - [3. API 设计](#3-api-设计) - [4. 用户工作流](#4-用户工作流) - [影响分析](#影响分析) - [正面影响](#正面影响) - [负面影响](#负面影响) - [缓解措施](#缓解措施) - [实现计划](#实现计划) - [相关文档](#相关文档) - [变更记录](#变更记录) --- ## 背景 ### 业务需求 在 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` 枚举,新增分镜类型 ```sql -- 更新表注释 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}` **数据示例**: ```sql -- 为分镜创建关键帧组标签 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` 字段 ```sql -- 添加关键帧索引字段 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 枚举定义 ```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 创建关键帧组 ```http 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" } ``` **后端处理逻辑**: ```python 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 生成关键帧图片 ```http 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" } ``` **后端处理逻辑**: ```python 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 查询分镜的关键帧组 ```http 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**: ```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 删除关键帧组 ```http DELETE /api/v1/storyboards/keyframe-groups/{tag_id} # 响应 { "success": true, "code": 200, "message": "关键帧组已删除", "timestamp": "2026-02-07T11:00:00Z" } ``` **后端处理逻辑**: ```python 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 生成任务的成功率 - [ ] 收集用户反馈 - [ ] 优化查询性能 **负责人**:全团队 **风险**:低 --- ## 相关文档 ### 架构决策记录 - [ADR 01: 资源归属从剧本级迁移到项目级](01-project-level-resource-ownership.md) - [ADR 02: 跨项目资源共享](02-cross-project-resource-sharing.md) ### 技术栈规范(jointo-tech-stack) - [数据库设计规范](../../.claude/skills/jointo-tech-stack/references/database.md) - [数据库迁移规范](../../.claude/skills/jointo-tech-stack/references/migration.md) - [后端架构规范](../../.claude/skills/jointo-tech-stack/references/backend.md) - [API 设计规范](../../.claude/skills/jointo-tech-stack/references/api-design.md) --- ## 变更记录 **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周