From 8ba749234eea410cc7d6d525ccd09680e73132dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Fri, 14 Aug 2026 12:45:37 +0800 Subject: [PATCH] 1 --- README.md | 13 ++++++++++++- config.yml | 8 ++++++++ go.mod | 4 ++-- ui-src/src/components/StoryPlayer.vue | 2 +- 技术设计.md | 28 ++++++++++++++++++++++++++- 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1eee49a..b33fb3b 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ | 积分 | 闯关得分、每日签到,积分流水明细 | | 徽章 | 成就徽章(集卡、连续学习、通关数、复习、探索等),自动解锁 | | 奖品 | 积分兑换虚拟/实物奖品,兑换记录与状态 | -| 朗读拼音 | 拼音随内容一次性标注入库(逐字对齐 JSON,`common/pinyin.go` + 多音字词表),朗读音频优先播文件、无文件时浏览器语音降级朗读(H5),低龄档(4-6)全量开启;TTS 文件生成留后续接入 | +| 朗读拼音 | 拼音随内容一次性标注入库(逐字对齐 JSON,`common/pinyin.go` + 多音字词表),朗读音频优先播文件、无文件时浏览器语音降级朗读(H5),低龄档(4-6)全量开启;TTS 音频文件由 `cmd/gentss/` 离线管线生成(Edge TTS 神经网络语音,逐句 mp3 回填 audio 字段,五端通用) | | 视觉素材生成 | 场景插画/角色立绘/道具图标/计策卡图标由 `cmd/genasset/` 离线生成管线调用本机 oMLX(Qwen3.5-9B)生成 SVG 并转 PNG,打包入客户端随五端发布;未生成素材按 emoji 映射兜底。剧本台词流同管线生成:`go run ./cmd/genasset --only=scripts` 生成草稿(workspace/scripts/)→ 人工精修 → `--import-scripts` 校验 + 注音 + 回填 scene_node.script(幂等) | ### 后台(Web) @@ -183,6 +183,17 @@ cd admin-src && npm install && npm run dev # vite 代理到后端 后端启动时自动初始化数据库与种子数据(36 计内容、题目、奖品、徽章)。 +### TTS 音频生成 + +```bash +# Edge TTS 神经网络语音(晓晓)逐句合成 mp3,回填台词/内容/场景/选项/学堂各 audio 字段(幂等) +go run ./cmd/gentss --strategy=1 # 按计策分批生成,防全量耗时 +go run ./cmd/gentss --list # 查看待生成任务 +go run ./cmd/gentss # 全量(未生成部分) +``` + +生成前朗读走浏览器语音降级(H5);生成后五端优先播放 mp3 文件。协议与路径规则见技术设计.md 4.16。 + ### Docker 部署 ```bash diff --git a/config.yml b/config.yml index 10bf685..aafaaa4 100644 --- a/config.yml +++ b/config.yml @@ -23,3 +23,11 @@ genasset: timeout: 600 # 本地 ~5 tok/s,单张 SVG 1-3 分钟 max_tokens: 24576 retries: 3 +# 离线 TTS 音频生成(cmd/gentss,仅开发机运行;Edge 朗读 WebSocket) +tts: + voice: zh-CN-XiaoxiaoNeural # 中文女声;可换 zh-CN-YunxiNeural / zh-CN-XiaoyiNeural 等 + rate: "+0%" # 语速,如 "+10%" + version: "1-143.0.3650.75" # Sec-MS-GEC-Version(Chromium 版本号,协议漂移时调) + concurrency: 2 # 并行合成连接数 + timeout: 60 # 单句连接/接收超时(秒) + retries: 3 # 网络失败/403 时钟偏差重试次数 diff --git a/go.mod b/go.mod index 7c65a7f..8aaded6 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2 github.com/gogf/gf/v2 v2.10.2 github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/mozillazg/go-pinyin v0.21.0 golang.org/x/crypto v0.55.0 ) @@ -20,8 +22,6 @@ require ( github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/grokify/html-strip-tags-go v0.1.0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/ui-src/src/components/StoryPlayer.vue b/ui-src/src/components/StoryPlayer.vue index cf486c9..77e6c79 100644 --- a/ui-src/src/components/StoryPlayer.vue +++ b/ui-src/src/components/StoryPlayer.vue @@ -74,7 +74,7 @@ export default { }) this.after(MAX_LINE_MS, () => this.forceNext(i)) try { - speak(line.text, '', () => { + speak(line.text, line.audio, () => { // 行号守卫:迟到的第 i 句 onEnd 不得提前放行当前句 if (this.lineIndex === i && !this.ended) { this.lineDone.sp = true diff --git a/技术设计.md b/技术设计.md index f8165f6..c6b668d 100644 --- a/技术设计.md +++ b/技术设计.md @@ -406,7 +406,7 @@ CREATE TABLE admin_user ( - **标注算法**:按**多音字词表最长匹配**切分原文——命中词表(成语整体、专名)的片段整体转拼音,未命中片段逐字转(`github.com/mozillazg/go-pinyin`,StyleTone 带声调符号);词表在 `biz/consts` 集中维护(36 计成语整体拼音 + 教育专名 override),保证准确性 - **标注幂等**:文本未变不重标(标注函数纯函数、无副作用);已存在库的旧数据在启动时补标一次(扫描 `*_pinyin` 为空的记录) - **朗读音频(M1.5 降级方案)**:audio 字段全空且无 TTS 账号时——**前端降级朗读**:H5 端优先播文件(`uni.createInnerAudioContext`,有 `*_audio` 字段就播),无文件则用浏览器原生 `SpeechSynthesis`(zh-CN)朗读该文本,零成本立刻可听;朗读按钮统一封装 `ui-src/src/utils/speech.js` -- **TTS 流水线(后续里程碑)**:与拼音同一"写时生成"流水线——TTS(如火山/微软)在初始化与后台保存时异步生成音频落 `workspace/uploads/audio/`,表存相对路径;文件名 `{表}_{内容id}_{文本hash}.mp3` 保证**幂等**(文本未变重复保存不重生成);生成失败不阻塞内容落库,音频缺失时前端自动降级为浏览器朗读(前端零改动) +- **TTS 音频文件管线**:与拼音同一"写时生成"原则,但走离线管线 `cmd/gentss/`(Edge TTS 神经网络语音,见 4.16),一次性生成 mp3 回填各 `*_audio` 字段;生成失败不阻塞内容落库,音频缺失时前端自动降级为浏览器朗读(前端零改动) - **静态资源**:Go 静态路由托管 `workspace/uploads/`,URL 直接进前端;元素素材(场景/人物/道具图片与名称音频)同样按"写时一次性"生成与上传,随内容接口返回 ### 4.7 触控互动组件(M1.5:互动形态 2/3/5/7/8) @@ -545,6 +545,32 @@ config JSON 结构(`scene_node.config`,前端解析渲染):`{"kind":"pro - **13 张表补全**:M1 时仅 5 个模块(parent/child/strategy/level/level_play)有完整分层,其余 15 张只有 entity/dao;一次补齐为每表一组文件,6 张在用表的跨表 DAO 直调收拢到各自 service;补齐的表先只建门面结构,不挂 g.Meta 不注册路由(路由静态,按需暴露) - **收口(2026-08-14)**:level_play(闯关流程)并入 level 表文件、表初始化列表并入 main.go、service/auth.go 并入 common/auth——各层严格 = 17 表文件(18 表 − point_log 豁免) +### 4.16 音频生成管线(Edge TTS 神经网络语音) + +朗读音频由离线管线 `cmd/gentss/` 一次性生成(独立 Go 程序,仅开发机运行),逐句合成 mp3 落静态目录并回填各 `*_audio` 字段——运行时零生成依赖,五端通用(小程序/App 不支持 `speechSynthesis`,文件是唯一朗读通道)。 + +**协议要点**(实现对照 edge-tts 7.2.8 源码,本机已验证): +- 端点为微软 Edge 朗读私有 WebSocket:`wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4&ConnectionId=&Sec-MS-GEC=&Sec-MS-GEC-Version=` +- **Sec-MS-GEC 纯算法生成**(无 HTTP 预请求):`ticks = UTC秒 + 11644473600 → ticks -= ticks % 300(5 分钟窗)→ ticks ×= 1e9/100(Windows 100ns 刻度)→ SHA256(取整字符串 + TrustedClientToken).hex().upper()`;403 时读响应 `Date` 头算时钟偏移累加后重试 +- 握手头:`Origin: chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold` + `Cookie: muid=<32位大写hex>` + Edge UA + permessage-deflate +- 连接后发两帧文本:`Path:speech.config`(输出格式 `audio-24khz-48kbitrate-mono-mp3`)→ `Path:ssml`(`{文本}`,文本先剔除控制字符再转义 `& < >`) +- 收帧:二进制帧前 2 字节大端 header 长度,`Path:audio` 载荷拼接为 mp3;文本帧 `Path:turn.end` 结束(`turn.start`/`response`/`audio.metadata` 忽略);每句一个连接串行最稳,超长文本按 4096 字节分块多轮连接后拼接 +- `tts:` 配置段:voice(默认 zh-CN-XiaoxiaoNeural)/ rate / version(Chromium 版本号,协议漂移时调)/ concurrency / timeout / retries + +**生成范围与路径规则**(URL 前缀 `/static/generated/audio/`,与 PNG 素材同挂载): + +| 内容源 | 文件路径 | 回填字段 | +|---|---|---| +| script 台词行(每行) | `audio/{level}/{node}/{line_idx}.mp3` | script JSON 行内 `audio`(整列 Update) | +| 节点内容 scene_node.content | `audio/{level}/{node}/content.mp3` | scene_node.audio | +| 开场 level.scene_content | `audio/{level}/scene.mp3` | level.scene_audio | +| 选项文本 node_option.text | `audio/{level}/{node}/opt_{option_id}.mp3` | node_option.audio | +| 学堂 strategy.teach_content | `audio/teach/{strategy_id}.mp3` | strategy.teach_audio | + +**幂等**:mp3 文件已存在且对应字段非空 → 跳过;`--force` 重生成覆盖;回填单条 Update 无事务(沿用 genasset 先例)。**脚本行音频回填**:整列读 script JSON(行内无 `audio` 的行逐个合成),合成成功后整列重写 JSON 落库,避免逐行 Update。 + +**降级链**:文件缺失 → H5 浏览器 `speechSynthesis` 兜底(speech.js 既有逻辑);文件存在 → 五端一律播 mp3。 + ## 5. 开发计划 | 里程碑 | 内容 | 验收标准 |