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.
 

12 KiB

分镜服务完整实现总结

日期:2026-02-04
类型:功能实现总结
影响范围:后端服务层、API 层、数据库


实现概述

完成了分镜管理系统的核心功能实现,包括:

  1. 分镜基础管理(CRUD)
  2. 分镜看板服务(可视化展示)
  3. 项目素材引用计数(已有字段支持)

已完成功能

1. 分镜基础管理

数据库表

  • storyboards - 分镜主表
  • storyboard_items - 分镜元素关联表(统一管理 ElementTag 和 Resource)

核心功能

  • 分镜 CRUD 操作
  • 分镜排序管理(order_index 字段)
  • 分镜元素关联(ElementTag + Resource 统一管理)
  • 分镜筛选(按景别、运镜)
  • 分镜搜索(全文搜索)
  • 时长统计

API 端点(13个):

  • GET /storyboards - 获取分镜列表
  • GET /storyboards/{id} - 获取分镜详情
  • POST /storyboards - 创建分镜
  • PATCH /storyboards/{id} - 更新分镜
  • DELETE /storyboards/{id} - 删除分镜
  • POST /storyboards/reorder - 重新排序
  • GET /storyboards/filter - 筛选分镜
  • GET /storyboards/search - 搜索分镜
  • GET /storyboards/statistics/duration - 时长统计
  • POST /storyboards/{id}/items - 添加元素
  • DELETE /storyboards/{id}/items/{item_id} - 移除元素
  • PATCH /storyboards/{id}/items/{item_id} - 更新元素
  • POST /storyboards/{id}/items/reorder - 调整元素顺序

2. 分镜看板服务

设计特点

  • 无独立数据存储(实时从分镜表计算)
  • 六种轨道类型支持(当前实现 storyboard 轨道)
  • 时间轴计算(累积计算每个分镜的 start_time 和 end_time)

API 端点(4个):

  • GET /projects/{project_id}/storyboard-board - 获取看板数据
  • POST /projects/{project_id}/storyboard-board/reorder - 调整分镜顺序
  • GET /projects/{project_id}/storyboard-board/tracks/{track_type} - 获取轨道数据
  • GET /projects/{project_id}/storyboard-board/items/time-range - 时间范围查询

3. 项目素材引用计数

数据库字段

  • project_resources.usage_count - 引用计数字段(已存在)
  • CHECK 约束:usage_count >= 0
  • 索引:idx_project_resources_usage_count

实现状态

  • ⚠️ 字段已存在,但引用计数维护逻辑需要在 StoryboardService 中实现
  • ⚠️ 当前 add_element_to_storyboard()remove_element_from_storyboard() 方法未包含计数更新逻辑

技术栈符合性

完全符合 jointo-tech-stack 规范

  1. 数据库设计

    • UUID v7 应用层生成(generate_uuid()
    • 无物理外键约束
    • 枚举类型使用 SMALLINT + IntEnum
    • 时间戳使用 TIMESTAMPTZ
    • 全文搜索索引(GIN + pg_trgm)
  2. 日志系统

    • 使用标准库 logging
    • 使用 %-formatting 格式化
    • 异常日志使用 exc_info=True
  3. 异步编程

    • 所有数据库操作使用 async/await
    • 使用 AsyncSession
  4. 事务管理

    • Repository 使用 flush()
    • Service 使用 commit()
    • 批量操作使用事务
  5. 统一响应格式

    • 所有 API 使用 ApiResponse 格式

数据库表结构

storyboards 表

字段 类型 说明
storyboard_id UUID 主键(UUID v7)
project_id UUID 所属项目
title VARCHAR(255) 分镜标题
description TEXT 分镜描述
shooting_description TEXT 拍摄描述
shot_size SMALLINT 景别类型(1-8)
camera_movement SMALLINT 运镜类型(1-9)
estimated_duration NUMERIC(10,3) 预估时长(秒)
actual_duration NUMERIC(10,3) 实际时长(秒)
start_time NUMERIC(10,3) 时间轴开始时间
end_time NUMERIC(10,3) 时间轴结束时间
thumbnail_url TEXT 缩略图 URL
thumbnail_id UUID 缩略图附件 ID
order_index INTEGER 显示顺序(镜号)
transition_type TEXT 转场类型
transition_duration NUMERIC(5,2) 转场时长
metadata JSONB 扩展字段
created_at TIMESTAMPTZ 创建时间
updated_at TIMESTAMPTZ 更新时间

索引

  • idx_storyboards_project_id
  • idx_storyboards_order
  • idx_storyboards_shot_size
  • idx_storyboards_camera_movement
  • idx_storyboards_thumbnail_id
  • idx_storyboards_metadata_gin
  • idx_storyboards_title_trgm(全文搜索)
  • idx_storyboards_description_trgm(全文搜索)
  • idx_storyboards_shooting_description_trgm(全文搜索)

约束

  • storyboards_order_unique(project_id, order_index) 唯一
  • storyboards_time_checkend_time > start_time
  • storyboards_duration_check:时长必须 > 0

storyboard_items 表

字段 类型 说明
item_id UUID 主键(UUID v7)
storyboard_id UUID 所属分镜
item_type SMALLINT 元素类型(1=ElementTag, 2=Resource)
element_tag_id UUID 剧本元素标签 ID
resource_id UUID 项目素材 ID
element_name TEXT 元素名称(冗余)
tag_label TEXT 标签名称(冗余)
cover_url TEXT 封面 URL(冗余)
is_visible BOOLEAN 是否在画面内
spatial_position TEXT 画面位置
action_description TEXT 动作描述
display_order INTEGER 显示顺序
z_index INTEGER 视觉层级
metadata JSONB 扩展属性
created_at TIMESTAMPTZ 创建时间

索引

  • idx_storyboard_items_storyboard_id
  • idx_storyboard_items_element_tag
  • idx_storyboard_items_resource
  • idx_storyboard_items_order
  • idx_storyboard_items_type

约束

  • storyboard_items_one_fk_check:确保只有一个外键字段非空
  • storyboard_items_tag_unique(storyboard_id, element_tag_id) 唯一
  • storyboard_items_resource_unique(storyboard_id, resource_id) 唯一

待实现功能

1. 引用计数维护逻辑

需要在 StoryboardService 中实现:

async def add_element_to_storyboard(self, ...):
    # ... 现有逻辑 ...
    
    # 如果是项目素材,增加引用计数
    if item_type == ItemType.RESOURCE and resource_id:
        await self.session.execute(
            update(ProjectResource)
            .where(ProjectResource.project_resource_id == resource_id)
            .values(usage_count=ProjectResource.usage_count + 1)
        )
    
    # ... 提交事务 ...

async def remove_element_from_storyboard(self, ...):
    # ... 现有逻辑 ...
    
    # 如果是项目素材,减少引用计数
    if item.item_type == ItemType.RESOURCE and item.resource_id:
        await self.session.execute(
            update(ProjectResource)
            .where(ProjectResource.project_resource_id == item.resource_id)
            .values(usage_count=ProjectResource.usage_count - 1)
        )
    
    # ... 提交事务 ...

2. 删除保护

需要在 ProjectResourceService.delete_resource() 中添加:

async def delete_resource(self, user_id, resource_id, force=False):
    resource = await self.repo.get_by_id(resource_id)
    
    # 检查引用计数
    if resource.usage_count > 0 and not force:
        raise ValidationError(
            "该素材正在被 %d 个分镜使用,无法删除。"
            "请先从分镜中移除该素材,或使用强制删除。" % resource.usage_count
        )
    
    # ... 删除逻辑 ...

3. 其他轨道数据

StoryboardBoardService._calculate_storyboard_board_tracks() 中添加:

  • resource 轨道(从 storyboard_items 查询)
  • video 轨道(需要实现 storyboard_videos 表)
  • sound 轨道(需要实现 storyboard_sound_effects 表)
  • subtitle 轨道(需要实现 storyboard_dialogues 表)
  • voice 轨道(需要实现 storyboard_voiceovers 表)

性能优化建议

1. 分镜看板缓存

# 使用 Redis 缓存分镜看板数据(5 分钟)
cache_key = "storyboard-board:%s" % project_id
cached_data = await redis_client.get(cache_key)
if cached_data:
    return StoryboardBoardData.parse_raw(cached_data)

# 计算数据并缓存
storyboard_board_data = await self.get_storyboard_board_data(...)
await redis_client.setex(cache_key, 300, storyboard_board_data.json())

2. 分页加载

对于大型项目(100+ 分镜),实现分页:

async def get_storyboard_board_data_paginated(
    self, user_id, project_id, page=1, page_size=50
):
    offset = (page - 1) * page_size
    storyboards = await self.storyboard_repo.get_by_project(
        project_id, order_by='order_index', limit=page_size, offset=offset
    )
    # ... 计算轨道数据 ...

3. 预加载关联数据

使用 selectinload() 减少 N+1 查询:

from sqlalchemy.orm import selectinload

storyboards = await session.execute(
    select(Storyboard)
    .where(Storyboard.project_id == project_id)
    .options(
        selectinload(Storyboard.items),
        selectinload(Storyboard.generated_video),
        selectinload(Storyboard.dialogues)
    )
    .order_by(Storyboard.order_index)
)

测试建议

单元测试

StoryboardService

  • test_create_storyboard() - 测试创建分镜
  • test_update_storyboard() - 测试更新分镜
  • test_delete_storyboard() - 测试删除分镜
  • test_reorder_storyboards() - 测试重新排序
  • test_add_element_to_storyboard() - 测试添加元素
  • test_remove_element_from_storyboard() - 测试移除元素
  • test_search_storyboards() - 测试搜索功能

StoryboardBoardService

  • test_get_storyboard_board_data() - 测试获取看板数据
  • test_reorder_storyboards() - 测试调整分镜顺序
  • test_get_track_items() - 测试获取轨道数据
  • test_get_items_by_time_range() - 测试时间范围查询

集成测试

API 测试

  • test_storyboard_crud_api() - 测试分镜 CRUD 端点
  • test_storyboard_board_api() - 测试分镜看板端点
  • test_storyboard_filter_api() - 测试筛选端点
  • test_storyboard_search_api() - 测试搜索端点

相关文档


总结

分镜管理系统的核心功能已完整实现,包括:

  • 分镜 CRUD 和排序管理
  • 分镜元素关联(统一管理 ElementTag 和 Resource)
  • 分镜筛选和搜索
  • 分镜看板服务(实时计算,无独立存储)
  • 数据库表结构和索引
  • 17 个 API 端点

待完善

  • ⏸️ 引用计数维护逻辑(字段已存在,需添加更新逻辑)
  • ⏸️ 其他轨道数据(resource、video、sound、subtitle、voice)
  • ⏸️ 性能优化(缓存、分页、预加载)
  • ⏸️ 单元测试和集成测试

系统已具备基础的分镜管理和可视化看板功能,可以支持分镜的创建、编辑、排序、筛选、搜索和看板展示。


变更作者:Kiro AI
审核状态:待审核
部署状态:待部署