1
This commit is contained in:
@@ -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(幂等) |
|
| 视觉素材生成 | 场景插画/角色立绘/道具图标/计策卡图标由 `cmd/genasset/` 离线生成管线调用本机 oMLX(Qwen3.5-9B)生成 SVG 并转 PNG,打包入客户端随五端发布;未生成素材按 emoji 映射兜底。剧本台词流同管线生成:`go run ./cmd/genasset --only=scripts` 生成草稿(workspace/scripts/)→ 人工精修 → `--import-scripts` 校验 + 注音 + 回填 scene_node.script(幂等) |
|
||||||
|
|
||||||
### 后台(Web)
|
### 后台(Web)
|
||||||
@@ -183,6 +183,17 @@ cd admin-src && npm install && npm run dev # vite 代理到后端
|
|||||||
|
|
||||||
后端启动时自动初始化数据库与种子数据(36 计内容、题目、奖品、徽章)。
|
后端启动时自动初始化数据库与种子数据(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 部署
|
### Docker 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -23,3 +23,11 @@ genasset:
|
|||||||
timeout: 600 # 本地 ~5 tok/s,单张 SVG 1-3 分钟
|
timeout: 600 # 本地 ~5 tok/s,单张 SVG 1-3 分钟
|
||||||
max_tokens: 24576
|
max_tokens: 24576
|
||||||
retries: 3
|
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 时钟偏差重试次数
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ require (
|
|||||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
|
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
|
||||||
github.com/gogf/gf/v2 v2.10.2
|
github.com/gogf/gf/v2 v2.10.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
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
|
github.com/mozillazg/go-pinyin v0.21.0
|
||||||
golang.org/x/crypto v0.55.0
|
golang.org/x/crypto v0.55.0
|
||||||
)
|
)
|
||||||
@@ -20,8 +22,6 @@ require (
|
|||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // 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/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.10 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default {
|
|||||||
})
|
})
|
||||||
this.after(MAX_LINE_MS, () => this.forceNext(i))
|
this.after(MAX_LINE_MS, () => this.forceNext(i))
|
||||||
try {
|
try {
|
||||||
speak(line.text, '', () => {
|
speak(line.text, line.audio, () => {
|
||||||
// 行号守卫:迟到的第 i 句 onEnd 不得提前放行当前句
|
// 行号守卫:迟到的第 i 句 onEnd 不得提前放行当前句
|
||||||
if (this.lineIndex === i && !this.ended) {
|
if (this.lineIndex === i && !this.ended) {
|
||||||
this.lineDone.sp = true
|
this.lineDone.sp = true
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ CREATE TABLE admin_user (
|
|||||||
- **标注算法**:按**多音字词表最长匹配**切分原文——命中词表(成语整体、专名)的片段整体转拼音,未命中片段逐字转(`github.com/mozillazg/go-pinyin`,StyleTone 带声调符号);词表在 `biz/consts` 集中维护(36 计成语整体拼音 + 教育专名 override),保证准确性
|
- **标注算法**:按**多音字词表最长匹配**切分原文——命中词表(成语整体、专名)的片段整体转拼音,未命中片段逐字转(`github.com/mozillazg/go-pinyin`,StyleTone 带声调符号);词表在 `biz/consts` 集中维护(36 计成语整体拼音 + 教育专名 override),保证准确性
|
||||||
- **标注幂等**:文本未变不重标(标注函数纯函数、无副作用);已存在库的旧数据在启动时补标一次(扫描 `*_pinyin` 为空的记录)
|
- **标注幂等**:文本未变不重标(标注函数纯函数、无副作用);已存在库的旧数据在启动时补标一次(扫描 `*_pinyin` 为空的记录)
|
||||||
- **朗读音频(M1.5 降级方案)**:audio 字段全空且无 TTS 账号时——**前端降级朗读**:H5 端优先播文件(`uni.createInnerAudioContext`,有 `*_audio` 字段就播),无文件则用浏览器原生 `SpeechSynthesis`(zh-CN)朗读该文本,零成本立刻可听;朗读按钮统一封装 `ui-src/src/utils/speech.js`
|
- **朗读音频(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 直接进前端;元素素材(场景/人物/道具图片与名称音频)同样按"写时一次性"生成与上传,随内容接口返回
|
- **静态资源**:Go 静态路由托管 `workspace/uploads/`,URL 直接进前端;元素素材(场景/人物/道具图片与名称音频)同样按"写时一次性"生成与上传,随内容接口返回
|
||||||
|
|
||||||
### 4.7 触控互动组件(M1.5:互动形态 2/3/5/7/8)
|
### 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 不注册路由(路由静态,按需暴露)
|
- **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 豁免)
|
- **收口(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=<uuid去横线>&Sec-MS-GEC=<token>&Sec-MS-GEC-Version=<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`(`<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'><voice name='{voice}'><prosody rate='{rate}'>{文本}</prosody></voice></speak>`,文本先剔除控制字符再转义 `& < >`)
|
||||||
|
- 收帧:二进制帧前 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. 开发计划
|
## 5. 开发计划
|
||||||
|
|
||||||
| 里程碑 | 内容 | 验收标准 |
|
| 里程碑 | 内容 | 验收标准 |
|
||||||
|
|||||||
Reference in New Issue
Block a user