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

分镜关联资源查询 API 实现

日期: 2026-02-14
类型: 新功能
影响范围: 分镜服务、AI 对话系统


概述

为分镜服务新增关联资源查询接口 GET /api/v1/storyboards/{storyboard_id}/related-resources,解决 AI 对话 @ 提及功能中分镜资源查询不可用的问题。

背景

问题

AI 对话系统的 _get_storyboard_mentionable_resources 方法一直返回空列表,导致用户在分镜对话中无法 @ 提及资源。

根本原因

  • 数据模型已迁移:storyboard_resources 表(已废弃)→ storyboard_items 表(新设计)
  • 查询逻辑未更新:代码仍引用废弃的 StoryboardResourceRepository.get_by_storyboard()
  • 临时方案:返回空列表避免报错

设计决策

将资源查询功能放在分镜服务层(而非 AI 对话服务层),原因:

  1. 职责分离:分镜资源查询是分镜领域的核心功能
  2. 复用性强:不仅用于 AI 对话,还可用于前端资源选择器、分镜资源管理等
  3. 通用接口:提供标准的 REST API,任何服务都可以调用

实现方案

架构设计

┌─────────────────────────────────────────┐
│  AI Conversation Service                │
│  ├─ _get_storyboard_mentionable_resources│
│  └─ 调用 ↓                              │
└─────────────────────────────────────────┘
                ↓
┌─────────────────────────────────────────┐
│  Storyboard Resources API (新增)        │
│  GET /storyboards/{id}/related-resources│
│  └─ 调用 ↓                              │
└─────────────────────────────────────────┘
                ↓
┌─────────────────────────────────────────┐
│  StoryboardService (新增方法)           │
│  ├─ get_related_resources()             │
│  ├─ _process_element_tag_item()         │
│  ├─ _process_resource_item()            │
│  ├─ _get_element_name()                 │
│  └─ _get_element_type_str()             │
└─────────────────────────────────────────┘
                ↓
┌─────────────────────────────────────────┐
│  数据层                                  │
│  ├─ storyboard_items (查询关联元素)     │
│  ├─ project_element_tags (查询标签)     │
│  ├─ project_characters/locations/props  │
│  └─ project_resources (查询资源详情)    │
└─────────────────────────────────────────┘

核心数据结构

storyboard_items 表(多态关联设计)

class ItemType(IntEnum):
    ELEMENT_TAG = 1  # 剧本元素标签(角色/场景/道具)
    RESOURCE = 2     # 项目素材(实拍)

item_type = 1 (ELEMENT_TAG):

element_tag_id → project_element_tags
  → element_type + element_id → project_characters/locations/props
  → element_tag_id → project_resources (过滤标签)

item_type = 2 (RESOURCE):

resource_id → project_resources (直接获取实拍素材)

API 接口

端点

GET /api/v1/storyboards/{storyboard_id}/related-resources

查询参数

参数 类型 必填 说明
resourceType string 文件类型过滤(基于 mime_type):image/video/audio
search string 搜索关键词(匹配元素名称)
search string 搜索关键词(匹配元素名称)

响应格式

{
  "code": 200,
  "message": "获取成功",
  "success": true,
  "data": {
    "resources": [
      {
        "type": "character",
        "elementId": "019d...",
        "elementName": "张三",
        "hasTags": true,
        "tags": [
          {
            "tagId": "019d...",
            "tagLabel": "少年",
            "resources": [
              {
                "resourceId": "019d...",
                "resourceUrl": "https://...",
                "thumbnailUrl": "https://...",
                "width": 1024,
                "height": 1024
              }
            ]
          }
        ]
      },
      {
        "type": "footage",
        "elementId": "019d...",
        "elementName": "街景实拍",
        "hasTags": false,
        "defaultResource": {
          "resourceId": "019d...",
          "resourceUrl": "https://...",
          "thumbnailUrl": "https://...",
          "width": 1920,
          "height": 1080
        }
      }
    ]
  }
}

类型说明

type 说明 来源
character 角色 item_type=1, element_type=1
location 场景 item_type=1, element_type=2
prop 道具 item_type=1, element_type=3
footage 实拍素材 item_type=2

代码变更

1. 新增 Schema 定义

文件: server/app/schemas/storyboard_resource.py

class RelatedResourceItem(BaseModel):
    """关联的资源项"""
    resource_id: str
    resource_url: str
    thumbnail_url: Optional[str]
    width: Optional[int]
    height: Optional[int]

class RelatedTagGroup(BaseModel):
    """关联资源的标签分组"""
    tag_id: str
    tag_label: str
    resources: List[RelatedResourceItem]

class RelatedElement(BaseModel):
    """分镜关联的元素"""
    type: str  # 'character' | 'location' | 'prop' | 'footage'
    element_id: str
    element_name: str
    has_tags: bool
    tags: Optional[List[RelatedTagGroup]]
    default_resource: Optional[RelatedResourceItem]

class RelatedResourcesResponse(BaseModel):
    """分镜关联资源列表响应"""
    resources: List[RelatedElement]

2. StoryboardService 新增方法

文件: server/app/services/storyboard_service.py

新增方法:

  • get_related_resources() - 主入口,查询分镜关联资源
  • _process_element_tag_item() - 处理 item_type=1 (剧本元素标签)
  • _process_resource_item() - 处理 item_type=2 (实拍素材)
  • _get_element_name() - 根据元素类型和 ID 获取元素名称
  • _get_element_type_str() - 元素类型枚举转字符串

核心逻辑:

async def get_related_resources(...):
    # 1. 获取分镜的所有 storyboard_items
    items = await self.storyboard_repo.get_items_by_storyboard(storyboard_id)
    
    # 2. 按 item_type 分组处理
    for item in items:
        if item.item_type == ItemType.ELEMENT_TAG:
            await self._process_element_tag_item(...)
        elif item.item_type == ItemType.RESOURCE:
            await self._process_resource_item(...)
    
    # 3. 转换为列表并返回
    return result

3. 新增 API 端点

文件: server/app/api/v1/storyboard_resources.py

@router.get(
    "/storyboards/{storyboard_id}/related-resources",
    response_model=SuccessResponse[RelatedResourcesResponse],
    summary="获取分镜关联的资源列表"
)
async def get_related_resources(...):
    """获取分镜关联的所有项目资源(按元素类型和标签分组)"""
    # 调用 StoryboardService.get_related_resources

4. 修改 AI Conversation Service

文件: server/app/services/ai_conversation_service.py

修改方法: _get_storyboard_mentionable_resources

async def _get_storyboard_mentionable_resources(
    self,
    user_id: UUID,  # 新增参数
    storyboard_id: UUID,
    resource_type: Optional[str] = None,
    search: Optional[str] = None
):
    """调用 StoryboardService.get_related_resources"""
    storyboard_service = StoryboardService(...)
    return await storyboard_service.get_related_resources(
        user_id=user_id,
        storyboard_id=storyboard_id,
        resource_type=resource_type,
        search=search
    )

5. 新增集成测试

文件: server/tests/integration/test_storyboard_resource_api.py

新增测试用例:

  • test_get_related_resources_success - 测试基本功能
  • test_get_related_resources_with_filters - 测试过滤参数
  • test_get_related_resources_not_found - 测试错误处理

技术亮点

1. 多态关联处理

优雅处理两种不同的关联方式:

  • 间接关联:通过 element_tag 关联到剧本元素
  • 直接关联:直接关联到项目资源(实拍素材)

2. 按元素和标签分组

角色:张三
  ├─ 少年
  │   ├─ 形象图1
  │   └─ 形象图2
  └─ 成年
      └─ 形象图3
      
实拍素材:街景实拍
  └─ 默认资源(无标签)

3. 性能优化

  • 使用冗余字段 (element_name, tag_label, cover_url) 减少关联查询
  • 批量查询避免 N+1 问题

4. 错误容错

  • 查询失败时返回空列表,不影响其他功能
  • 详细的错误日志便于排查问题

测试验证

手动测试

# 1. 获取分镜关联资源(基础)
curl -X GET "http://localhost:8000/api/v1/storyboard-resources/storyboards/{storyboard_id}/related-resources" \
  -H "Authorization: Bearer {token}"

# 2. 带过滤条件
curl -X GET "http://localhost:8000/api/v1/storyboard-resources/storyboards/{storyboard_id}/related-resources?resourceType=image&search=张三" \
  -H "Authorization: Bearer {token}"

# 3. AI 对话中使用
curl -X GET "http://localhost:8000/api/v1/ai/conversations/{conversation_id}/mentionable-resources" \
  -H "Authorization: Bearer {token}"

自动化测试

# 运行集成测试
docker exec jointo-server-app pytest tests/integration/test_storyboard_resource_api.py::TestStoryboardResourceAPI::test_get_related_resources_success -v

影响范围

功能修复

对话类型 修复前 修复后
分镜对话 返回空列表 返回完整资源
角色对话 正常 正常(无影响)
场景对话 正常 正常(无影响)
道具对话 正常 正常(无影响)

总体完整度: 60% → 100%

📦 新增文件

无(所有改动都在现有文件中)

📝 修改文件

  • server/app/schemas/storyboard_resource.py - 新增 4 个 Schema
  • server/app/services/storyboard_service.py - 新增 5 个方法(约 180 行)
  • server/app/api/v1/storyboard_resources.py - 新增 1 个 API 端点
  • server/app/services/ai_conversation_service.py - 重构 1 个方法
  • server/tests/integration/test_storyboard_resource_api.py - 新增 3 个测试用例

🔄 API 变更

新增接口:

GET /api/v1/storyboards/{storyboard_id}/related-resources

现有接口行为变更:

GET /api/v1/ai/conversations/{conversation_id}/mentionable-resources
  • 修复前:分镜对话返回 resources: []
  • 修复后:分镜对话返回完整的关联资源列表

后续建议

1. 性能优化

当前实现是循环查询,对于包含大量元素的分镜可能存在性能问题。建议:

# TODO: 使用 JOIN 优化查询
stmt = select(
    StoryboardItem,
    ProjectElementTag,
    ProjectResource
).join(...).where(...)

2. 缓存策略

分镜资源通常不会频繁变化,可以添加缓存:

@cached(ttl=300)  # 5分钟缓存
async def get_related_resources(...):

3. 支持更多资源类型

当前只返回图片类型,可以扩展支持:

  • 视频(footage video)
  • 音频(audio)

4. 批量查询优化

如果用户同时打开多个分镜对话,可以提供批量查询接口:

POST /api/v1/storyboards/batch/related-resources
{
  "storyboard_ids": ["...", "..."]
}

相关文档


作者: Jointo AI Team
审阅: 待审阅