Просмотр исходного кода

docs(speaking): add design spec for video scene mode and word-level pronunciation

Spec covers:
- "影片场景" toggle in TopicDiscussionConfig
- 2x4 video grid in TopicDiscussionPreview with select/play
- Single-round image-response dialogue (Doubao seedream image gen via OneHub)
- Word-level pronunciation underlines (correct/wrong/missed) in DetailedReport
- OverallReport LLM evaluation now consumes word_analysis
- Azure raw evaluation persisted + INFO logged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmylee 1 месяц назад
Родитель
Сommit
9d996e146b

+ 482 - 0
docs/superpowers/specs/2026-05-15-video-scene-and-word-pronunciation-design.md

@@ -0,0 +1,482 @@
+# 影片场景模式 + 单词级发音可视化 — 设计文档
+
+**日期**: 2026-05-15
+**作者**: jimmylee
+**状态**: 待实施
+
+---
+
+## 1. 背景与目标
+
+英语口语工具(`TopicDiscussion`)目前只支持纯文本多轮对话。本次新增两块能力:
+
+1. **影片场景模式(Video Scene)** — 老师可在配置面板开启影片场景开关。开启后,学生侧首页横向展示 8 个内置影片卡片,学生选一个并点 "开始对话",AI 会基于该影片描述提一个问题,学生回答一次后 AI 返回**一张生成图片**作为回应,然后练习结束。
+2. **单词级发音可视化(Word-level Pronunciation Display)** — 不论是否开启影片场景,详细报告中每个学生回答下方,在 "进阶表达" 之后,新增一个组件展示该句各个单词的发音着色(正确/错误/漏读)。同时让 OverallReport 的 LLM 评估也使用单词级数据生成更具体的反馈。
+
+---
+
+## 2. 技术发现(基于代码探索)
+
+- **Azure 已经返回单词级数据**:`app/providers/azure_speech.py` 已使用 `granularity=Word, enable_miscue=True`,并对每个 word 返回 `error_type`(`None`/`Mispronunciation`/`Omission`/`Insertion`)与 `accuracy_score`,完整存于 `pronunciation_evaluation.word_analysis` JSON 列。**正确/错误/漏读完全可做到**,无需调研。
+- **后端 `/report` 当前未透传 `wordAnalysis`**:类型上前端 `BackendEvaluation.wordAnalysis` 已经存在但 `adaptReport` 丢弃,后端 serializer 也需调整。
+- **OneHub 兼容 OpenAI `images.generate`**:参考 `application_center_api/utils/utils.py:aget_dalle_image`,模型 `doubao-seedream-3-0-t2i-250415`,响应 `b64_json`,解码后上传 S3 拿公网 URL。
+- **`dialogue_message` 表无 `image_url` 列**:需 ALTER。
+- **现有 `/speak-stream` (WebSocket) 已具备转录、PronunciationEvaluation、S3 上传、turn-id 幂等**:全部复用,只在 LLM 输出环节按 session mode 分支。
+
+---
+
+## 3. 范围
+
+### 3.1 配置面板(前端 — TopicDiscussionConfig.vue + store)
+
+在 "展示学习报告" toggle **下方**新增 "影片场景" toggle。
+
+**Store 改动**:`TopicDiscussionConfig` 新增顶层字段(与 `practice`、`evaluation` 平级):
+
+```ts
+videoScene: {
+  enabled: boolean    // 默认 false
+}
+```
+
+理由:它改变运行时语义(轮次=1、AI 返图),不属于"评估配置"。
+
+`DEFAULT_CONFIG` 与 `EMPTY_CONFIG` 都加这一字段(默认 false)。新增 store action `toggleVideoScene()`。
+
+### 3.2 影片资源(前端数据)
+
+`src/views/Editor/EnglishSpeaking/data/topicDiscussionVideos.json`:
+
+```json
+[
+  {
+    "id": "v1",
+    "title": "<placeholder title>",
+    "videoUrl": "<placeholder mp4 url>",
+    "description": "<placeholder English description for system prompt>"
+  },
+  ... 共 8 条占位
+]
+```
+
+`description` 字段是 AI system prompt 的关键输入。资源由用户后续提供,先用占位顶住开发。
+
+### 3.3 预览层 — 影片选择(TopicDiscussionPreview.vue)
+
+仅当 `speakingStore.config.videoScene.enabled === true` 时,在 `ready` 阶段标题下方插入一个 **2×4 网格** 影片卡片栏:
+
+```
+┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
+│标题1│ │标题2│ │标题3│ │标题4│
+│○ ▷│ │○ ▷│ │○ ▷│ │○ ▷│
+└──────┘ └──────┘ └──────┘ └──────┘
+┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
+│标题5│ │标题6│ │标题7│ │标题8│
+│○ ▷│ │○ ▷│ │○ ▷│ │○ ▷│
+└──────┘ └──────┘ └──────┘ └──────┘
+```
+
+CSS:`grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, auto); gap: 12px`。无横向滚动。
+
+每卡:
+- 标题 div(顶部)
+- 选中圆圈(单选 radio 风格,只能选一个)
+- 播放小三角(▷)
+
+交互:
+- 点圆圈 → 选中本卡(自动取消其他卡)
+- 点 ▷ → 弹出 `VideoPlayerModal`(`<video controls>` + 右上角 ✕,占预览区域 ~80% 宽,maxWidth 720px,16:9)
+- 关闭按 ✕ 或点遮罩
+
+"开始对话" 按钮 disabled 条件追加:`videoScene.enabled && !selectedVideo`,提示文案:`请先选择一个影片`。
+
+新增组件:
+- `preview/VideoSceneGrid.vue`(影片卡片网格)
+- `preview/VideoPlayerModal.vue`(播放弹窗)
+
+### 3.4 创建 session 时传 selectedVideo
+
+`TopicDiscussionPreview.vue:startDialogue()` → `api.createSession()` 的 body 增加:
+
+```ts
+selectedVideo?: {
+  id: string
+  title: string
+  videoUrl: string
+  description: string
+}
+```
+
+后端入库到 `DialogueSession.role_config.selected_video`(JSON 列已存在,无需 schema 改动)。同时 session 的 `mode` 字段也存到 `role_config.mode`,值为 `'normal' | 'video_scene'`。
+
+`mode === 'video_scene'` 时:
+- `total_rounds` 强制 `1`(不论 config 里 practice.rounds/duration 是什么)
+- system_prompt 模板尾部追加 video 场景上下文段:
+
+  ```
+  ─ Video Scene Context ─
+  The student has chosen a video scene to discuss:
+  Title: {selected_video.title}
+  Description: {selected_video.description}
+
+  Your single question must reference this scene specifically and invite
+  the student to describe / react to it.
+  ```
+
+### 3.5 单轮图像回应对话(后端 — dialogue_service + 新 provider)
+
+#### 3.5.1 新 provider — `app/providers/image_gen.py`
+
+```python
+class ImageGenProvider(Protocol):
+    async def generate(self, prompt: str) -> str: ...   # returns image URL
+
+
+class DoubaoImageGen(ImageGenProvider):
+    def __init__(self, storage: AudioStorage):
+        self.client = AsyncOpenAI(
+            base_url=settings.ONEHUB_BASE_URL,
+            api_key=settings.ONEHUB_API_KEY,
+        )
+        self.storage = storage
+
+    async def generate(self, prompt: str) -> str:
+        response = await self.client.images.generate(
+            model="doubao-seedream-3-0-t2i-250415",
+            prompt=prompt,
+            n=1,
+            quality="hd",
+            size="1024x1024",
+            response_format="b64_json",
+        )
+        img_bytes = base64.b64decode(response.data[0].b64_json)
+        timestamp = int(time.time())
+        url = await self.storage.upload(img_bytes, f"images/{timestamp}.png")
+        return url
+```
+
+S3 复用 `S3AudioStorage`(`upload(bytes, filename) -> url`),最终 key 为 `speaking/images/{timestamp}.png`。失败时(任何 exception)→ 返回占位 URL `settings.IMAGE_GEN_PLACEHOLDER_URL` + `logger.warning`,不阻塞用户流程。
+
+`get_dialogue_service()` 注入 `image_gen=DoubaoImageGen(storage=S3AudioStorage())`。
+
+#### 3.5.2 image gen prompt 模板
+
+`app/service/speaking/prompts.py`(或现有 prompts 常量文件)新增:
+
+```python
+VIDEO_SCENE_IMAGE_PROMPT_TEMPLATE = """\
+<placeholder — 后续由 jimmylee 调整>
+Generate a vivid photorealistic image based on:
+- Video scene title: {video_title}
+- Video scene description: {video_description}
+- AI question to student: {ai_question}
+- Student response: {student_transcript}
+
+The image should depict what the student is describing, in the context of the video scene.
+"""
+```
+
+后续真实改文案就改这一处。
+
+#### 3.5.3 `dialogue_message.image_url` 新列
+
+```sql
+ALTER TABLE dialogue_message ADD COLUMN image_url VARCHAR(512) NULL;
+```
+
+`models/dialogue.py` 同步加 `image_url: Mapped[Optional[str]]` 字段。
+
+`init.sql` 同步更新(项目无 alembic 迁移,与现有 schema 演进方式一致)。
+
+AI 消息:`content` 仍存 image gen prompt 文本(便于审计/重生成),`image_url` 存生成的 URL。
+
+#### 3.5.4 `/speak-stream` 分支(WebSocket)
+
+在收到 transcript、写完 student msg + evaluation 占位后:
+
+```python
+mode = (session.role_config or {}).get("mode", "normal")
+
+if mode == "video_scene":
+    # 取上一条 AI 消息作为 question
+    ai_question = next((m.content for m in reversed(history) if m.role == "ai"), "")
+    selected = (session.role_config or {}).get("selected_video", {})
+    image_prompt = VIDEO_SCENE_IMAGE_PROMPT_TEMPLATE.format(
+        video_title=selected.get("title", ""),
+        video_description=selected.get("description", ""),
+        ai_question=ai_question,
+        student_transcript=transcript,
+    )
+    image_url = await image_gen.generate(image_prompt)
+    await websocket.send_json({"type": "image", "url": image_url, "round": current_round})
+    full_response = image_prompt   # 持久化到 content 便于审计
+    ai_image_url = image_url
+else:
+    # 现有 LLM 流式分支
+    full_response = ""
+    async for token in llm.chat_stream(...):
+        full_response += token
+        await websocket.send_json({"type": "token", "content": token})
+    ai_image_url = None
+```
+
+之后照常保存 AI 消息(带 `image_url=ai_image_url`)、推进轮次。`total_rounds=1` 的情况下,`is_complete = True`,session 自动 mark completed。后台仍跑 pronunciation_evaluation。
+
+#### 3.5.5 前端 SSE/WS 事件类型扩展
+
+`types/englishSpeaking.ts:SSEEvent` union 加入:
+
+```ts
+| { type: 'image'; url: string; round: number }
+```
+
+`composables/useDialogueEngine.ts` 收到 `image` 事件:把当前 AI message 的 `imageUrl` 字段填好,并视为 AI turn 完成(等价于 `done`,但 done 事件后端仍会发,engine 需幂等处理)。
+
+`DialogueChatView.vue` 渲染 AI 消息时:
+
+```vue
+<template v-if="message.imageUrl">
+  <img :src="message.imageUrl" class="ai-image-bubble" />
+</template>
+<template v-else>
+  <!-- 现有 voice-bar + text bubble -->
+</template>
+```
+
+`Message` 类型加可选 `imageUrl?: string`。
+
+### 3.6 详细报告 — 单词级发音着色组件
+
+#### 3.6.1 数据透传
+
+**后端** `/report` round response 增加 `wordAnalysis` 字段:
+
+```python
+# dialogue_service.py:get_report() 中构建 round entry
+entry["evaluation"]["wordAnalysis"] = ev.word_analysis  # 直接透传 JSON
+entry["imageUrl"] = msg.image_url                        # AI 消息新加
+```
+
+**前端** `services/llmService.ts`:
+- `BackendEvaluation.wordAnalysis` 类型从 `unknown` 改为 `WordAnalysisItem[]`(新类型)。
+- `BackendRound` 新增 `imageUrl?: string | null`。
+- `adaptReport` 把 `wordAnalysis` 透传到 `SentenceEvaluation.wordAnalysis`,`imageUrl` 透传到 `SentenceEvaluation.imageUrl`。
+
+**类型** `types/englishSpeaking.ts`:
+
+```ts
+export interface WordAnalysisItem {
+  word: string
+  accuracyScore: number
+  errorType: 'None' | 'Mispronunciation' | 'Omission' | 'Insertion' | 'UnexpectedBreak' | 'MissingBreak'
+}
+
+export interface SentenceEvaluation {
+  // ...existing
+  wordAnalysis?: WordAnalysisItem[]
+  imageUrl?: string
+}
+```
+
+#### 3.6.2 新组件 `preview/WordPronunciationDisplay.vue`
+
+**位置**:`DetailedReport.vue` 的 `feedback-section` 内,**直接放在 "进阶表达" feedback-block 之后**(同一展开面板内,与已有 feedback-block 共享布局)。
+
+**展示规则**:
+- 以 Azure `word_analysis` 顺序渲染(包含 Omission 占位词,否则漏读不可见)。
+- 每个 word 一个 `<span>`,文字色不变(`#1f2937`),只设 `border-bottom: 2px solid <color>`(下划线)。
+
+**颜色映射**:
+
+| Azure error_type | 含义 | 下划线色 |
+|---|---|---|
+| `None` | 正确 | `#16a34a` 绿 |
+| `Mispronunciation` | 错误 | `#ef4444` 红 |
+| `Omission` | 漏读 | `#9ca3af` 灰 |
+| `Insertion` | 多读(归"错误") | `#ef4444` 红 |
+| `UnexpectedBreak` / `MissingBreak` | 韵律(忽略) | 无下划线 |
+
+布局:`display: inline; line-height: 2.0`(为下划线留空间),word 之间空格用普通 ` `。新增 block label `🎤 发音详情`(沿用现有 `.feedback-block-label` 风格)。
+
+#### 3.6.3 影片场景模式下的报告
+
+- AI 消息 `imageUrl` 存在 → DetailedReport 中 AI 卡片渲染 `<img>` 而非纯文本(替换 `card-text` 块)。
+- 学生消息(只有 1 条)依然走完整 DetailedReport 流程,包括新发音组件。
+
+### 3.7 OverallReport — LLM 评估纳入词级数据(无 UI 改动)
+
+**改动 1** — `dialogue_service.py:_build_overall_history()` 在 student 项里增加 `wordAnalysis`:
+
+```python
+if msg.role == "student" and msg.evaluation:
+    ev = msg.evaluation
+    item["pronunciation"] = {
+        "accuracy": ev.accuracy_score,
+        "fluency": ev.fluency_score,
+        "completeness": ev.completeness_score,
+        "prosody": ev.prosody_score,
+    }
+    item["wordAnalysis"] = ev.word_analysis      # ← 新增
+    if ev.content_feedback:
+        item["contentFeedback"] = ev.content_feedback
+```
+
+**改动 2** — `overall_report_evaluator.py:SYSTEM_PROMPT` 修订:
+- 明确把 "**word-level pronunciation analysis**(per-word `error_type` 与 `accuracy_score`)" 列为 LLM 第 2 类输入(原文含糊地说"Speech-API的每条语音评分")。
+- 要求 LLM 在 highlights / suggestions 中**点名具体的发音问题词**(例:"`although` 多次发音准确度偏低,可重点练 /ɔː/" / "`banana` 出现漏读")。
+- 增加约束:不要把发音满分的词列进 suggestions。
+
+**`OverallReport.vue` 不动**。
+
+### 3.8 Azure 评分日志与持久化
+
+**改动 1**(持久化全量):`pronunciation_evaluation.word_analysis` JSON 列直接存 Azure 完整返回 dict(本来已经是 JSON 列),便于后续 DB 查询/分析。当下 `azure_speech.py` 已经返回所有字段,只需 `dialogue_service._evaluate_pronunciation` 把整个 dict 写入。
+
+**改动 2**(实时日志):每次 `_evaluate_pronunciation` 完成后,以 `INFO` 级别打印 Azure 完整返回:
+
+```python
+logger.info(
+    "[pronunciation:azure-result] session=%s round=%s msg_id=%s evaluation_id=%s\n"
+    "  accuracy=%s fluency=%s completeness=%s prosody=%s\n"
+    "  word_analysis=%s",
+    session_uuid, round_no, message_id, evaluation_id,
+    result.get("accuracy_score"), result.get("fluency_score"),
+    result.get("completeness_score"), result.get("prosody_score"),
+    json.dumps(result.get("word_analysis", []), ensure_ascii=False, indent=2),
+)
+```
+
+INFO 级别确保上线后不靠 DEBUG 也能 grep。
+
+---
+
+## 4. 数据流总览
+
+### 4.1 影片场景模式开启 → 学生练习
+
+```
+[老师] Config 开 videoScene.enabled
+       └─→ updateSpeakingConfig(POST → 后端 speaking_config 表)
+[学生] 打开练习页
+       └─→ TopicDiscussionPreview 拉取 config
+       └─→ 渲染 VideoSceneGrid (8 张卡)
+       └─→ 学生选 v3,点 [开始对话]
+       └─→ createSession(body 含 selectedVideo + mode='video_scene')
+       └─→ 后端建 session: total_rounds=1, role_config={mode, selected_video}
+            system_prompt 含 Video Scene Context
+       └─→ generateGreeting → AI 提一个针对 v3 场景的问题
+       └─→ 学生录音 → /speak-stream
+            ├─ 转录 → "send transcript event"
+            ├─ DoubaoImageGen.generate(prompt) → image_url
+            ├─ "send image event"
+            ├─ 保存 AI msg (image_url=...)
+            ├─ session.status = completed
+            └─ 后台 pronunciation_evaluation
+[学生] DialogueChatView 渲染 AI 消息为 <img>
+       └─→ 自动跳到 report 阶段
+       └─→ getReport poll 直到 ready → OverallReport + DetailedReport
+            DetailedReport 展示 1 条 AI(图片) + 1 条 student(带 WordPronunciationDisplay)
+```
+
+### 4.2 普通模式 + 词级展示
+
+```
+[学生] 完整多轮对话(现有逻辑) → completed
+[报告] /report 返回 wordAnalysis 数组
+       └─→ DetailedReport 每条学生卡片展开后,在"进阶表达"下方
+            渲染 WordPronunciationDisplay(各 word 带颜色下划线)
+       └─→ OverallReport 的 aiComment / highlights / suggestions
+            由 LLM 基于 word_analysis 生成更具体的反馈
+```
+
+---
+
+## 5. 关键决策列表
+
+| # | 决策 | 备选/理由 |
+|---|---|---|
+| 1 | `videoScene.enabled` 在 config 顶层(平级 practice/evaluation) | 改变运行时语义,不属评估 |
+| 2 | `selectedVideo` 在 session.role_config(每学生不同) | 不能存到老师配的全局 config |
+| 3 | 影片资源本地 `topicDiscussionVideos.json`,8 条含 description | 用户后续提供资源 |
+| 4 | 复用 `/speak-stream` + 新增 `image` 事件类型 | 不开新端点,复用幂等机制 |
+| 5 | `dialogue_message.image_url` 新列(migration) | 语义清晰,避免与 audio_url 混 |
+| 6 | Image gen = `doubao-seedream-3-0-t2i-250415`,沿用 `aget_dalle_image` 模式 | 与现网生产一致 |
+| 7 | 失败回退占位图 + log warning | 不阻塞用户流程 |
+| 8 | 词级展示按 Azure word_analysis 顺序(含 Omission 占位) | 漏读必须能展示 |
+| 9 | Insertion 归红色"错误" | 用户只列 3 类色 |
+| 10 | 影片场景下也跑发音评估 | 词级展示与是否影片场景无关 |
+| 11 | system_prompt 在 video_scene 模式追加 Video Scene Context 段 | 让 AI 提问围绕场景 |
+| 12 | OverallReport 生成时 LLM 输入加 word_analysis,SYSTEM_PROMPT 要求点名问题词 | 无 UI 改动 |
+| 13 | `pronunciation_evaluation.word_analysis` 存 Azure 完整返回 + INFO 日志 | 便于调试与离线分析 |
+| 14 | 预览层 2×4 网格 | 用户指定 |
+| 15 | 影片播放弹窗:80% 宽 / maxWidth 720px / 16:9 | 默认值,可调 |
+
+---
+
+## 6. Out of Scope
+
+- 老师在 config 阶段编辑 / 上传 / 替换影片资源(下个迭代)。
+- 视频缩略图自动提取(本期就用占位)。
+- 影片场景模式下多轮对话(明确锁死单轮)。
+- 影片场景模式下结果页的 OverallReport 文案是否要按图像评估调整(本期沿用现有评估逻辑,LLM 看到 conversation_history 即可)。
+- 真实 image gen prompt 文案设计(占位由 jimmylee 后续改)。
+- 真实 video scene system_prompt 段落措辞(本期给基础版,可迭代)。
+
+---
+
+## 7. 风险与缓解
+
+| 风险 | 缓解 |
+|---|---|
+| Doubao image gen 慢(>10s)拖延整个 turn | WS 超时设 30s,失败回退占位图 |
+| word_analysis JSON 体积大,影响 OverallReport LLM 上下文 | 当前 16k+ 上下文够用,如果触发 token 限制再做截断 |
+| `enable_miscue` 在不同 Azure region 行为差异 | 已经在生产路径,假设稳定 |
+| 影片资源 mp4 跨域 / CORS | 由资源提供方保证,弹窗内 `<video>` 标签直接加载 |
+| 同一 session 多次刷新页面图片重复请求生成 | turn-id 幂等机制已覆盖,缓存的 AI msg 直接回放 |
+
+---
+
+## 8. 文件清单(预期改动)
+
+### 前端(PPT 仓库)
+- `src/views/Editor/EnglishSpeaking/configs/TopicDiscussionConfig.vue` — 加 toggle
+- `src/store/speaking.ts` — 加 `videoScene` 字段 + action
+- `src/types/englishSpeaking.ts` — 加 `WordAnalysisItem`、`videoScene`、`SentenceEvaluation.wordAnalysis/imageUrl`、`SSEEvent` image 变体
+- `src/views/Editor/EnglishSpeaking/data/topicDiscussionVideos.json` — 新文件,8 条占位
+- `src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue` — 渲染 VideoSceneGrid + 传 selectedVideo
+- `src/views/Editor/EnglishSpeaking/preview/VideoSceneGrid.vue` — 新组件
+- `src/views/Editor/EnglishSpeaking/preview/VideoPlayerModal.vue` — 新组件
+- `src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue` — AI 消息分支渲染图片
+- `src/views/Editor/EnglishSpeaking/preview/DetailedReport.vue` — AI 图片渲染 + 插入 WordPronunciationDisplay
+- `src/views/Editor/EnglishSpeaking/preview/WordPronunciationDisplay.vue` — 新组件
+- `src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts` — 处理 image 事件
+- `src/views/Editor/EnglishSpeaking/services/llmService.ts` — 类型扩展、adaptReport 透传 wordAnalysis/imageUrl、createSession 传 selectedVideo
+
+### 后端(cococlass-english-speaking-api 仓库)
+- `app/models/dialogue.py` — `DialogueMessage.image_url` 新字段
+- `init.sql` — 同步加 image_url 列
+- `app/api/dialogue.py` — `CreateSessionRequest` 加 `selectedVideo` / `mode`,`/speak-stream` 加 image gen 分支与 image 事件
+- `app/service/speaking/dialogue_service.py` —
+  - `create_session_only` 接收 mode/selected_video,total_rounds=1 强制,system_prompt 加 Video Scene Context 段
+  - `_evaluate_pronunciation` 持久化 azure raw + INFO 日志
+  - `_build_overall_history` 加 wordAnalysis
+  - `get_report` 透传 wordAnalysis 与 imageUrl
+- `app/service/speaking/overall_report_evaluator.py` — SYSTEM_PROMPT 修订
+- `app/service/speaking/prompts.py`(或现有 prompts 常量位置)— `VIDEO_SCENE_IMAGE_PROMPT_TEMPLATE`、Video Scene Context 模板段
+- `app/providers/image_gen.py` — 新文件,`ImageGenProvider` + `DoubaoImageGen`
+- `app/providers/protocols.py` — 加 `ImageGenProvider`
+- `app/config.py` — 新 setting `IMAGE_GEN_PLACEHOLDER_URL`(占位回退 URL)
+
+---
+
+## 9. 验收标准
+
+1. 配置面板能看到 "影片场景" toggle,默认关闭。
+2. 开启 toggle 后保存,学生侧首页出现 2×4 影片网格;点 ▷ 能播放,点 ✕ 能关闭。
+3. 选中一个影片才能点 "开始对话";未选时按钮 disabled 且有提示。
+4. 影片场景模式下:轮次锁为 1;AI 提的问题与所选影片的 description 相关;学生回答后 AI 返回一张图片;DialogueChatView 中正确渲染图片;练习自动结束并跳转报告页。
+5. 不论是否影片场景模式,详细报告中每个学生回答展开后,在"进阶表达"下方都能看到 `WordPronunciationDisplay`,展示带正确(绿)/错误(红)/漏读(灰)下划线的单词。
+6. OverallReport 的 aiComment / highlights / suggestions 在使用了真实数据时,能在文本中提到具体的问题词(如 `although`、`banana`),证明 LLM 看到了 word_analysis。
+7. 后端日志中能 grep 到 `[pronunciation:azure-result]` 行,内含 word_analysis 完整 JSON。
+8. `dialogue_message.image_url` 列存在,影片场景模式下的 AI 消息行 `image_url` 非空。
+9. 普通模式(videoScene.enabled=false)的所有现有功能不受影响(回归)。