/~learn/s/6b6fda74
为文档浏览模块增加了"听书"能力:点击按钮即可用豆包语音合成朗读 Markdown 文档内容,支持三音色混音、语速/音高调节、断点续听、进度条拖拽。后端对接火山引擎豆包语音大模型 V3 HTTP API,前端使用 HTML5 Audio API 播放。
浏览器(前端) Python HTTP Server(后端) 火山引擎 TTS API
----------- ------------------------ -----------------
DOC_PAGE 模板 unified_server.py openspeech.bytedance.com
├─ JS: ttsExtractChunks() 提取文本片段 ├─ GET /api/tts/voices ──► JSON 声音列表 /api/v3/tts/unidirectional
├─ JS: ttsBuildUrl() 构造请求 ├─ GET /api/tts?text=...──► MP3 音频流 ├─ x-api-key 鉴权
├─ JS: ttsPlayChunk() 播放+预加载下一段 │ └─ TTSService.synthesize() ├─ custom_mix_bigtts 混音
├─ JS: ttsSaveProgress() → localStorage │ ├─ SQLite 缓存查询/写入 ├─ speech_rate [-50, 100]
├─ CSS: 底部固定悬浮播放器 │ ├─ _call_api() → HTTP POST └─ post_process.pitch [-12,12]
└─ HTML: 设置面板(模式/声音/语速/音高/混音比例) │ └─ 流式 JSON Lines → base64 解码
└─ tts_cache.db(SQLite WAL)
https://openspeech.bytedance.com/api/v3/tts/unidirectionalseed-tts-1.0(豆包语音大模型)x-api-key header(非旧版 Bearer;token)api_balance_config.json → volcengine.tts.api_key
{
"user": {"uid": "aohan_quant"},
"req_params": {
"text": "要合成的文本",
"speaker": "custom_mix_bigtts",
"audio_params": {
"format": "mp3",
"sample_rate": 24000,
"speech_rate": -30
},
"mix_speaker": {
"speakers": [
{"source_speaker": "zh_female_cancan_mars_bigtts", "mix_factor": 0.6},
{"source_speaker": "zh_female_sajiaonvyou_moon_bigtts", "mix_factor": 0.1},
{"source_speaker": "zh_female_meilinvyou_moon_bigtts", "mix_factor": 0.3}
]
},
"additions": "{\"post_process\":{\"pitch\":7}}"
}
}
API 返回流式 JSON Lines,每行一个 JSON 对象:
code: 0 + data 字段 → base64 编码的 MP3 音频块code: 20000000 → 合成完成标记
将所有 code=0 的 data 解码后拼接即得完整 MP3。
使用 custom_mix_bigtts speaker + mix_speaker.speakers[] 实现最多 3 种音色同时混合:
| 复制声音 | 替代公共音色 | 混音因子 | voice_type |
| --------- | ------------ | --------- | ------------ |
| 宝宝冯 | 灿灿/Shiny | 0.6 | zh_female_cancan_mars_bigtts |
| 奶音甜妹 | 撒娇甜妹 | 0.1 | zh_female_sajiaonvyou_moon_bigtts |
| 慵懒女友 | 魅力女友 | 0.3 | zh_female_meilinvyou_moon_bigtts |
> 注意:如果用户在豆包 App 中使用了"声音复刻"功能创建了专属声音,其 voice_id 为 S_xxx 或 ICL_xxx 格式,可在火山引擎控制台 → 音色库 → 我的音色中获取,然后替换 default_mix 中的 source_speaker。
用户截图中的配置到 API 参数的映射关系:
| 用户配置 | API 参数 | 转换公式 |
| --------- | --------- | --------- |
| 语速 0.70 | speech_rate | round((speed - 1.0) * 100) → -30 |
| 音高 1.5 | post_process.pitch | 半音单位,范围 [-12, 12],1.5 倍音高 ≈ +7 半音 |
speed 范围 [0.5, 2.0],对应 speech_rate [-50, 100]。speech_rate=0 表示正常语速。
为避免重复合成相同内容,通过 SQLite 本地缓存合成结果:
adata/aohan/quant/db/tts_cache.dbtts_cache(cache_key TEXT PK, audio_blob BLOB, text_length INT, voice_sig TEXT, created_at TIMESTAMP)SHA256(text | voice_signature) — 换声音/语速/音高都会产生不同缓存INSERT OR REPLACE 保证幂等
ttsExtractChunks)从 Markdown 渲染后的 HTML DOM 中提取纯文本,跳过以下内容:
<pre><code> 代码块.mermaid 图表<table> 表格<img> 图片--- 块)
TTSService.split_text)
tts_chunks = []; // 文本片段数组
tts_idx = 0; // 当前播放片段索引
tts_audio = null; // HTML5 Audio 对象
tts_playing = false; // 播放状态
ttsPlay() → 重置索引,调用 ttsPlayChunk(0)ttsPlayChunk(i) → 用 ttsBuildUrl(chunks[i]) 构造请求 URL → fetch() 获取 MP3 → URL.createObjectURL() → new Audio(blobUrl) → audio.play()tts_idx + 1),消除段落间停顿audio.onended → tts_idx++ → 如果还有下一段,ttsPlayChunk(tts_idx)tts_audio.ontimeupdate → 当前段内偏移 + 已播完段落估算时长 → 更新 UI
localStorage key: tts_v2:{pathname}{idx, time, total}ttsJump(saved.idx, saved.time) 跳转到保存的段落和时间
┌──────────────────────────────────────────────────────────┐
│ 项目架构文档 ⚙️ □ │
│ [▶] ────●──────────── 02:00 / 05:30 │
└──────────────────────────────────────────────────────────┘
Blob URL 播放 MP3audio.ontimeupdate 进度更新
| 路由 | 方法 | 功能 | 返回 |
| ------ | ------ | ------ | ------ |
/api/tts/voices | GET | 获取可用声音列表 | JSON |
/api/tts?text=&voice=&speed=&pitch=&mix= | GET | 合成音频 | audio/mpeg |
/api/tts/stats | GET | 缓存统计 | JSON |
| 文件 | 角色 |
| ------ | ------ |
adata/aohan/quant/utils/tts_service.py | TTS 后端服务:API 调用、缓存、分段、声音列表 |
unified_server.py | Web 服务器:3 个 TTS 路由 + DOC_PAGE 模板前端代码 |
api_balance_config.json | API 密钥配置(volcengine.tts.*) |
adata/aohan/quant/db/tts_cache.db | SQLite 音频缓存(自动创建) |
api_balance_config.json 已加入 .gitignore,API Key 不会提交到仓库