|
|
@@ -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)的所有现有功能不受影响(回归)。
|