# Phase 4 测试 > 对应施工单:`phase-4-plan.md` > 代码事实:`phase-3-review.md` > 语音回归:`phase-3-test.md` §1 + `phase-2-voice-test.md` §0 / §5 > 固件 **不必重烧** 本阶段验收:**跟读是 MP3 序列 + 定时录音**;分析失败不影响下一句;后评估只有定量 metrics,没有及格结论。 不要测当场「说错重播」、倒背/口算产品、音素、租户。 相对 Phase 2 口令表的刻意变化:听窗内说完 **不会马上** 下一句,要等到 `listen_ratio × 该句 duration_ms`。 --- ## 0. 开始前 1. Liquibase 新 changeset:`expected_text` / `metrics` / `assessed_at`。**没有** `passed` 列。未改 `202608311800.sql`。 2. 音频契约(至少 unit01 两句): ```text /english_learning/shadowing/lessons/unit01/01.mp3 /english_learning/shadowing/lessons/unit01/01.txt /english_learning/shadowing/lessons/unit01/02.mp3 /english_learning/shadowing/lessons/unit01/02.txt ``` 云端 `transcripts` 或 `sentences[]` 与上述 txt 正文一致(热路径不读卡上 txt 也可以测评估,但 **stem 必须能对上**)。 3. `listen_ratio` 在 1.2–1.5。 4. pytest: ```text cd xiaozhi-esp32-server/main/xiaozhi-server python -m pytest tests/test_app_runtime.py tests/test_assessment.py -q ``` 准备 server secret、mac。GET:`/xiaozhi/task/instance/{id}`。 --- ## 1. pytest(必须先过) | 用例 | 通过? | |------|--------| | `resolve_expected_text`:basename `01.mp3` → txt 正文;**路径字符串不得当原文** | | | `text_similarity_v0` 返回 `{method, value}`,**对象不含** `passed`/`grade` | | | 评估缺 asr 或缺 expected → `metrics=[]`,不抛 | | | 模拟听窗:注入 ASR **不**触发 play next;只有 timer 回调才 index+1 | | | timer 回调在 ASR 失败、assess 抛错、upsert 抛错后仍 index+1 | | | `listen_ratio` 0.5 → 夹紧到 1.2;3.0 → 夹紧到 1.5 | | | Phase 3:upsert 调度失败仍能 save | | --- ## 2. 语音:序列时钟(核心) 从 Chat 说 **开始跟读**。记下 01 播完到 02 响起的间隔,应大约是 `ratio × 01 的时长`(不要短到「一说完就切」)。 | 检查 | 通过? | |------|--------| | 01 播完后立刻进入可跟读(不要先讲笑话) | | | 听窗内认真跟读:02 **不会立刻**响,要等到时钟 | | | 听窗内说「今天天气怎么样」或不说话:时钟到了仍播 **02**,**不**再播 01 | | | 说 **退出跟读** 可立刻回 Chat | | | 整段结束后招呼回 Chat;**讲个笑话**可用 | | --- ## 3. 失败不得挡播放(必须) | 场景 | 通过? | |------|--------| | 停 manage_api,跟读至少两句:02 仍会在时钟后出现 | | | 无 01.txt / 无 transcripts:播放两句仍完成;metrics 可空 | | | 全程闭嘴:两句仍按时钟播完(turns 的 asr 可空) | | 恢复 manage_api。 --- ## 4. 后评估对账(允许晚几秒) 跟读至少一句(有说话),等会话结束或再等几秒,GET `{id}`。 | 检查 | 通过? | |------|--------| | `prompt_text` 仍以 `.mp3` 结尾 | | | `expected_text` 等于对应 txt 正文(不是路径) | | | 若有 asr:`metrics` 含 `method=text_similarity_v0` 且 `value` 在 0–1 | | | JSON 与 GET **没有** `passed` / `grade` / 「正确」字段 | | | 列表接口仍不返回整份 turns | | 说错那句:`value` 可以很低——这是定量结果,**不要**因此期望设备重播。 --- ## 5. Agent ≠ App;复读 | 检查 | 通过? | |------|--------| | Chat 笑话只进 chat_history | | | 复读:无 metrics 要求;结束仍只招呼一次 | | | 复读默认会带 `speed`(省略则为按天数算出的值,第一天约 0.8) | | | `repeat=5` 时每一遍倍速约 0.8→0.9→1.0→1.1→1.2,**不会**卡在 0.75/1.0 档 | | | 显式指定 `speed=1.05` 时五遍都用 1.05 | | --- ## 6. 固件 本阶段跟读评估仍不改固件协议。**允许的补充:** `play_audio.speed` 从离散档改为 `[0.5, 2.0]` 夹紧(重采样本来就支持任意倍率)。 | 检查 | 通过? | |------|--------| | 无 JUDGE / 新 MQTT action / 新 MCP 工具名 | | | `self.mp3.play` 仍是 `name/speed/repeat/power_off_after`;speed 可传 0.8 / 1.05 | | | 说错后日志里 **没有** 对同一路径的立即第二次 play(只有时钟后的 02) | | --- ## 7. 不要测什么 - 说错重播同一句(那是已废弃草案) - 「>82% 算好」的产品结论 - 倒背 / 口算 / 接龙 / 心理诊断 - 家长 UI、租户 - 把切换词写入固件 --- ## 8. 失败时先看哪 | 现象 | 多半是 | |------|--------| | 一说完就切下一句 | ASR 仍在 `handle_shadowing_turn` 里 play next(未解耦) | | 说错重播 01 | 把 S8 retry 做回去了 | | 停 API 后卡在第一句 | upsert/assess 又变成同步 await | | metrics 有 passed | DTO 没按计划 | | expected 是路径 | 用 prompt_text 当了原文 | | 时钟明显短于 1.2× | 仍用固定 800ms 而忽略 duration_ms | --- ## 口令(与 Phase 2 相同) | 意图 | 说 | |------|----| | 跟读 | 开始跟读 | | 复读 | 复读机 | | 回聊 | 退出跟读 / 我们来聊天吧 |