|
|
@@ -0,0 +1,227 @@
|
|
|
+# 英文口语 · 单轮内容评语(MVP 设计)
|
|
|
+
|
|
|
+> 状态:设计收敛,待实现
|
|
|
+> 日期:2026-04-23
|
|
|
+> 归属后端:`cococlass-english-speaking-api`
|
|
|
+> 前端消费:`PPT` → `src/views/Editor/EnglishSpeaking/` + `enspeak` 原型的 `SentenceCard` 契约
|
|
|
+> 关联设计:`doc/EnglishSpeakingIntegration.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 背景与问题
|
|
|
+
|
|
|
+现有评分链路覆盖**发音侧**四维:`accuracy / fluency / completeness / prosody`(Azure Speech SDK 的 Pronunciation Assessment)。发音侧回答的是"听起来像不像母语者",不回答"说了什么是否合适"。
|
|
|
+
|
|
|
+enspeak 原型 `DetailedReport.tsx:55` 的 `SentenceCard` 已经给出 UI 契约:每个学生句除发音四分外,还展示 `feedback.{highlights, corrections, suggestions}`。这部分目前为空,本设计补齐。
|
|
|
+
|
|
|
+MVP 目标:用一次 LLM 调用生成这三段结构化评语,不引入任何新 provider,不改动对话主流程。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 核心约束
|
|
|
+
|
|
|
+1. **评分只在结果页展示**:`content_feedback` 永远**不出现在 `/speak` 的响应里**(无论 SSE 流还是最终帧)。对话进行中前端拿不到任何评分数据,避免分心 + 避免评分阻塞对话节奏。
|
|
|
+2. **评分数据的唯一出口是 `/api/speaking/dialogue/report`**:学生/教师打开结果页时按 `sessionId` 拉取,拿到每轮的发音四分 + `content_feedback`。
|
|
|
+3. **评估粒度 = 每轮(per-turn)**:每一轮学生发言独立评估、独立存库。结果页按轮次渲染卡片,OverallReport 的 `overallScore` / 聚合数据由**前端从 per-turn 结果计算**(平均分、top-N 亮点),不引入 session 级 LLM 调用。
|
|
|
+4. **评估时机 = 每轮 `/speak` 完成后立即后台触发,绝不延后到会话结束批量跑**:
|
|
|
+ - `asyncio.create_task(_evaluate_pronunciation)` 在本轮 `/speak` 返回前就挂起,与 AI 回复并发
|
|
|
+ - N 轮对话的 N 次评估**摊在对话进行的整个时间窗口内并发完成**,结果页首屏延迟 ≈ 最后一轮的单轮评估延迟,而非 N × 单轮延迟
|
|
|
+ - 失败隔离:某一轮 Azure / LLM 失败不影响其他轮次;学生中途放弃对话时已完成轮次的评分仍可查看
|
|
|
+5. **评估生成是 fire-and-forget**:`/speak` 立即返回,不等待任何评分完成。每轮内部串行 Azure → Content LLM(Content LLM 需要 Azure 的四分作为输入,故不能并行)。
|
|
|
+6. **结果页需容忍 pending**:打开结果页时可能最近几轮的评估还没跑完,前端应轮询 `/report` 或按 `status` 字段显示"评估中"占位。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 范围
|
|
|
+
|
|
|
+**In scope**
|
|
|
+- 每轮学生发言结束后,在已有的后台评估任务里多挂一次 LLM 调用,产出 `content_feedback`
|
|
|
+- 对 `PronunciationEvaluation` 表加一列 `content_feedback JSON`
|
|
|
+- `GET /api/speaking/dialogue/report` 返回值把 `content_feedback` 并入每个 `evaluation`
|
|
|
+
|
|
|
+**Out of scope(后续迭代)**
|
|
|
+- Session 级的连贯性/任务达成评语(本期 per-turn only)
|
|
|
+- 独立的"内容分"数值(本期只出评语文字,不出 0–100 分)
|
|
|
+- LLM judge 的 self-consistency / calibration / 金标对比
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 数据契约
|
|
|
+
|
|
|
+### 4.1 输出结构(与 enspeak `SentenceCard.feedback` 对齐)
|
|
|
+
|
|
|
+```jsonc
|
|
|
+{
|
|
|
+ "highlights": ["..."], // 1–2 条,中文,≤30 字
|
|
|
+ "corrections": [
|
|
|
+ {
|
|
|
+ "original": "I go to park yesterday", // 英文原句
|
|
|
+ "corrected": "I went to the park yesterday", // 英文改正
|
|
|
+ "explanation": "过去式应用 went,park 前加 the" // 中文解释
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "suggestions": ["..."] // 1–2 条,中文,≤30 字
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 空数组合法(没话可说时不强凑)
|
|
|
+- 整个对象允许为 `null`(LLM 失败时的降级)
|
|
|
+
|
|
|
+### 4.2 数据库变更
|
|
|
+
|
|
|
+`pronunciation_evaluation` 表新增一列:
|
|
|
+
|
|
|
+```python
|
|
|
+content_feedback: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
|
|
+```
|
|
|
+
|
|
|
+无独立状态字段:
|
|
|
+- `status == "completed"` 且 `content_feedback == null` → 内容评语调用失败(LLM 超时/解析失败),不阻塞发音结果
|
|
|
+- `status == "failed"` → 发音失败,内容评语跳过不调用
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 架构
|
|
|
+
|
|
|
+```
|
|
|
+/speak (SSE)
|
|
|
+ │
|
|
|
+ ├─ 同步:ASR → LLM 对话回复
|
|
|
+ │
|
|
|
+ └─ 后台 asyncio.create_task(_evaluate_pronunciation)
|
|
|
+ │
|
|
|
+ ├─ Azure Pronunciation Assessment → 4 个分数 + word_analysis
|
|
|
+ │
|
|
|
+ └─ [新增] ContentEvaluator.evaluate()
|
|
|
+ · 输入:4 分 + 学生转录 + AI 上一句
|
|
|
+ · 输出:{highlights, corrections, suggestions}
|
|
|
+ · 失败:log 并置 content_feedback=null,不影响 status
|
|
|
+```
|
|
|
+
|
|
|
+新增模块一个:`app/service/speaking/content_evaluator.py`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. LLM 调用规格
|
|
|
+
|
|
|
+### 6.1 Provider
|
|
|
+
|
|
|
+复用现有 `LLMProvider`(onehub_llm)。无需新 provider 协议。
|
|
|
+
|
|
|
+### 6.2 模型与参数
|
|
|
+
|
|
|
+- 模型:`gpt-4o-mini` 或等价轻量模型(onehub 内部选)
|
|
|
+- `temperature = 0`
|
|
|
+- `response_format = { "type": "json_schema", "json_schema": FEEDBACK_SCHEMA }`
|
|
|
+ - onehub 不支持 json_schema 时降级为 `{ "type": "json_object" }` + prompt 里加 schema 说明
|
|
|
+- 超时:10s,失败即降级 `content_feedback=null`
|
|
|
+
|
|
|
+### 6.3 Prompt(system)
|
|
|
+
|
|
|
+```
|
|
|
+You are an English tutor evaluating a student's single spoken turn in an
|
|
|
+open dialogue. You receive:
|
|
|
+- Azure pronunciation scores (accuracy/fluency/completeness/prosody, 0–100)
|
|
|
+- The immediate prior AI turn (context)
|
|
|
+- The student's transcript
|
|
|
+
|
|
|
+Produce JSON with three arrays:
|
|
|
+- highlights: 1–2 Chinese sentences praising specific strengths. Reference a
|
|
|
+ pronunciation dimension if that score is ≥ 85. ≤ 30 chars each.
|
|
|
+- corrections: grammar / word-choice fixes. Each item:
|
|
|
+ { original (EN), corrected (EN), explanation (ZH, ≤ 30 chars) }
|
|
|
+- suggestions: 1–2 Chinese actionable improvements. Reference a pronunciation
|
|
|
+ dimension if that score is < 70. ≤ 30 chars each.
|
|
|
+
|
|
|
+Rules:
|
|
|
+- Empty arrays are valid. Do not invent errors to fill quota.
|
|
|
+- If the student only said a filler ("yes", "ok", "hmm"), return all empty
|
|
|
+ arrays plus one encouragement in highlights.
|
|
|
+- Never include the scores as raw numbers in output text; describe
|
|
|
+ qualitatively ("发音准确度很高" not "accuracy 92").
|
|
|
+```
|
|
|
+
|
|
|
+### 6.4 User payload
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "pronunciation": {
|
|
|
+ "accuracy": 72, "fluency": 85, "completeness": 90, "prosody": 60
|
|
|
+ },
|
|
|
+ "ai_said": "What did you do last weekend?",
|
|
|
+ "student_said": "I go to park with my family and play ball."
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+历史消息**只给 AI 上一句**(per-turn 评语场景足够;深层连贯性留给后续 session-level 迭代)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 失败策略
|
|
|
+
|
|
|
+| 场景 | status | content_feedback | 前端表现 |
|
|
|
+|---|---|---|---|
|
|
|
+| Azure 成功 + LLM 成功 | completed | `{...}` | 完整卡片 |
|
|
|
+| Azure 成功 + LLM 超时 | completed | `null` | 只显示发音四分 |
|
|
|
+| Azure 成功 + JSON 解析失败 | completed | `null` | 只显示发音四分 |
|
|
|
+| Azure 失败 | failed | `null`(不调用) | 卡片置错误态 |
|
|
|
+
|
|
|
+前端 `hasEvaluation = score !== undefined || feedback` 的逻辑原样兼容。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 成本与性能预算
|
|
|
+
|
|
|
+- 每轮:输入 ~500 token,输出 ~200 token
|
|
|
+- 按 `gpt-4o-mini` ≈ $0.15 / 1M input、$0.60 / 1M output 估算:**单轮 < $0.0002**
|
|
|
+- 延迟:10s 内必须完成或放弃。用户在对话中不等此结果(已是后台 fire-and-forget),延迟只影响"查看结果页"的 pending 窗口大小
|
|
|
+- 不做 self-consistency / 多次采样,MVP 接受 ±10 分波动(本期无数字分,只有文字评语,波动容忍度高)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. API 契约改动
|
|
|
+
|
|
|
+`GET /api/speaking/dialogue/report` 返回的 `evaluations[i]` 加一个字段:
|
|
|
+
|
|
|
+```jsonc
|
|
|
+{
|
|
|
+ "round": 1,
|
|
|
+ "status": "completed",
|
|
|
+ "accuracy_score": 72,
|
|
|
+ "fluency_score": 85,
|
|
|
+ "completeness_score": 90,
|
|
|
+ "prosody_score": 60,
|
|
|
+ "content_feedback": { // ← 新增,可为 null
|
|
|
+ "highlights": [...],
|
|
|
+ "corrections": [...],
|
|
|
+ "suggestions": [...]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+PPT 前端 `src/types/englishSpeaking.ts` 的 evaluation 类型同步加 `contentFeedback`。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 测试约束
|
|
|
+
|
|
|
+- `tests/service/speaking/test_content_evaluator.py`
|
|
|
+ - mock LLM provider,验证 prompt 组装
|
|
|
+ - JSON schema 合法 / 非法 / 超时三条分支
|
|
|
+ - 空学生转录 / 全 filler 转录 → 走 "all empty + encouragement" 分支
|
|
|
+- `tests/service/speaking/test_dialogue_service.py`(扩充)
|
|
|
+ - Azure 成功 + content 成功 → 两个字段都写入
|
|
|
+ - Azure 成功 + content 失败 → status completed,content_feedback null
|
|
|
+ - Azure 失败 → content 不被调用
|
|
|
+- 不覆盖真实 LLM 调用(集成测试在后续阶段)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 11. 实施步骤(给实现计划用的骨架)
|
|
|
+
|
|
|
+1. DB 迁移:`pronunciation_evaluation.content_feedback JSON nullable`
|
|
|
+2. 新建 `app/service/speaking/content_evaluator.py`,实现 `evaluate(transcript, prior_ai_turn, pron_scores) -> dict | None`
|
|
|
+3. 在 `dialogue_service._evaluate_pronunciation` 的 Azure 调用成功分支后追加 content 评估
|
|
|
+4. `GET /report` 返回值补 `content_feedback`
|
|
|
+5. PPT 前端 `EnglishSpeaking/` 下报告渲染补 `feedback` 展示(enspeak `SentenceCard` 三段 UI 直接移植)
|
|
|
+6. Unit tests
|