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
20 KiB
ADR 03: 分镜关键帧管理架构
状态:提议中 日期:2026-02-07 决策者:架构团队
目录
背景
业务需求
在 AI 视频生成流程中,分镜(storyboard)需要生成多张关键帧图片,用于指导 AI 生成流畅的视频。
核心需求:
- 关键帧序列:一个分镜需要生成多张连续的关键帧图片(如:坐下 → 站起的动作序列)
- 多版本管理:用户可以为同一个分镜创建多个关键帧组(如"初稿"、"修改版"、"最终版")
- 生成类型:支持不同的生成方式(单图9宫格、单镜4图、单镜9图)
- AI 参考:生成的关键帧图片用于 AI 视频生成的参考,确保视频连贯性
业务场景示例:
分镜 #1:孙悟空从坐下到站起来
├─ 场景:咖啡厅(白天)← 场景变体标签决定
├─ 角色:孙悟空(少年)← 角色变体标签决定
├─ 动作:从坐下到站起来
└─ 关键帧需求:
├─ 关键帧1:坐下
├─ 关键帧2:准备站起
├─ 关键帧3:半站起
└─ 关键帧4:完全站起
用户期望:
- 可以为一个分镜生成多组关键帧(如"初稿"、"最终版")
- 每组关键帧包含多张连续的图片(4张或9张)
- 可以选择不同的生成类型(单镜4图、单镜9图等)
- 关键帧图片用于 AI 视频生成的参考
当前架构
现有表结构:
-
storyboards 表:存储分镜信息
- 包含:title、description、shot_size、camera_movement 等
- 通过 storyboard_items 关联角色、场景、道具
-
storyboard_images 表:存储分镜图片
- 包含:url、ai_prompt、ai_prompt_id、ai_params、status 等
- 已有
element_tag_id字段(但未使用)
-
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字段标识关键帧顺序
设计原则
- 架构统一:所有变体都用标签管理(角色变体、场景变体、道具变体、分镜关键帧组)
- 复用现有表:无需新建表,扩展现有的
project_element_tags表 - 字段复用:
storyboard_images.element_tag_id字段已存在,直接使用 - 语义清晰:分镜的"初稿"、"最终版"确实是变体的概念
- 扩展性好:
meta_data可以存储生成类型等参数 - 向后兼容:不影响现有的角色/场景/道具变体标签
架构设计
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 排序)
→ 根据关键帧生成中间的过渡帧
→ 生成流畅的视频
影响分析
正面影响
-
架构统一
- 所有变体都用标签管理(角色、场景、道具、分镜)
- 代码复用度高,维护成本低
-
实现简单
- 无需新建表,只需扩展枚举
- 复用现有的 element_tag_id 字段
- 只需添加 1 个字段(frame_index)
-
扩展性好
- meta_data 可以存储更多参数(如 seed、steps、cfg_scale)
- 支持未来新增的生成类型
-
用户体验
- 可以创建多个关键帧组对比效果
- 关键帧顺序清晰(frame_index)
- 支持重新生成
-
AI 视频生成
- 关键帧图片为 AI 提供精准参考
- 确保视频连贯性和一致性
负面影响
-
实现成本
- 需要修改 element_type 枚举
- 需要添加 frame_index 字段
- 需要更新前后端代码
- 预计工作量:8-10人天
-
数据迁移
- 需要执行数据库迁移脚本
- 需要更新 Python 枚举定义
- 风险:低(只是添加字段和枚举值)
-
查询复杂度
- 需要 JOIN project_element_tags 和 storyboard_images
- 需要过滤 element_type = 4
- 缓解:添加索引,使用 Repository 层封装
缓解措施
-
性能优化
- 为 (element_type, element_id) 创建复合索引
- 为 (element_tag_id, frame_index) 创建复合索引
- 对高频查询结果进行缓存
-
风险控制
- 在测试环境充分测试
- 保留旧数据,支持快速回滚
- 分阶段发布(先后端,再前端)
-
用户体验
- 提供清晰的关键帧组管理界面
- 显示生成进度和状态
- 支持预览和重新生成
实现计划
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 = 4StoryboardImageRepository支持 frame_index 查询
- 编写单元测试
负责人:后端团队 风险:中等
Phase 3:API 接口开发(2-3天)
任务:
- 实现新增接口
POST /api/v1/storyboards/{id}/keyframe-groupsPOST /api/v1/storyboards/keyframe-groups/{tag_id}/generateGET /api/v1/storyboards/{id}/keyframe-groupsDELETE /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周