# Agent 分镜增强设计

## 背景

当前 Agent 制作流程在 `storyboard_gen` 阶段会批量生成分镜，并把以下信息落到 `storyboards` 表：

- `short_description`
- `detailed_description`
- `characters`
- `start_frame_prompt`
- `motion_script`

但实际使用中仍有两个明显缺口：

1. `characters` 里混杂了角色引用和场景引用，前端无法稳定区分“出镜角色”和“分镜场景”。
2. 视频生成提示词目前依赖单条接口 `POST /projects/{projectId}/storyboards/{storyboardId}/generate-prompt` 临时生成，Agent 跑完后并不会把这部分结果落库，因此 Agent 页面和分镜页都不能把“完整可用”的分镜成果直接展示出来。

本次设计目标是让 Agent 制作完成后，每条分镜都已经具备：

- 分镜描述
- 出镜角色
- 分镜场景
- 首帧提示词
- 镜头运动描述
- 视频生成提示词

## 目标

### 业务目标

- Agent 完成后，分镜结果不再只有“001 + 分镜描述”，而是可直接用于审核和后续视频生成的完整产物。
- 分镜场景从混杂字符串中拆出来，变成明确字段。
- 视频生成提示词自动生成并持久化，不再要求用户逐条手动点“生成提示词”。

### 非目标

- 不在本次改造中重做整个分镜编辑器。
- 不把 `storyboards` 改造成全量结构化 JSON 模型。
- 不改现有视频生成主流程的入参协议，只补充分镜默认可用数据。
- 不在本次改动 `end_frame_prompt` 的语义与展示。
- 不在本次支持一个分镜横跨多个场景引用。

## 方案对比

### 方案 A：只改前端展示

直接把现有 `characters / startFramePrompt / motionScript` 展示出来。

优点：

- 改动最小

缺点：

- 场景仍然混在 `characters` 里
- 没有持久化 `videoPrompt`
- Agent 结果仍不完整

### 方案 B：中等改版，新增明确字段并在 Agent 内补全

保留现有分镜字段，新增 `scene_ref` 与 `video_prompt`，Agent 在生成完基础分镜后自动补齐视频提示词并落库。

优点：

- 兼容旧数据
- 字段语义清晰
- 可复用现有 `generatePrompt` 能力
- Agent 结果直接可用

缺点：

- Agent 总耗时会增加
- 需要一次数据库迁移和一轮批量补全逻辑

### 方案 C：大改为全结构化分镜模型

把分镜彻底改成 `characterRefs[] / sceneRef / imagePrompt / videoPrompt / dialogueSegments[]` 等结构。

优点：

- 长期最清晰

缺点：

- 改动范围过大
- 需要同时改 API、前端编辑器、导入导出与视频生成链路

### 结论

采用方案 B。

## 核心设计

### 1. 数据模型

在 `storyboards` 表新增两个字段，迁移文件命名为 `V17__storyboard_scene_ref_video_prompt.sql`：

- `scene_ref VARCHAR(128) NULL`
  - 存单个场景引用，格式统一为 `@场景名`
  - 仅表示该分镜主场景
  - 显式不支持多场景
- `video_prompt LONGTEXT NULL`
  - 存分镜最终视频生成提示词

保留原字段：

- `characters`
- `start_frame_prompt`
- `motion_script`
- `end_frame_prompt`，本次不动，沿用现状

其中 `characters` 在本次改造后只表达“出镜角色引用”，不再混入场景引用。旧数据继续兼容，读取时允许为空或保留旧格式。

### 2. 分镜生成输出

更新 `StoryboardPipelineServiceImpl.generateStoryboards(...)` 的大模型输出 schema，要求模型返回：

- `characters`
  - 仅角色引用，格式 `@角色A,@角色B`
- `scene_ref`
  - 单个场景引用，格式 `@咖啡馆`
- `start_frame_prompt`
- `motion_script`
- 现有分镜描述字段

同时保留后端兜底：

- 若模型未返回 `scene_ref`，则从旧格式 `characters` 中按项目场景名单提取第一个场景引用写入 `scene_ref`
- 若模型仍把场景混入 `characters`，则在服务端拆分并清洗，最终只把角色引用落入 `characters`

这样既提升新数据质量，也兼容旧 prompt 风格和非理想输出。

### 3. 视频提示词持久化

复用现有 `StoryboardPipelineService.generatePrompt(Long storyboardId, Long tenantId)`，但新增一层“生成并保存”能力：

- 新增服务方法，例如：
  - `populateVideoPrompt(Long storyboardId, Long tenantId)`
  - `populateVideoPrompts(List<Long> storyboardIds, Long tenantId)`

行为：

1. 调用现有提示词生成逻辑
2. 将结果写入 `storyboards.video_prompt`
3. 返回更新后的分镜对象或提示词内容

为了避免重复维护 prompt 模板，本次不新写第二套视频提示词生成 prompt，而是直接复用现有 `generatePrompt` 主逻辑。

#### generatePrompt 用户消息模板调整

现有 `generatePrompt` 在拼装“当前分镜信息”时，只引用了 `sb.getCharacters()`。本次改造后 `characters` 不再混入场景引用，必须同步调整 user message 模板：

- 在“出场角色/场景”行之外新增“分镜场景：${sceneRef}”行
- 当 `sceneRef` 为空且 `characters` 仍为旧格式时，回退到现有逻辑读旧 `characters`
- 否则提示词会丢失场景上下文，质量明显回退

#### 计费策略

`generatePrompt` 自带 `billingService.checkBalance` + `charge`，`operation=storyboard_prompt_generate`，`unitCount=1`。Agent 自动批量补全后，每个项目会多出“分镜数”次 LLM 调用与扣费：

- 复用同一个 `operation=storyboard_prompt_generate` 计费 key，不新增计费项
- 在补全开始前先按“剩余待补条数”做一次合并 `checkBalance`，避免逐条触发预检
- 实际扣费仍按条记账，便于审计
- 余额不足导致中途失败时：已成功条数保留，剩余分镜 `video_prompt` 留空，整步标记失败

### 4. Agent 流程调整

保持 Agent 现有步骤数量不变，仍然保留 `storyboard_gen` 这一步，对前端步骤卡片和 SSE 事件名不做额外拆分。

但 `storyboard_gen` 内部流程改为两段：

1. 为每一集生成基础分镜
2. 对该集刚生成的分镜批量补全 `video_prompt`

建议每集内按“生成分镜 -> 生成该集视频提示词”的顺序执行，而不是全项目先生成完分镜再统一补 prompt。理由：

- 更利于日志输出和进度理解
- 单集失败时影响范围更小
- 方便未来扩展为“按集重跑”

`storyboard_gen` 结束时，产出应包含：

- 总分镜数
- 成功写入 `video_prompt` 的数量

SSE 日志建议新增类似文案：

- `第 1 集分镜：12 个镜头`
- `第 1 集视频提示词：12 条已生成`
- `第 1 集分镜生成中 6/12`
- `第 1 集提示词生成中 3/12`

#### 并发策略

提示词补全默认在**同集内**用固定并发度执行，默认值建议 `3`，配置上限建议不超过 `5`：

- 串行下 5 集 × 10 镜 ≈ 50 次 LLM 串行，Agent 总耗时翻倍，体验差
- 跨集仍按集顺序执行，保证日志和失败定位清晰
- 并发度取自配置，便于按账户配额或模型 RPM 调整
- 任一并发任务抛出余额不足异常时，立即中断同集后续任务并标记步骤失败

#### 重跑与幂等

本次只为 **`video_prompt` 补全链路** 引入幂等保证，不改变基础分镜生成 `generateStoryboards(...)` 的现有语义。

- 若某集基础分镜已经存在，但部分或全部 `video_prompt` 为空，则 Agent 重跑时允许只补全缺失的 `video_prompt`
- 若某集基础分镜与 `video_prompt` 都已齐全，则跳过该集的提示词补全
- 若用户希望“整集分镜从头重做”，仍沿用现有做法：先手动删除该集分镜，再重新生成

这样可以修复“分镜已生成但提示词补全失败”的场景，同时避免这次把“基础分镜重跑语义”一起做大。

### 5. API 与 DTO

现状澄清：`StoryboardDTO` 已经透传 `startFramePrompt` 与 `motionScript`，前端“看不到”是渲染层未消费这两个字段。本次真正需要新增的只有：

- `sceneRef: string | null`
- `videoPrompt: string | null`

扩展点：

- 后端 `StoryboardDTO` 新增上述两字段并在 `from(...)` 中映射
- 前端 `Storyboard` 类型同步新增对应字段

现有接口不新增新路由，直接在已有分镜列表/详情返回中带出新字段：

- `GET /projects/{projectId}/episodes/{episodeId}/storyboards`
- `GET /projects/{projectId}/storyboards`
- `POST /projects/{projectId}/episodes/{episodeId}/storyboards/generate`
- `PUT /projects/{projectId}/storyboards/{storyboardId}`

保留现有单条 `generate-prompt` 接口，但语义改成：

- 生成最新提示词
- 同步回写 `storyboards.video_prompt`
- 返回 `prompt`

这样前端无论走 Agent 自动生成，还是手动“重新生成提示词”，结果都会统一落库。

### 6. 前端展示

本次只做“补足信息展示”，不重做页面结构。

#### AgentStudio

当前 `AgentStudio` 主要展示步骤状态和日志。本次不要求在 Agent 页面直接展开全部分镜详情，仅要求：

- `storyboard_gen` 的 `output` 文案增加完整产出摘要
- 让用户知道已生成分镜数量与视频提示词数量

#### StoryboardWorkspace

在已有分镜工作台中为每条分镜补充可见信息：

- 出镜角色
- 分镜场景
- 首帧提示词
- 镜头运动描述
- 视频生成提示词

展示优先级：

1. `sceneRef`
2. `videoPrompt`
3. 现有 `characters / startFramePrompt / motionScript`

当旧数据没有 `sceneRef` / `videoPrompt` 时：

- `sceneRef` 可尝试从旧 `characters` 做只读推断展示
- `videoPrompt` 为空时显示“未生成”

### 7. 兼容与迁移

#### 新数据

新跑的 Agent 和新生成的分镜应直接落：

- `characters` 仅角色
- `scene_ref`
- `video_prompt`

#### 旧数据

旧分镜可能存在：

- `characters` 同时含角色和场景
- `video_prompt` 为空

兼容策略：

- 不做全库一次性回填迁移，避免首次部署过重
- 旧数据在重新生成分镜或手动重新生成提示词时逐步转正
- 前端读取时允许空字段并做柔性展示

#### 手动编辑

若用户手动更新分镜内容后再次点击“生成提示词”，后端应重新生成并覆盖 `video_prompt`，保持结果与当前分镜内容一致。

但用户也可能只编辑分镜正文而**不**点重新生成，此时 `video_prompt` 会与正文脱节。本次采用以下策略：

- `update(...)` 在检测到以下任一字段被修改时，自动清空 `video_prompt`
  - `shortDescription`
  - `detailedDescription`
  - `characters`
  - `sceneRef`
  - `dialogues`
  - `cameraDirection`
  - `compositionGuide`
  - `durationSeconds`
  - `motionScript`
- 前端读到 `videoPrompt` 为空时按“未生成”展示，用户可手动重新生成
- 这样避免“陈旧提示词”被静默用于视频生成

## 失败处理

### 分镜生成成功，视频提示词补全失败

不回滚已生成的分镜记录。

处理方式：

- `storyboard_gen` 步骤视为失败
- 日志写清楚是“分镜已生成，但视频提示词补全失败”
- 已落库的分镜可继续在工作台查看
- `video_prompt` 为空的分镜允许后续单条重试

这是比强事务回滚更合适的行为，因为分镜本身仍然是有价值产物。

### 单条提示词生成失败

建议默认整步失败，但日志里包含：

- 失败分镜 ID / 场景号
- 已成功条数 / 总条数

整步失败后，已生成的分镜与已写入的 `video_prompt` 都保留。用户可通过：

- 重新执行 Agent（按上文幂等补全策略，仅对未补全的分镜重补提示词）
- 或在工作台逐条点击“重新生成提示词”

二者任一方式恢复，无需手动清理脏数据。

后续如有需要，可扩展为“容忍单条失败并记录失败列表”，但这不作为本次必做范围。

### 余额不足

并发或串行补全过程中，若 `billingService.checkBalance` 抛出余额不足：

- 已成功条数保留
- 立即中断当前集剩余任务及后续集
- 整步标记失败，日志注明“余额不足，已生成 X 条分镜，已补全 Y 条提示词”
- 用户充值后按上文幂等补全策略重跑即可继续

## 测试策略

### 后端

- `StoryboardPipelineServiceImpl`
  - 新 schema 正常解析 `scene_ref`
  - 旧格式 `characters` 能拆分出角色与场景
  - `generatePrompt` 回写 `video_prompt`
  - `generatePrompt` user message 在 `sceneRef` 存在时正确包含场景行；为空时回退读旧 `characters`
  - 编辑分镜正文后 `video_prompt` 被清空
- `AgentRunServiceImpl`
  - `storyboard_gen` 完成后，分镜已持久化 `video_prompt`
  - 任一提示词生成失败时，步骤状态正确标记为失败
  - 重跑场景：已存在分镜的集只补缺失的 `video_prompt`，不改变基础分镜生成的旧行为
  - 同集并发补全 N 条任务时进度日志正常
  - 余额不足时立即中断剩余任务并标记失败
- DTO/Mapper
  - 新字段能正确出入参

### 数据库迁移

- Flyway 迁移可重复执行
- 老库升级后，旧查询与插入不报错

### 前端

- 分镜列表能显示 `sceneRef`
- `videoPrompt` 有值时正确展示
- `videoPrompt` 为空时展示兜底态
- 旧数据不崩溃

## 实施边界

本次实现只覆盖以下内容：

- 数据库字段新增
- 后端分镜生成与提示词落库
- Agent `storyboard_gen` 内部补全逻辑
- 分镜工作台展示新增字段

以下内容明确不在本次范围：

- 新增 Agent 独立步骤卡片，例如 `storyboard_prompt_gen`
- 全量旧数据离线回填脚本
- 分镜编辑器的深度结构化改造
- 视频生成页面的大规模交互重构
- `end_frame_prompt` 字段的语义调整或前后端展示
- 多场景分镜的支持

## 预期结果

完成后，Agent 制作跑完后，用户进入分镜页应能直接看到：

- `001 / 002 / ...`
- 分镜描述
- 出镜角色
- 分镜场景
- 首帧提示词
- 镜头运动描述
- 视频生成提示词

同时，后续视频生成默认可以直接复用已持久化的 `video_prompt`，不再依赖“临时现生提示词”的操作链路。
