# Agent Storyboard Enrichment Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make Agent-generated storyboards persist and expose `sceneRef` and `videoPrompt`, while keeping the existing script-import and storyboard generation flow compatible.

**Architecture:** Add two columns to `storyboards`, normalize storyboard generation output so scene references are stored separately from character references, then extend the existing prompt-generation pipeline to persist `video_prompt`. Agent `storyboard_gen` keeps the same outward step shape, but internally runs “generate storyboards -> fill missing video prompts” and exposes richer progress/log output. Frontend changes are limited to type updates and storyboard workspace display.

**Tech Stack:** Spring Boot, MyBatis-Plus, Flyway, React, TypeScript, TanStack Query, Maven

---

## File Structure

**Files to create**

- `yaoai-comic-studio/yaoai-bootstrap/src/main/resources/db/migration/V17__storyboard_scene_ref_video_prompt.sql`
- `yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImplTest.java`
- `yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/agent/service/impl/AgentRunServiceImplTest.java`

**Files to modify**

- `yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/Storyboard.java`
- `yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/StoryboardMapper.java`
- `yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/StoryboardDTO.java`
- `yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/StoryboardController.java`
- `yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/StoryboardPipelineService.java`
- `yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImpl.java`
- `yaoai-comic-studio/yaoai-agent/src/main/java/com/yaoai/agent/service/impl/AgentRunServiceImpl.java`
- `doc/html/src/lib/api/ai.ts`
- `doc/html/src/hooks/useAi.ts`
- `doc/html/src/app/pages/StoryboardWorkspace.tsx`

**Primary boundaries**

- Database and DTO changes stay centered on the `storyboards` aggregate.
- Prompt persistence stays inside `StoryboardPipelineServiceImpl`; controller and Agent only orchestrate.
- Frontend uses the existing storyboard list API and does not require a new route.

---

### Task 1: Add storyboard storage contract

**Files:**
- Create: `yaoai-comic-studio/yaoai-bootstrap/src/main/resources/db/migration/V17__storyboard_scene_ref_video_prompt.sql`
- Modify: `yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/Storyboard.java`
- Modify: `yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/StoryboardMapper.java`
- Modify: `yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/StoryboardDTO.java`
- Test: `yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImplTest.java`

- [ ] **Step 1: Write the migration and DTO contract test**

```java
@Test
void from_shouldExposeSceneRefAndVideoPrompt() {
    Storyboard sb = new Storyboard();
    sb.setId(1L);
    sb.setSceneRef("@咖啡馆");
    sb.setVideoPrompt("0-3s：中景推进，咖啡馆内安静对视");

    StoryboardDTO dto = StoryboardDTO.from(sb);

    assertEquals("@咖啡馆", dto.getSceneRef());
    assertEquals("0-3s：中景推进，咖啡馆内安静对视", dto.getVideoPrompt());
}
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```bash
mvn -q -pl yaoai-comic-studio/yaoai-bootstrap -am -Dtest=StoryboardPipelineServiceImplTest#from_shouldExposeSceneRefAndVideoPrompt test
```

Expected: FAIL because `sceneRef` and `videoPrompt` fields do not exist yet.

- [ ] **Step 3: Add Flyway migration**

```sql
ALTER TABLE storyboards
    ADD COLUMN scene_ref VARCHAR(128) DEFAULT NULL
        COMMENT '分镜主场景引用，格式 @场景名',
    ADD COLUMN video_prompt LONGTEXT DEFAULT NULL
        COMMENT '分镜视频生成提示词';
```

- [ ] **Step 4: Extend entity and DTO**

```java
private String sceneRef;
private String videoPrompt;
```

```java
dto.setSceneRef(s.getSceneRef());
dto.setVideoPrompt(s.getVideoPrompt());
```

- [ ] **Step 5: Add mapper helpers for follow-up tasks**

```java
@Select("SELECT * FROM storyboards WHERE episode_id=#{episodeId} AND tenant_id=#{tenantId} AND (video_prompt IS NULL OR video_prompt='') ORDER BY sequence_num ASC")
List<Storyboard> findMissingVideoPromptByEpisode(Long episodeId, Long tenantId);
```

- [ ] **Step 6: Run test to verify it passes**

Run:

```bash
mvn -q -pl yaoai-comic-studio/yaoai-bootstrap -am -Dtest=StoryboardPipelineServiceImplTest#from_shouldExposeSceneRefAndVideoPrompt test
```

Expected: PASS

- [ ] **Step 7: Commit**

```bash
git add yaoai-comic-studio/yaoai-bootstrap/src/main/resources/db/migration/V17__storyboard_scene_ref_video_prompt.sql yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/Storyboard.java yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/StoryboardMapper.java yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/StoryboardDTO.java yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImplTest.java
git commit -m "feat(storyboard): add scene ref and video prompt storage"
```

---

### Task 2: Normalize storyboard generation and persist prompt output

**Files:**
- Modify: `yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/StoryboardPipelineService.java`
- Modify: `yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImpl.java`
- Modify: `yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/StoryboardController.java`
- Test: `yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImplTest.java`

- [ ] **Step 1: Write failing tests for scene extraction, prompt persistence, and video prompt invalidation**

```java
@Test
void generateStoryboards_shouldSplitSceneRefFromCharacters() {
    String raw = """
        [{
          "sequence_num": 1,
          "scene_number": "001",
          "short_description": "两人在咖啡馆对视",
          "detailed_description": "晚间咖啡馆内，人物对坐，气氛克制",
          "characters": "@林夏,@程野,@咖啡馆",
          "start_frame_prompt": "cinematic cafe interior, two people facing each other",
          "motion_script": "缓慢推进"
        }]
        """;

    when(llmService.chat(any())).thenReturn(raw);

    List<Storyboard> storyboards = service.generateStoryboards(11L, 22L, 33L);

    assertEquals("@咖啡馆", storyboards.get(0).getSceneRef());
    assertEquals("@林夏,@程野", storyboards.get(0).getCharacters());
}

@Test
void generatePrompt_shouldPersistVideoPrompt() {
    Storyboard sb = new Storyboard();
    sb.setId(7L);
    sb.setTenantId(33L);
    sb.setProjectId(22L);
    sb.setSceneRef("@咖啡馆");
    when(storyboardMapper.selectById(7L)).thenReturn(sb);
    when(llmService.chat(any())).thenReturn("0-3s：中景推进，咖啡馆内安静对视");

    String prompt = service.generatePrompt(7L, 33L);

    assertEquals("0-3s：中景推进，咖啡馆内安静对视", prompt);
    verify(storyboardMapper).updateById(argThat(updated ->
        "0-3s：中景推进，咖啡馆内安静对视".equals(updated.getVideoPrompt())
    ));
}

@Test
void update_shouldClearVideoPromptWhenStoryboardBodyChanges() {
    Storyboard existing = new Storyboard();
    existing.setId(9L);
    existing.setTenantId(33L);
    existing.setShortDescription("旧描述");
    existing.setVideoPrompt("旧视频提示词");
    when(storyboardMapper.selectById(9L)).thenReturn(existing, existing);

    Storyboard patch = new Storyboard();
    patch.setShortDescription("新描述");

    service.update(9L, 33L, patch);

    verify(storyboardMapper).updateById(argThat(updated -> updated.getVideoPrompt() == null));
}
```

- [ ] **Step 2: Run the focused backend tests**

Run:

```bash
mvn -q -pl yaoai-comic-studio/yaoai-bootstrap -am -Dtest=StoryboardPipelineServiceImplTest test
```

Expected: FAIL because scene splitting, prompt persistence, and invalidation are not implemented.

- [ ] **Step 3: Extend the service API**

```java
String generatePrompt(Long storyboardId, Long tenantId);
List<Storyboard> populateMissingVideoPromptsByEpisode(Long episodeId, Long tenantId);
```

- [ ] **Step 4: Normalize model output and add helper methods**

```java
private record StoryboardRefs(String characters, String sceneRef) {}

private StoryboardRefs normalizeStoryboardRefs(String rawRefs, Long projectId, Long tenantId) {
    List<Scene> scenes = sceneMapper.findByProject(projectId, tenantId);
    Set<String> sceneRefs = scenes.stream()
            .map(Scene::getName)
            .filter(Objects::nonNull)
            .map(name -> "@" + name)
            .collect(Collectors.toSet());

    List<String> tokens = Arrays.stream(nullSafe(rawRefs).split(","))
            .map(String::trim)
            .filter(token -> !token.isBlank())
            .toList();

    String sceneRef = tokens.stream().filter(sceneRefs::contains).findFirst().orElse(null);
    String characters = tokens.stream().filter(token -> !sceneRefs.contains(token)).collect(Collectors.joining(","));
    return new StoryboardRefs(characters, sceneRef);
}
```

- [ ] **Step 5: Persist `video_prompt` inside `generatePrompt` and add episode batch fill**

```java
Storyboard update = new Storyboard();
update.setId(sb.getId());
update.setVideoPrompt(prompt);
storyboardMapper.updateById(update);
return prompt;
```

```java
@Override
public List<Storyboard> populateMissingVideoPromptsByEpisode(Long episodeId, Long tenantId) {
    List<Storyboard> pending = storyboardMapper.findMissingVideoPromptByEpisode(episodeId, tenantId);
    for (Storyboard storyboard : pending) {
        generatePrompt(storyboard.getId(), tenantId);
    }
    return storyboardMapper.findByEpisode(episodeId, tenantId);
}
```

- [ ] **Step 6: Clear `video_prompt` whenever storyboard body fields change**

```java
if (bodyFieldsChanged(existing, patch)) {
    patch.setVideoPrompt(null);
}
```

- [ ] **Step 7: Keep controller contract unchanged but ensure prompt endpoint returns persisted data**

```java
String prompt = storyboardService.generatePrompt(storyboardId, tenantId);
return ApiResponse.success(Map.of("prompt", prompt));
```

- [ ] **Step 8: Re-run backend tests**

Run:

```bash
mvn -q -pl yaoai-comic-studio/yaoai-bootstrap -am -Dtest=StoryboardPipelineServiceImplTest test
```

Expected: PASS

- [ ] **Step 9: Commit**

```bash
git add yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/StoryboardPipelineService.java yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImpl.java yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/StoryboardController.java yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImplTest.java
git commit -m "feat(storyboard): persist scene refs and video prompts"
```

---

### Task 3: Teach Agent `storyboard_gen` to backfill prompts

**Files:**
- Modify: `yaoai-comic-studio/yaoai-agent/src/main/java/com/yaoai/agent/service/impl/AgentRunServiceImpl.java`
- Modify: `yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/StoryboardPipelineService.java`
- Test: `yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/agent/service/impl/AgentRunServiceImplTest.java`

- [ ] **Step 1: Write failing Agent tests**

```java
@Test
void storyboardGen_shouldPopulateMissingVideoPromptsAfterGeneratingStoryboards() {
    Episode ep = new Episode();
    ep.setId(101L);
    ep.setEpisodeNumber(1);
    when(outlinePipeline.generateEpisodes(anyLong(), anyLong(), anyLong())).thenReturn(List.of(ep));
    when(storyboardPipeline.generateStoryboards(101L, 22L, 33L)).thenReturn(List.of(new Storyboard(), new Storyboard()));
    when(storyboardPipeline.populateMissingVideoPromptsByEpisode(101L, 33L)).thenReturn(List.of(new Storyboard(), new Storyboard()));

    service.startRun(22L, 33L, "生成完整短剧");

    verify(storyboardPipeline).populateMissingVideoPromptsByEpisode(101L, 33L);
}
```

- [ ] **Step 2: Run Agent test to verify it fails**

Run:

```bash
mvn -q -pl yaoai-comic-studio/yaoai-bootstrap -am -Dtest=AgentRunServiceImplTest test
```

Expected: FAIL because Agent only generates storyboards today.

- [ ] **Step 3: Update the `storyboard_gen` execution block**

```java
for (Episode ep : episodes) {
    List<Storyboard> boards = storyboardPipeline.generateStoryboards(ep.getId(), projectId, tenantId);
    total += boards.size();
    sseManager.emitLog(runId, "info", "第" + ep.getEpisodeNumber() + "集分镜：" + boards.size() + " 个镜头");

    List<Storyboard> enriched = storyboardPipeline.populateMissingVideoPromptsByEpisode(ep.getId(), tenantId);
    long promptCount = enriched.stream().filter(sb -> sb.getVideoPrompt() != null && !sb.getVideoPrompt().isBlank()).count();
    sseManager.emitLog(runId, "info", "第" + ep.getEpisodeNumber() + "集视频提示词：" + promptCount + " 条已生成");
}
```

- [ ] **Step 4: Add bounded prompt-fill execution**

```java
private static final int STORYBOARD_PROMPT_CONCURRENCY = 3;
```

Use a fixed-size executor or `CompletableFuture` batch inside `populateMissingVideoPromptsByEpisode`, but keep cross-episode execution serial.

- [ ] **Step 5: Mark the summary output clearly**

```java
return "共生成 " + total + " 个分镜，并补全视频提示词";
```

- [ ] **Step 6: Re-run Agent and backend tests**

Run:

```bash
mvn -q -pl yaoai-comic-studio/yaoai-bootstrap -am -Dtest=StoryboardPipelineServiceImplTest,AgentRunServiceImplTest test
```

Expected: PASS

- [ ] **Step 7: Commit**

```bash
git add yaoai-comic-studio/yaoai-agent/src/main/java/com/yaoai/agent/service/impl/AgentRunServiceImpl.java yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/StoryboardPipelineService.java yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/agent/service/impl/AgentRunServiceImplTest.java
git commit -m "feat(agent): enrich storyboard generation with prompt backfill"
```

---

### Task 4: Expose and display the richer storyboard fields

**Files:**
- Modify: `doc/html/src/lib/api/ai.ts`
- Modify: `doc/html/src/hooks/useAi.ts`
- Modify: `doc/html/src/app/pages/StoryboardWorkspace.tsx`

- [ ] **Step 1: Add the frontend type fields**

```ts
export interface Storyboard {
  id: string;
  episodeId: string;
  sequenceNum: number;
  sceneNumber: string;
  shortDescription: string;
  detailedDescription: string;
  characters: string;
  sceneRef?: string | null;
  videoPrompt?: string | null;
  dialogues: string;
  cameraDirection: string;
  compositionGuide: string;
  durationSeconds: number;
  startFramePrompt: string;
  motionScript: string;
  notes: string;
  status: string;
  createdAt: string;
  updatedAt: string;
}
```

- [ ] **Step 2: Keep the existing React Query hooks and only adjust the consumed shape**

```ts
mutationFn: ({ id, patch }: { id: string; patch: Record<string, unknown> }) =>
  aiApi.updateStoryboard(projectId, id, patch),
```

No new route or new hook is needed.

- [ ] **Step 3: Add display blocks to the storyboard workspace**

```tsx
<div className="space-y-3">
  <Field label="出镜角色" value={expanded.characters || "未标注"} />
  <Field label="分镜场景" value={expanded.sceneRef || "未标注"} />
  <Field label="首帧提示词" value={expanded.startFramePrompt || "未生成"} />
  <Field label="镜头运动" value={expanded.motionScript || "未生成"} />
  <Field label="视频提示词" value={expanded.videoPrompt || "未生成"} multiline />
</div>
```

- [ ] **Step 4: Show old data safely**

```tsx
const sceneRefDisplay = expanded.sceneRef?.trim() || inferSceneRefFromCharacters(expanded.characters) || "未标注";
```

- [ ] **Step 5: Build the frontend**

Run:

```bash
cd doc/html
npm run build
```

Expected: PASS

- [ ] **Step 6: Commit**

```bash
git add doc/html/src/lib/api/ai.ts doc/html/src/hooks/useAi.ts doc/html/src/app/pages/StoryboardWorkspace.tsx
git commit -m "feat(frontend): show enriched storyboard fields"
```

---

### Task 5: Full verification and cleanup

**Files:**
- Modify: `docs/superpowers/specs/2026-04-28-agent-storyboard-enrichment-design.md` only if implementation deviates

- [ ] **Step 1: Run backend compile**

Run:

```bash
mvn -q -pl yaoai-comic-studio/yaoai-bootstrap -am -DskipTests compile
```

Expected: PASS

- [ ] **Step 2: Run focused tests**

Run:

```bash
mvn -q -pl yaoai-comic-studio/yaoai-bootstrap -am -Dtest=StoryboardPipelineServiceImplTest,AgentRunServiceImplTest test
```

Expected: PASS

- [ ] **Step 3: Run frontend build**

Run:

```bash
cd doc/html
npm run build
```

Expected: PASS

- [ ] **Step 4: Smoke-check the critical user flows**

```text
1. 输入或上传剧本生成大纲、分集、角色与场景，确认这些前置流程不变。
2. 运行 Agent 制作，确认 storyboard_gen 日志包含“分镜数量”和“提示词数量”。
3. 打开分镜页，确认同一条分镜能看到角色、场景、首帧提示词、镜头运动、视频提示词。
4. 手动修改分镜正文后刷新数据，确认 videoPrompt 变为空。
5. 手动调用“生成提示词”后，确认 videoPrompt 被重新写回。
```

- [ ] **Step 5: Final commit**

```bash
git add .
git commit -m "feat(agent): enrich storyboard output for scene and video prompt"
```

---

## Self-Review

- Spec coverage:
  - Database fields: covered in Task 1
  - Scene extraction and prompt persistence: covered in Task 2
  - Agent enrichment and prompt backfill: covered in Task 3
  - Frontend display: covered in Task 4
  - Verification and user-facing regression checks: covered in Task 5
- Placeholder scan:
  - No `TODO` / `TBD`
  - Every task has explicit files, commands, and code snippets
- Type consistency:
  - Uses `sceneRef` and `videoPrompt` consistently in DTO and frontend
  - Keeps database names as `scene_ref` and `video_prompt`

