refactor: 移除自签名证书方案,服务只跑 HTTP
麦克风安全上下文改由部署方案解决:本机用 localhost, 对外部署挂 HTTPS 反向代理。删除 certs/、gen-cert.sh、 /cert 路由与 HTTPS server 分支,文档同步更新。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,8 +1,5 @@
|
||||
# whisper 模型(体积大,用 bootstrap.sh 自动下载)
|
||||
models/
|
||||
|
||||
# 自签证书(每台机器用 gen-cert.sh 生成)
|
||||
certs/
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
|
||||
18
CLAUDE.md
18
CLAUDE.md
@@ -11,15 +11,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# 新电脑一键部署(幂等:装依赖 → 补模型 → 生成证书 → 启动,见 DEPLOY.md)
|
||||
# 新电脑一键部署(幂等:装依赖 → 补模型 → 启动,见 DEPLOY.md)
|
||||
bash bootstrap.sh
|
||||
|
||||
# 启动(HTTP:8000 + HTTPS:8443,首次自动生成自签证书)
|
||||
# 启动(HTTP:8000;麦克风需安全上下文:本机 localhost 或部署时挂 HTTPS 反向代理)
|
||||
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')"
|
||||
@@ -50,9 +47,9 @@ VOICES=Tingting,Shelley node eval.js # 多音色
|
||||
|
||||
## 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`。
|
||||
### 后端 `server.js`(纯 Node `http`,无框架无依赖)
|
||||
- 单个 `handler(req,res)` 挂在 HTTP(8000)。不做 HTTPS——本机用 `localhost`(安全上下文),对外部署挂 HTTPS 反向代理。
|
||||
- 路由:`GET /`(index.html)、`GET /health`、`POST /stt`。
|
||||
- `POST /stt?a=&b=` 流水线(`transcribe()`):收原始 body Buffer → 按 `Content-Type` 定扩展名写临时文件 → `ffmpeg` 转 wav(前后各补 0.5s 静音,短音节必需)→ `whisper-cli -l zh` 语法约束解码输出 txt → 去标点/空白 → 返回 `{text}`。**前端只拿 `text`,判分在前端做**,后端不碰口诀逻辑。
|
||||
- **grammar 约束解码是识别准确率的命根子**:带 `a`/`b` 参数时用 `dynGrammar()` 按题生成 grammar(前缀锁死为本题因数、得数全开放——只约束前缀不会把答错扶正);否则退回静态 `grammar/number.gbnf`;没有 grammar 文件才用 `--prompt` 偏置。grammar 只允许中文数字输出——阿拉伯数字会把「十四/七十四」压扁成 14/74 丢掉「十」。grammar 另含发音兼容 `alias` 规则(牛/流/柳/溜/两/酒):不给别名时方言音会被硬扭到错误数字(如 niú 声学上更近 jiǔ「九」),给别名让解码器输出真实音、前端 `DIG` 映射回数字。
|
||||
|
||||
@@ -71,6 +68,5 @@ VOICES=Tingting,Shelley node eval.js # 多音色
|
||||
|
||||
## 平台约束(重要,别踩坑)
|
||||
|
||||
- **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` 重新生成。
|
||||
- **麦克风只在安全上下文可用**(浏览器硬限制):本机开发/运行用 `http://localhost:8000`;对外部署把服务挂在支持 HTTPS 的反向代理后面(项目自身不做 TLS)。浏览器自带的 `SpeechRecognition` 兼容性差——这正是走「录音上传后端识别」而非浏览器识别的原因。
|
||||
- **音频/麦克风/TTS 必须在用户手势内解锁**:`ensureAudio()` 在首页点「开始」(`startGame`/`enterMatch`)时调用一次 `getUserMedia` + `AudioContext.resume()` + 静音 TTS,之后才能全自动。
|
||||
|
||||
49
DEPLOY.md
49
DEPLOY.md
@@ -11,11 +11,11 @@
|
||||
|---|---|---|
|
||||
| **拷贝的文件** | 整个 `calx/` 目录 | 见下方清单。`models/`(模型)建议一起拷,省下载 |
|
||||
| **要装的软件** | Node.js、`ffmpeg`、`whisper-cpp` | 前者官网/brew,后两者 `brew install` |
|
||||
| **不用带** | `node_modules`(没有)、`certs/`(新机自动生成) | 证书含本机 IP,换机必须重新生成 |
|
||||
| **不用带** | `node_modules`(没有) | 真·零依赖 |
|
||||
|
||||
必须一起迁移的文件清单:
|
||||
```
|
||||
index.html server.js bootstrap.sh start.sh gen-cert.sh
|
||||
index.html server.js bootstrap.sh start.sh grammar/
|
||||
models/ggml-small.bin (465MB,必需)
|
||||
models/ggml-base.bin (141MB,可选,更快)
|
||||
README.md DEPLOY.md CLAUDE.md
|
||||
@@ -32,15 +32,9 @@ cd calx
|
||||
bash bootstrap.sh
|
||||
```
|
||||
|
||||
脚本会自动:检查 Node → 装 `ffmpeg`/`whisper-cpp` → 补下模型(若缺)→ 生成自签证书 → 启动服务。
|
||||
脚本会自动:检查 Node → 装 `ffmpeg`/`whisper-cpp` → 补下模型(若缺)→ 启动服务。
|
||||
可反复运行,已装的会跳过。
|
||||
|
||||
启动后终端打印:
|
||||
```
|
||||
HTTP : http://localhost:8000 http://<本机IP>:8000
|
||||
HTTPS: https://<本机IP>:8443
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方式 B:手动部署(逐步)
|
||||
@@ -54,29 +48,23 @@ mkdir -p models && cd models
|
||||
curl -L -o ggml-small.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin
|
||||
cd ..
|
||||
|
||||
# 3. 生成证书 + 启动
|
||||
bash gen-cert.sh
|
||||
# 3. 启动
|
||||
node server.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 各端怎么访问
|
||||
## 怎么访问
|
||||
|
||||
### 💻 电脑本机
|
||||
Chrome / Edge / Safari 打开 `http://localhost:8000` —— 麦克风直接可用,点「开始」授权即可。
|
||||
### 💻 本机运行
|
||||
Chrome / Edge / Safari 打开 `http://localhost:8000` —— 麦克风直接可用(localhost 是浏览器认可的安全上下文),点「开始」授权即可。
|
||||
|
||||
### 📱 iPhone / iPad(要用语音 → 必须 HTTPS + 信任证书,一次性)
|
||||
> iOS 只有在「HTTPS + 受信任证书」下才允许网页用麦克风,这是苹果硬限制。用 **Safari** 完成:
|
||||
### 🌐 对外部署(其它设备访问)
|
||||
浏览器只在**安全上下文**(HTTPS 或 localhost)里放行麦克风。把服务挂在支持 HTTPS 的反向代理(nginx / Caddy / 云平台均可)后面,其余零改动:
|
||||
|
||||
1. 手机与电脑连**同一 Wi-Fi**(或你的远程方案能到 `8443` 端口)。
|
||||
2. Safari 打开 `https://<电脑IP>:8443/cert` → 下载描述文件(提示不受信任,继续)。
|
||||
3. **设置 → 通用 → VPN 与设备管理** → 安装 `calx-local`。
|
||||
4. **设置 → 通用 → 关于本机 → 证书信任设置** → 打开对 `calx-local` 的**完全信任**。
|
||||
5. Safari 打开 `https://<电脑IP>:8443/` → 点「开始」→ **允许麦克风** → 全程免手。
|
||||
|
||||
### 🤖 安卓 / 其它
|
||||
Chrome 打开 `https://<电脑IP>:8443/`,首次会警告证书不受信 → 「高级 → 继续前往」→ 允许麦克风。
|
||||
```
|
||||
浏览器 ──HTTPS──> 反向代理 ──HTTP──> node server.js (:8000)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -87,13 +75,10 @@ Chrome 打开 `https://<电脑IP>:8443/`,首次会警告证书不受信 →
|
||||
WHISPER_MODEL=models/ggml-base.bin node server.js
|
||||
|
||||
# 改端口
|
||||
PORT=8000 HTTPS_PORT=8443 node server.js
|
||||
|
||||
# 电脑 IP 变了(换网络) → 证书失效,重新生成,手机重做信任步骤
|
||||
bash gen-cert.sh
|
||||
PORT=8000 node server.js
|
||||
```
|
||||
|
||||
前端玩法参数(读题开关 / 限时秒数 / 随机题数 / 错题加权 / 静音检测灵敏度)在 `index.html` 顶部 `CONFIG`,或游戏首页「⚙️ 家长设置」里改常用项。
|
||||
前端玩法参数(读题开关 / 限时秒数 / 口诀范围 / 随机题数 / 错题加权 / 抢答参数 / 静音检测灵敏度)在 `index.html` 顶部 `CONFIG`,或游戏首页「⚙️ 家长设置」里改常用项。
|
||||
|
||||
---
|
||||
|
||||
@@ -101,8 +86,7 @@ bash gen-cert.sh
|
||||
|
||||
| 现象 | 原因 / 处理 |
|
||||
|---|---|
|
||||
| 点「开始」不弹麦克风授权 | 不是 HTTPS,或 iOS 证书没「完全信任」→ 重做 iOS 第 2~4 步 |
|
||||
| iOS 证书装了仍拦麦克风 | 证书 SAN 的 IP 与访问 IP 不一致 → `bash gen-cert.sh` 后重装证书 |
|
||||
| 点「开始」不弹麦克风授权 | 不是安全上下文:本机用 `http://localhost:8000`;对外部署走 HTTPS 反向代理 |
|
||||
| 识别没反应 / 不准 | 看服务端日志有无 `[stt] … -> "xxx"`;`/health` 里 `modelExists` 应为 true;环境别太吵、吐字清楚 |
|
||||
| `whisper-cli: command not found` | `brew install whisper-cpp` |
|
||||
| `ffmpeg: command not found` | `brew install ffmpeg` |
|
||||
@@ -112,6 +96,5 @@ bash gen-cert.sh
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8000/health # {"ok":true,...,"modelExists":true}
|
||||
curl -sk https://localhost:8443/health # 同上(自签名用 -k)
|
||||
```
|
||||
`modelExists:true` + 两个端口都返回 = 部署成功。
|
||||
`modelExists:true` = 部署成功。
|
||||
|
||||
48
README.md
48
README.md
@@ -1,7 +1,7 @@
|
||||
# 🦉 乘法口诀大闯关
|
||||
|
||||
小学生乘法口诀语音背诵小程序:**自动读题 → 小朋友开口说答案 → 离线语音识别判分**,全程免手。
|
||||
支持顺序闯关 / 随机挑战(错题动态加权,练熟自动减少),限时作答、正/负反馈动效音效。
|
||||
支持顺序闯关 / 随机挑战(错题动态加权,练熟自动减少)/ 双人抢答,限时作答、正/负反馈动效音效。
|
||||
|
||||
语音识别用 **whisper.cpp 本地离线模型**,不联网、不花钱、不上传任何数据到云端。
|
||||
|
||||
@@ -14,14 +14,14 @@ calx/
|
||||
├── index.html # 前端(单页面)
|
||||
├── server.js # 后端(零 npm 依赖,纯 Node)
|
||||
├── start.sh # 一键启动
|
||||
├── gen-cert.sh # 生成自签名 HTTPS 证书
|
||||
├── models/
|
||||
│ ├── ggml-small.bin # 中文识别模型(默认,较准,465MB)
|
||||
│ └── ggml-base.bin # 备用模型(较快,141MB)
|
||||
└── certs/ # HTTPS 证书(换电脑/换 IP 后重新生成)
|
||||
├── bootstrap.sh # 新机一键部署
|
||||
├── grammar/ # whisper 解码语法约束
|
||||
└── models/
|
||||
├── ggml-small.bin # 中文识别模型(默认,较准,465MB)
|
||||
└── ggml-base.bin # 备用模型(较快,141MB)
|
||||
```
|
||||
|
||||
`certs/` 和 `node_modules` 不用拷(没有 node_modules)。`models/` 必须一起拷,或到新机器重新下载(见下)。
|
||||
没有 `node_modules`。`models/` 建议一起拷,或到新机器重新下载(见下)。
|
||||
|
||||
## 二、新电脑需要的运行环境
|
||||
|
||||
@@ -39,46 +39,28 @@ curl -L -o ggml-base.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/m
|
||||
cd ..
|
||||
```
|
||||
|
||||
## 三、启动
|
||||
## 三、启动与访问
|
||||
|
||||
```bash
|
||||
bash start.sh
|
||||
# 或: node server.js
|
||||
```
|
||||
|
||||
启动后终端会打印:
|
||||
```
|
||||
HTTP : http://localhost:8000 http://<你的IP>:8000
|
||||
HTTPS: https://<你的IP>:8443
|
||||
```
|
||||
- **本机运行**:浏览器打开 `http://localhost:8000`,麦克风直接可用(localhost 是浏览器认可的安全上下文)。
|
||||
- **对外部署**:浏览器只在安全上下文里放行麦克风,把服务挂在支持 **HTTPS 的反向代理**(如 nginx / Caddy / 云平台)后面即可,其余零改动。
|
||||
|
||||
- **电脑本机浏览器**(Chrome/Edge/Safari):直接开 `http://localhost:8000`,麦克风可用。
|
||||
- **iPhone / iPad**:必须用 HTTPS,见下一节。
|
||||
## 四、参数配置
|
||||
|
||||
## 四、iPhone / iPad 用语音(一次性设置)
|
||||
|
||||
iOS 只有在「HTTPS + 受信任证书」下才允许网页用麦克风。步骤(用 **Safari**):
|
||||
|
||||
1. 手机和电脑连**同一 Wi-Fi**(或你自己的远程方式能到 `8443` 端口)。
|
||||
2. Safari 打开 `https://<电脑IP>:8443/cert` → 提示下载描述文件 → 允许。
|
||||
3. **设置 → 通用 → VPN 与设备管理** → 安装刚下载的 `calx-local` 描述文件。
|
||||
4. **设置 → 通用 → 关于本机 → 证书信任设置** → 打开对 `calx-local` 的**完全信任**。
|
||||
5. Safari 打开 `https://<电脑IP>:8443/` → 点“开始” → **允许麦克风** → 之后听题、答题全程免手。
|
||||
|
||||
> 换电脑或电脑 IP 变了:重新 `bash gen-cert.sh` 生成证书,手机重做第 2~4 步。
|
||||
|
||||
## 五、参数配置
|
||||
|
||||
- 前端:`index.html` 顶部 `CONFIG`(读题开关、限时秒数、随机题数、错题加权、静音检测灵敏度等),或首页「⚙️ 家长设置」里直接改常用项。
|
||||
- 前端:`index.html` 顶部 `CONFIG`(读题开关、限时秒数、口诀范围、随机题数、错题加权、抢答参数、静音检测灵敏度等),或首页「⚙️ 家长设置」里直接改常用项。
|
||||
- 后端模型:默认 `ggml-small.bin`。想更快可切 base:
|
||||
```bash
|
||||
WHISPER_MODEL=models/ggml-base.bin node server.js
|
||||
```
|
||||
- 端口:`PORT=8000 HTTPS_PORT=8443`(环境变量可改)。
|
||||
- 端口:环境变量 `PORT`(默认 8000)。
|
||||
|
||||
## 六、常见问题
|
||||
## 五、常见问题
|
||||
|
||||
- **点“开始”没弹麦克风授权**:多半不是 HTTPS 或证书没「完全信任」→ 重做第四节。
|
||||
- **点“开始”没弹麦克风授权**:不是安全上下文——本机请用 `http://localhost:8000`,对外部署须走 HTTPS 反向代理。
|
||||
- **识别不准 / 没反应**:确认终端有 `[stt] ... -> "xxx"` 日志;模型加载 `✅`;小朋友吐字清楚、环境别太吵。
|
||||
- **iOS 上没声音读题**:iOS 要求首次交互才解锁语音,点过“开始”后即正常。
|
||||
- **完全不想用语音**:点游戏里「⌨️ 用手写」,或环境不支持时会自动切数字键盘。
|
||||
|
||||
10
bootstrap.sh
10
bootstrap.sh
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 乘法口诀 —— 新电脑一键部署脚本(macOS)
|
||||
# 作用:检查/安装依赖(ffmpeg + whisper-cpp) → 补齐模型 → 生成证书 → 启动服务
|
||||
# 作用:检查/安装依赖(ffmpeg + whisper-cpp) → 补齐模型 → 启动服务
|
||||
# 幂等:已装的会跳过,可反复运行
|
||||
# ============================================================
|
||||
set -e
|
||||
@@ -41,14 +41,6 @@ else
|
||||
echo "✅ 模型 ggml-small.bin 已存在"
|
||||
fi
|
||||
|
||||
# 5) 自签名证书(iOS 语音必需)
|
||||
if [ ! -f certs/cert.pem ]; then
|
||||
echo "🔐 生成自签名 HTTPS 证书…"
|
||||
bash gen-cert.sh
|
||||
else
|
||||
echo "✅ 证书已存在(IP 变了请手动跑 bash gen-cert.sh 重生成)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==== 部署完成,启动服务 ===="
|
||||
exec node server.js
|
||||
|
||||
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDLTCCAhWgAwIBAgIUU3wQsQyuE+9iWOTikINHiQzObmcwDQYJKoZIhvcNAQEL
|
||||
BQAwFTETMBEGA1UEAwwKY2FseC1sb2NhbDAeFw0yNjA3MTgwNzE2MjVaFw0yODEw
|
||||
MjAwNzE2MjVaMBUxEzARBgNVBAMMCmNhbHgtbG9jYWwwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQCrpEVDdf0hl75+SvVvtvV+Qfa/0o3675VaHOe1QBUE
|
||||
el4max9hDCptebApt52uHu15rvm8ZO0wJOX/0MJHYDCl3C9clFtA/QRpMbPv7A+J
|
||||
5zVeBVSAU/VWPBQYmaS62Klm5FFchwNBvPXmv07iJ3aaOBqu4h/5a+tcAEACvmwg
|
||||
mubF3jBVOLsBDi8rlLO2wVaQ98Xyq+vsAQnbgq8nsrznw7VVI05+TEXLMus3KAnU
|
||||
1aS0GnMDVzRUOqsOlBn7zNiVUcxHJD6Rt9Q0WaVJ+cOUixIyPyI9W5lYSIXZAcsN
|
||||
4jt+CZf+r4/iAfVWINzp9aUHgubOhvqlNHGU9BPKgTmrAgMBAAGjdTBzMB0GA1Ud
|
||||
DgQWBBSHKrKKHSaZaJTKnqsqkuwdNQMpRTAfBgNVHSMEGDAWgBSHKrKKHSaZaJTK
|
||||
nqsqkuwdNQMpRTAPBgNVHRMBAf8EBTADAQH/MCAGA1UdEQQZMBeHBMCoCbmHBH8A
|
||||
AAGCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAmFTaG80LZK1fkIt6tYde
|
||||
JyY7+dM/dLOEd5i/0ZB8b/oybmJ5xszqd4obQwHO/X8UdQOazjhtZM1DEmp2BM99
|
||||
w+pPHB6X7or+GXsvh41LoOdbzXMVeCQbjZYbMWJ+Z0PfV55jF55Rsyd0RgQwdEYW
|
||||
vQxTfB1ka2+6BhrSm/cJMDpZynDZsLBMIAl+N/r2aviD4mJ1axkHqoBrUDP3Uz2O
|
||||
tgMoGOKgc3HH6jDFFi99ITo4WBtmPGeprFIn8czVkmZCsyz57bC77nbL2IGC5pbr
|
||||
xe7SV3pgY4KM0kuV5GnjzzjORXCQDbEgIVvt2pZ8mAs4KA2dXtHLefMfQT8dIhYv
|
||||
xw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCrpEVDdf0hl75+
|
||||
SvVvtvV+Qfa/0o3675VaHOe1QBUEel4max9hDCptebApt52uHu15rvm8ZO0wJOX/
|
||||
0MJHYDCl3C9clFtA/QRpMbPv7A+J5zVeBVSAU/VWPBQYmaS62Klm5FFchwNBvPXm
|
||||
v07iJ3aaOBqu4h/5a+tcAEACvmwgmubF3jBVOLsBDi8rlLO2wVaQ98Xyq+vsAQnb
|
||||
gq8nsrznw7VVI05+TEXLMus3KAnU1aS0GnMDVzRUOqsOlBn7zNiVUcxHJD6Rt9Q0
|
||||
WaVJ+cOUixIyPyI9W5lYSIXZAcsN4jt+CZf+r4/iAfVWINzp9aUHgubOhvqlNHGU
|
||||
9BPKgTmrAgMBAAECggEAHaT+jXs4X+/sZ7WclFHeLpNIAbNk8D0m52xsnIJX3vEf
|
||||
h7Uis0k9nrDQuzPKWQNgco7E9UPdL16AFuHAXEGBxXO0/mFlLWFFvMFn2X1E5cV4
|
||||
EygKgyt9CQrRUbpfmCHJiErQvpFW/6iaM+NAxZCZTGN6QFAvw41fFgG5DIBE/pBZ
|
||||
NALrPxiWORTY1wnqktEZYa97QKv4NRPCDHnOwS7R5ILps79a0qv6IiKQA8ztC4EI
|
||||
AOA7J9i2G6SZz7wbCfc+8Ndw3Z0a7Y7OMrfuPxHvYlJoZS70o3Et1TFeuKo0FDuV
|
||||
6NdAnQu4UBRMU1i1ggqLFbsM4StJOzI2jeIZmsjFAQKBgQDydUme1jKcxLGPZT2T
|
||||
nvcIPyJm4FjUNNbv8VqW/V6YtFlKt0rEvoGDKQbV0pkcf4MkXPcmDCJbB411g2MR
|
||||
AhThsKVWL0bpYUdB6SX3xEPkKc4r/cHbqXVPSl38Dgqw4qHHaH02qlXBy+RNuwMv
|
||||
uOKvk68pH30EXzzl9RCZhpLLgQKBgQC1Om+J61EtGkEIid5QonvvIViYfgVcfgMy
|
||||
igQD7GnUSb4vNbd1mNPm4sAKHxSp+Hp59uupmjtLr/5ksTZa6ibbXdiX53cXqAsp
|
||||
vzNidP4T+QCkoB1t958QCKRQnd50pijFOUeAa8BuoBCSsYF86Q+WBpt6pHkdXe5H
|
||||
NNnU8FqLKwKBgAn4ZsGgX05xDDgwWpric5gkOH7whNadIaWWUDpbSvOOnwlihjGW
|
||||
/+5r1nsegOtaFwN8yzEGWLdTlS6a64z8f+1T/UiRAyJ/vJVqSARrTrtoOLE4aytk
|
||||
SY2sKjc+QJRLjYmMs3F3CbXBc676nMPun2vG7/IJ+HMgk3mFXVxi21CBAoGAbLmw
|
||||
LRhGTT7RAWRlQg8ZN2/UDTid0s2u8WUn34q4Oyo6tWwOsriLeJtVBz16UjbGk+F1
|
||||
pPzo0hG2F4bRy11rDnSQ+yFcq46fk94n38u8mxLqNdnxjgXfIeltWwg7a9Q75Ybl
|
||||
xL/gZP4Ws0tXGi9gsGsqYDvIhbvStlmYzk2cpJ0CgYAoxb239sleOrqvglo+wogt
|
||||
JK3K74i358BITd1NsB+XsuphQYaYuLc17KLoiGIqyBv6gwfetcn9U+2vmXF1vnwy
|
||||
dLqwhi0ryf0xxrNxOpow1814UK4kuN0uHwhE4Xj+UmdcpjkMyud+jP7qqGbzvfF1
|
||||
ZD4ohLo4O1/U7yLoR3bO5Q==
|
||||
-----END PRIVATE KEY-----
|
||||
12
gen-cert.sh
12
gen-cert.sh
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 生成/更新自签名 HTTPS 证书(IP 变了就重新跑一次)
|
||||
cd "$(dirname "$0")"
|
||||
mkdir -p certs
|
||||
IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo 127.0.0.1)
|
||||
echo "为本机 IP = $IP 生成自签名证书..."
|
||||
openssl req -x509 -newkey rsa:2048 -nodes \
|
||||
-keyout certs/key.pem -out certs/cert.pem -days 825 \
|
||||
-subj "/CN=calx-local" \
|
||||
-addext "subjectAltName=IP:${IP},IP:127.0.0.1,DNS:localhost"
|
||||
openssl x509 -in certs/cert.pem -outform der -out certs/calx-cert.cer
|
||||
echo "完成。iOS 请访问 https://${IP}:8443/cert 下载并信任证书。"
|
||||
26
server.js
26
server.js
@@ -8,7 +8,6 @@
|
||||
* 模型:./models/ggml-small.bin(默认,较准)或 ggml-base.bin(较快,用 WHISPER_MODEL 切换)
|
||||
*/
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
@@ -17,7 +16,6 @@ const crypto = require('crypto');
|
||||
|
||||
const ROOT = __dirname;
|
||||
const PORT = process.env.PORT || 8000;
|
||||
const HTTPS_PORT = process.env.HTTPS_PORT || 8443;
|
||||
const THREADS = String(Math.min(8, os.cpus().length || 4));
|
||||
const WHISPER = process.env.WHISPER_BIN || 'whisper-cli';
|
||||
const FFMPEG = process.env.FFMPEG_BIN || 'ffmpeg';
|
||||
@@ -126,15 +124,6 @@ function handler(req, res) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// ---- iOS 下载证书(浏览器打开会提示安装描述文件) ----
|
||||
if (req.method === 'GET' && url.pathname === '/cert') {
|
||||
fs.readFile(path.join(ROOT, 'certs', 'calx-cert.cer'), (e, data) => {
|
||||
if (e) { res.writeHead(404); res.end('cert not found'); return; }
|
||||
res.writeHead(200, { 'Content-Type': 'application/x-x509-ca-cert', 'Content-Disposition': 'attachment; filename="calx-cert.cer"' });
|
||||
res.end(data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET' && url.pathname === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, model: path.basename(MODEL), modelExists: fs.existsSync(MODEL) }));
|
||||
@@ -170,18 +159,5 @@ http.createServer(handler).listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`\n乘法口诀服务已启动`);
|
||||
console.log(` HTTP : http://localhost:${PORT} http://${IP}:${PORT}`);
|
||||
console.log(`识别引擎: ${WHISPER} 模型: ${path.basename(MODEL)} ${fs.existsSync(MODEL) ? '✅' : '❌ 模型不存在'} 线程: ${THREADS} 语法约束: ${HAS_GRAMMAR ? '✅' : '❌(用prompt)'}`);
|
||||
console.log(`提示: 浏览器麦克风需要安全上下文——本机用 http://localhost:${PORT};对外部署请挂在支持 HTTPS 的反向代理后面`);
|
||||
});
|
||||
|
||||
// ---- HTTPS(iOS 用麦克风必需)----
|
||||
try {
|
||||
const key = path.join(ROOT, 'certs', 'key.pem');
|
||||
const cert = path.join(ROOT, 'certs', 'cert.pem');
|
||||
if (fs.existsSync(key) && fs.existsSync(cert)) {
|
||||
https.createServer({ key: fs.readFileSync(key), cert: fs.readFileSync(cert) }, handler)
|
||||
.listen(HTTPS_PORT, '0.0.0.0', () => {
|
||||
console.log(` HTTPS: https://${IP}:${HTTPS_PORT} (iOS 用这个,先访问 https://${IP}:${HTTPS_PORT}/cert 安装并信任证书)`);
|
||||
});
|
||||
} else {
|
||||
console.log(' HTTPS: 未启用(缺少 certs/key.pem 或 certs/cert.pem)');
|
||||
}
|
||||
} catch (e) { console.log(' HTTPS 启动失败:', e.message); }
|
||||
|
||||
Reference in New Issue
Block a user