word-audio-tts.md 7.7 KB

单词缺失读音(讯飞在线 TTS)

行为

LearningPageComponent.play 优先播放原有音频。空缺音频或实际播放失败才调用认证接口 app_word_audio_ensure,仅传 wordId 和可选 playbackFailed。接口返回 {state:'ready',url,source} 或 {state:'pending',retryAfterMs},客户端合并同词请求并有界等待。原有只读课程预览的点读限制保持不变,历史词表的缺音频按钮可以点击生成。切换词条、阶段或离开页面后不播放旧结果。自动播放被浏览器阻止只提示再点喇叭,不生成录音。

服务端读取本帐套已发布的 Model 52 词条(既有词库访问模型),仅允许登录的学生、老师和管理员触发。不开启任意文本合成。原有音频未报错时不读取 TTS 配置、不调用供应商;报错时仅探测受信任媒体域名,禁跟随重定向,仅将 404/410 认定为失效,403/500、浏览器限制、网络超时均不重做。第三方域名失效需要管理员手动清空原地址后生成,防止 SSRF。

缓存键由帐套、NFC/空白规范化后的文本、音标、固定语音配置计算 SHA256;保留大小写与音标区别。该键是数据库主键。不同课本中相同词与音标共用,不合并不同读音。讯飞仅接收词条文本;同形异音词仍需试听验收,有误可用现有后台音频上传覆盖。

短事务认领任务、90秒租约、令牌条件写入、每租户并发限制,供应商请求在事务外执行。先保存合成字节,再上传音频、回填 VocabularyWord.yp。中间字节保留在仅服务端可读的缓存内,用于上传失败或已生成文件丢失后的恢复。回填比较旧音频、原文、音标,避免覆盖人工修改。失败冷却一分钟;成功持久化后反复复用,不重复调用讯飞。调用成功但字节尚未持久化就崩溃仍可能产生一次额外调用,不能声称供应商计费严格 exactly-once。

配置与架构

本机私密配置:~/.config/xiaoshu/xfyun-word-audio.json,权限 600,绝不提交。

必要字段:XFYUN_APP_ID、XFYUN_API_KEY、XFYUN_API_SECRET、XFYUN_TTS_VCN。当前验收声音为 x4_EnUs_Laura_education(Laura 教育、美式英语)。固定语速50、MP3/16k、UTF8、单词模式 reg=0,不加背景音。

控制字段:XIAOSHU_TTS_ENABLED 默认 false,XIAOSHU_TTS_DAILY_LIMIT 默认500,XIAOSHU_TTS_USER_MINUTE_LIMIT 默认30,XIAOSHU_TTS_CONCURRENCY 默认1。按北京时间每天统计实际合成尝试;失败尝试也计入限额以避免费用失控。上传重试不计入合成额度。WordAudioAttempt 保存调用、失败、恢复事件。

2026-10-29 实际服务器探测:云函数提供 fetch 和 WebCrypto,但不提供 WebSocket/require;没有配置 XIAOSHUS3*;词库表没有 isDeleted 字段(按 status 过滤)。因此实际部署需要 Node 22+ 合成适配器 scripts/word-audio-worker.mjs。云函数持有 XIAOSHU_TTS_WORKER_URL(HTTPS /synthesize)和随机32字符以上 XIAOSHU_TTS_WORKER_TOKEN,通过认证 HTTP 调用适配器,适配器负责讯飞 WebSocket。独立部署适配器时讯飞密钥只需留在适配器;云函数不需要它们。

适配器默认仅监听 127.0.0.1:4381,公网部署在 HTTPS 反向代理之后,不开放 CORS。支持 /healthz。POST 仅接受共享凭证、固定发音人/语速、最长200字符,限制并发和每分钟请求量,供应商错误只返回固定提示。不要记录 Authorization 或签名 WebSocket URL。

存储先使用现有 XIAOSHUS3* 配置(固定对象键),未配置则复用已验证支持 MP3 的现有媒体上传服务。后者可能在上传响应丢失后产生重复文件,但仍会复用数据库中保存的音频字节,不重新调用讯飞。原有通用上传的文件白名单不放宽。

部署顺序与回退

  1. 将私密配置安装到适配器主机(权限600、运行用户可读),生成并共享服务端随机 token。运行 node scripts/word-audio-worker.mjs 或从仓库根目录 docker build -f deployment/word-audio/Dockerfile -t xiaoshu-word-audio .;Docker 配置只读挂载到 /run/secrets/word-audio.json,文件属主须为容器 node 用户 UID1000。端口仅绑定宿主机回环地址,通过现有 HTTPS 代理转发 /synthesize。镜像中不包含配置文件。
  2. 设置云函数的 worker URL/token、vcn 和限额,保持生成关闭。node scripts/configure-word-audio.mjs 默认只检查;--apply 使用操作者 XIAOSHU_MASTER_KEY 写入 Parse masterKeyOnly 私密配置;--gateway-only 仅传适配器参数与非讯飞密钥配置。--enable 必须与 --apply 一起才启用,禁用用不带 --enable 的 --apply。
  3. node scripts/migrate-word-audio.mjs 默认不写库;--apply 创建缓存、用量表与认领函数,Parse CLP 禁止客户端直接读写。迁移临时函数仅允许当前管理员、10分钟有效,finally 清理。应用变更前先完成迁移。
  4. 对线上网关做选择性合并并发布移动前端,保留其他已发布修复。禁止用本地整个旧网关盲目覆盖线上独立改动。先验证适配器与媒体存储,再小范围真实词条验证后开启开关。
  5. 回退只关闭生成开关,保留缓存表、MP3 和回填地址,已有音频继续播放。回退网关/前端不删除缓存或媒体文件。

2026-10-30 生产部署记录

已按授权部署到 121.37.7.67:Node.js 24、独立用户 xiaoshu-tts、systemd 服务 xiaoshu-word-audio,仅监听 127.0.0.1:4381,开机启动和失败自动重启。代码位于 /opt/xiaoshu-word-audio/releases/ca6a259,current 指向该版本;私密配置位于 /etc/xiaoshu-word-audio/config.json(600)。

复用既有 Nginx HTTPS 入口 https://server.yuban.co/xiaoshu-word-audio/;健康检查为 healthz,合成为 synthesize。原 Nginx 配置备份为 /etc/nginx/sites-available/server.yuban.co.before-xiaoshu-tts。未改动原服务的根路径。未授权合成请求实测返回 401。

缓存表、用量表及认领函数迁移已完成;客户端直接读写权限关闭。网关使用私密适配器配置,不持有讯飞 API 凭证,生成开关已开启。真实缺音词条 773766(cook)收到 3 个并发请求,结果为 ready/pending/pending,用量表仅有 1 次供应商调用;保存并回填后再次请求返回 existing。下载的 MP3 为 6264 字节、16kHz 单声道,解码检查通过。

移动端已发布 mobile-ca6a259e4f1e-20261029T152955651Z,公网 105 个文件哈希及 8 条无版本访问路径校验通过。音频下载字节与数据库缓存一致,未登录网关请求被拒绝;后端 19 项针对性测试通过。

部署证据及回退基线保存在本机受保护目录 ~/.local/state/xiaoshu/cloud-releases/2026-10-29-word-audio-tts/。现有 HTTPS 证书于 2026-11-04 到期,本次检查未发现服务器自动续期任务,需确认现有证书续期流程。此记录不代表手机微信实机验收。

验证命令

  • node --test scripts/tests/word-audio.test.mjs scripts/tests/word-audio-worker.test.mjs
  • npm run test:mobile:ci
  • npm run build:mobile && npm run cloud:validate
  • node scripts/check-word-audio-browser.mjs(375px,隔离接口与音频样本)
  • node scripts/check-word-audio-live.mjs 默认不调用供应商;--generate 使用一次 apple 合成,--extended 再生成 comfortable/read/NASA/Good morning,已有样音复用。输出只含声音名称、文件路径、字节数和哈希。

真实样音与文件存储回读证据位于忽略目录 tmp/word-audio-qa/。MP3 结构验证不等同真人对所有词汇发音准确性的验收,也不等同手机微信实机验收。