2026-05-15-video-scene-and-word-pronunciation-design.md 21 KB

影片场景模式 + 单词级发音可视化 — 设计文档

日期: 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 平级):

videoScene: {
  enabled: boolean    // 默认 false
}

理由:它改变运行时语义(轮次=1、AI 返图),不属于"评估配置"。

DEFAULT_CONFIG 与 EMPTY_CONFIG 都加这一字段(默认 false)。新增 store action toggleVideoScene()。

3.2 影片资源(前端数据)

src/views/Editor/EnglishSpeaking/data/topicDiscussionVideos.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 增加:

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

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 常量文件)新增:

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 新列

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 占位后:

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 加入:

| { type: 'image'; url: string; round: number }

composables/useDialogueEngine.ts 收到 image 事件:把当前 AI message 的 imageUrl 字段填好,并视为 AI turn 完成(等价于 done,但 done 事件后端仍会发,engine 需幂等处理)。

DialogueChatView.vue 渲染 AI 消息时:

<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 字段:

# 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:

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:

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 完整返回:

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