Files
calx/CLAUDE.md
2026-07-18 16:48:41 +08:00

67 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
小学生乘法口诀(九九表)语音背诵小程序。核心交互:**自动读题 → 小朋友开口说答案 → 本地离线语音识别判分**全程免手hands-free。两种模式顺序闯关、随机挑战随机模式对错题动态加权
只有两份代码文件:`index.html`(前端单页,含全部 UI/逻辑)和 `server.js`(后端,零 npm 依赖)。没有构建步骤、没有 `package.json`、没有 `node_modules`、没有测试框架。
## Commands
```bash
# 启动HTTP:8000 + HTTPS:8443首次自动生成自签证书
bash start.sh # 或 node server.js
# 换电脑 / IP 变了后重新生成自签名证书
bash gen-cert.sh
# 语法检查(无测试框架,改完用这个自查)
node -e "new (require('vm').Script)(require('fs').readFileSync('server.js','utf8'));console.log('server ok')"
node -e "const m=require('fs').readFileSync('index.html','utf8').match(/<script>([\s\S]*?)<\/script>/)[1];new (require('vm').Script)(m);console.log('frontend ok')"
# 端到端验证识别流水线(生成中文语音 → 打 /stt
say -v Tingting "三七二十一" -o /tmp/t.aiff
ffmpeg -y -i /tmp/t.aiff -c:a libopus /tmp/t.webm
curl -s -X POST --data-binary @/tmp/t.webm -H "Content-Type: audio/webm" http://localhost:8000/stt
# 切换到更快的 base 模型
WHISPER_MODEL=models/ggml-base.bin node server.js
```
**改动 `index.html` 里判分/解析逻辑(`judge`/`parseNumToken`/`extractLastNumber`)后**,务必把纯函数抠出来跑一批中文数字用例(`二十一`/`二一`/`21`/`三七二十一`/`答案是二十一` 等都应判对),这是最易回归的部分。
## 运行环境依赖(非 npm
后端 shell out 到两个系统命令,缺一不可:
- `ffmpeg` —— 把浏览器录音(桌面 `webm/opus`、iOS `mp4/aac`)统一转 16k 单声道 wav
- `whisper-cli` —— whisper.cpp 的离线识别二进制(`brew install whisper-cpp`
模型放在 `models/``ggml-small.bin`(默认,较准)、`ggml-base.bin`(较快)。识别全程离线,不联网、不上云。
## Architecture
### 后端 `server.js`(纯 Node `http`/`https`,无框架无依赖)
- 同一个 `handler(req,res)` 同时挂在 HTTP(8000) 和 HTTPS(8443) 上。HTTPS 用 `certs/` 里的自签证书,缺证书则只起 HTTP。
- 路由:`GET /`index.html`GET /cert`iOS 下载证书)、`GET /health``POST /stt`
- `POST /stt` 流水线(`transcribe()`):收原始 body Buffer → 按 `Content-Type` 定扩展名写临时文件 → `ffmpeg` 转 wav → `whisper-cli -l zh` 输出 txt → 去标点/空白 → 返回 `{text}`。**前端只拿 `text`,判分在前端做**,后端不碰口诀逻辑。
- `--prompt` 用中文数字示例做偏置,提升「得数」识别率。
### 前端 `index.html`(单文件,三个「页面」用 `.hide` 切换home / game / end
- **口诀数据**`buildProblems()` 生成 45 条三角表a≤b每条带 `weight`(随机模式加权用)。`kouOf()`/`numToCn()` 生成传统口诀读法(如 `三四十二``二五一十`)。
- **判分核心**(改这里要特别小心,容错是重点需求):`judge(transcript,a,b,p)` 先剥掉标点/运算词/口语填充词、再剥掉句首因数前缀(正反序、中/数字),然后 `parseNumToken`/`extractLastNumber` 把中文数字或阿拉伯数字解析成整数与乘积比对。要同时容忍 `二十一`/`二一`/`21`/完整口诀/句尾数字等说法。
- **免手录音闭环**(关键状态机):`autoAsk()`TTS 读题)→ `startRecordCycle()`MediaRecorder 录音 + `vadTick()` 音量静音检测自动断句)→ `stopRecord()``onRecStop()`(上传 `/stt``judge``resolve`)。没听清会自动重录,上限 `CONFIG.maxAttempts`
- **`resolve(correct,timedOut)`** 是所有作答路径(语音/键盘/超时)的唯一汇合点:更新分数/进度、随机模式调 `updateWeight()`、触发正/负反馈(`positive`/`negative` + 音效 `beep` + 彩带 + 结果卡 `afterSheet`)、再 `nextQuestion()`(顺序模式答错则 `retrySame` 重问同题)。
- **随机错题加权** `updateWeight()`:答错/超时大幅提权、答对且快降权、答对但慢小幅提权;`pickWeighted()` 按权重随机抽题 → 错题/慢题更常出现,练熟自动减少。权重同时看**对错**和**用时**`elapsed` vs `CONFIG.slowMs`)。
- **降级链**:不支持录音 / 麦克风不可用 → 自动切数字键盘(`switchToPad``padMode`/`autoActive` 两个开关联动),键盘作答同样汇入 `resolve()`
### 前端所有可调参数集中在 `index.html` 顶部的 `CONFIG`
读题开关、限时秒数、随机题数、错题加权系数、VAD 灵敏度(`vadThreshold`/`vadSilenceMs`)、重录次数等。首页「⚙️ 家长设置」面板直接绑定其中常用项。
## 平台约束(重要,别踩坑)
- **iOS 语音必须 HTTPS + 受信任证书**。iOS 上所有浏览器都是 WebKit`http://IP` 下苹果直接禁麦克风、不弹授权;且浏览器自带的 `SpeechRecognition` 在 iOS 基本不可用 —— 这正是要走「录音上传后端识别」而非浏览器识别的原因。iOS 首次需下载 `/cert` 并在「设置 → 通用 → 证书信任设置」里开完全信任,详见 `README.md`
- **音频/麦克风/TTS 必须在用户手势内解锁**`ensureAudio()` 在首页点「开始」(`startGame`)时调用一次 `getUserMedia` + `AudioContext.resume()` + 静音 TTS之后才能全自动。
- 证书 SAN 里写死了本机 LAN IPIP 变了必须 `gen-cert.sh` 重新生成。