init: init proj
This commit is contained in:
66
CLAUDE.md
Normal file
66
CLAUDE.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# 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 IP;IP 变了必须 `gen-cert.sh` 重新生成。
|
||||
Reference in New Issue
Block a user