← 文档库
📄 文档learn/其他业务/网站设计/听书功能体验复盘与改进方案.md
🌱 创建于2026-10-25 12:57
✏️ 修改于2026-10-25 12:57
👁️ 浏览量—
🔗 短链接/~learn/s/2434ac08
🔐 可见性

听书功能体验复盘与改进方案

> 📅 2026-10-25 建立 > 起因:车上通勤时想听《截面选品与择时信号的双时钟错配与持仓年龄》(learn/截面选基-理解手册/11-截面选品与择时信号的双时钟错配与持仓年龄.md),结果被四个问题全程打断——断断续续、读到一半没了、写着 15 分钟却几分钟就结束、声音小到听不清,而且不知道该给"车里"选哪个音色。 > 一句话结论:这次不是"今天网不好"的偶发,而是播放架构(逐段同步请求 + 只预取 2 段 + 服务端非流式)和参数设计(语速被应用两次 + 默认气声甜妹混音)共同决定的必然结果。 > 本文所有结论都标了代码位置,区分「代码已确认」与「无法确认(缺埋点)」,不做猜测式归因。

---

一、现场还原:五个卡点

# 用户看到的现象 用户感受 归属
---------------------------------
1 每句话之间有明显停顿,像"喘气" 断断续续 架构(逐段同步请求 + 预取太浅)
2 读到一半突然安静,进度条不动了 / 直接停在末尾 文章没读完就没了 失败处理(静默跳段)
3 面板显示约 15 分钟,实际几分钟就结束 时间显示骗人 参数(语速双重生效 + 估算忽略语速)
4 音量已开到很大仍听不清,尤其车噪下 声音小 参数(默认音色 + 无响度归一化)
5 约 100 个音色,分不清哪个适合车里;换个场景又要重调一遍 不知道怎么选 产品(无场景预设、无推荐、无记忆)

---

二、逐条根因(代码级)

2.1 断断续续:一段一次 HTTP,预取只有 2 段,且服务端不是流式

事实链(全部代码确认):

- 首次播放:并发预取前 3 段(unified_server.py:5823-5829); - 稳态:第 idx 段下载完成后才开始预取 idx+1(:5878-5881),idx+1 下载完成后才开始预取 idx+2(:5882-5887); - 也就是说预取要跟着"下载完成"排队,而不是跟着"播放进度"提前跑。一旦某段耗时 5s,后面两段只能干等。

结论:不是"拉取下一段时网络超时"这么简单,而是预取深度只有 2、且是串行的,加上服务端非流式,让每一段都必须独立扛住一次完整的网络往返 + 合成。

2.2 中途断掉:四条路径静默跳段,错误提示 5 秒后自动消失

播放链路上有四条失败分支,行为完全一致:报错一声、tts_idx++、立刻播下一段,不重试、不暂停。

位置 触发条件 行为
---------------------
unified_server.py:5874 后端返回 0 字节音频(blob.size===0) tts_idx++ 直接跳段
:5890-5896 fetch 失败 / HTTP 非 200 / 隧道被掐 弹错 → tts_idx++ → 继续
:5940-5947 <audio> 解码/加载失败(onerror) 弹错 → tts_idx++ → 继续
:5948-5953 audio.play() 被拒(自动播放策略等) 弹错 → tts_idx++ → 继续

而 ttsShowError() 的提示 5 秒后自动隐藏(:5721-5727)。

这就是"读着读着就没了"的机制:隧道抖动时如果连续十几段 fetch 同时失败,这十几段会在两三秒内被逐个 tts_idx++ 冲过去,一路冲到第 84 段,触发 ttsStop()——用户感知就是"突然停了",而且错误条早消失了,看起来像什么都没发生。

还有两个"合法停止"也会被误认为异常:

2.3 15 分钟只听了 6 分钟:语速被应用了两次

估算公式(unified_server.py:5958、:5961-5965):

t += tts_chunks[i].duration || (tts_chunks[i].text.length / 4.5);

完全不看语速。这篇文档按它是 1112 秒 ≈ 18.5 分钟(与用户记忆里的"15 分钟"同一量级,取决于当时正文版本)。

真正的元凶是语速被算了两次:

于是实际耗时 ≈ 估算时长 ÷ 语速²:

面板语速 上游合成 前端播放 实际总耗时(按 18.5 分钟估算)
---------------------------------------------------------
1.0x 1.0x 1.0x 18.5 分钟
1.2x 1.2x 1.2x 12.9 分钟
1.5x 1.5x 1.5x 8.2 分钟
2.0x 2.0x 2.0x 4.6 分钟

用户"才听了几分钟"完全对得上 2.0x 档——显示的还是 1.0x 的时长,所以体感是"写了 15 分钟,几分钟就没了"。再叠加 2.2 节的静默跳段,还会更短。

2.4 声音小听不清:不是音量没开,是音色和响度

2.5 选不出音色:约 100 个音色只有"分类",没有"场景"

VOICE_PRESETS 约 100 条(tts_service.py:25-...),字段是 id / name / category / desc。category 是"通用""解说""温柔女声"这种描述性标签,不是"什么场景用"。面板里只有:女声/男声/另类三个 tab + 名字搜索(unified_server.py:4998-5005)——没有推荐、没有预设、没有"场景 → 配置"的记忆。

偏好里只存了 speed / pitch / mix(tts_prefs,:5389、:6097-6133),没有"场景"概念,也无法为"车里"和"睡前"各存一套。

> 混音补充:_saturn / _uranus 系音色不支持混音,混了会返回空音频(tts_service.py:415-419,logs/tts_errors.log 里 2026-06-27 那批就是此原因)。做预设时必须避开这三系混音。

---

三、埋点与可观测性现状(回答"能看到是什么原因吗")

结论先说:今天这次"断断续续 / 中途断"无法精确归因。 现有三个通道都照不到播放链路。

通道 覆盖范围 对 TTS 能看到什么
---------------------------------
RUM(unified_server.py:692-724) 仅页面加载的 DNS/TCP/TLS/TTFB/解析耗时 看不到,播放期间不上报
慢请求落盘(web_perf.py:90-116) 耗时 ≥5000ms 或 5xx 只有 1 条:2026-10-25 12:40:16 · /api/tts · 5067ms
logs/tts_errors.log 仅 EMPTY_AUDIO(上游返回空音频) 572 行,最新 2026-06-28,10 月无记录

另外两个盲区:

所以第 2.1/2.2 节是"机制上的必然",但这次具体是哪几段超时、跳了几段,数据已经丢了。

埋点最小集(建议 P0 一并做)

事件 字段 落点
------------------
tts_chunk_start idx / total / chars / voice_sig / speed / cached /api/perf/report(通道已有,:20692-20713,加 type:"tts")
tts_chunk_done idx / ms / bytes / retry 同上(sendBeacon)
tts_chunk_skip idx / reason(empty_blob / fetch_fail / audio_error / play_rejected) 同上
tts_session_end expected_s / played_s / skip_count / end_reason(user_stop / ended / sleep / stuck) 同上,一次会话一条
服务端 /api/tts chars / cache_hit / upstream_ms / total_ms / status / error logs/tts_api.jsonl(≥3000ms 或非 200 必落)

有了这五项,才能回答用户原话里的问题:"到底哪一段断的、等了多久、为什么跳。"

---

四、改进方案

优先级 问题 方案 验收标准
-----------------------------
P0-1 语速双重生效 上游 speech_rate 固定 0,语速只由 playbackRate 控制(或反之,二选一) 1.5x 实测总耗时 = 估算 ÷ 1.5,误差 <10%
P0-2 估算忽略语速 ttsEstimateTotal() 返回 raw / tts_speed 面板总时长随语速实时变化
P0-3 静默跳段 每段失败重试 1 次;仍失败则暂停并常驻提示"第 N 段合成失败 · 重试 / 跳过",不自动消失 断网 10s,播放停在第 N 段而不是冲到文末
P0-4 预取太浅且串行 改成"按播放位置向前看 6–8 段"的滑动窗口,下载并发 ≤2,命中缓存时直接加深窗口 段间静音空洞 <0.3s 的占比 >95%(需 P0 埋点数据支撑)
P0-5 服务端非流式 边收上游分片边 chunked 下发,前端先播首片;短期折中:首段单请求、后续段后台并发合成 首段出声时间 <1.5s
P1-1 不会选音色 落地第五章的场景预设 + 一键应用 + 按场景记忆 车里打开听书,默认就是"车噪"预设
P1-2 声音小 加音量滑块(audio.volume 0–1);可选服务端响度归一化(是否支持上游响度参数需先查文档确认,不臆造字段) 嘈杂环境 A/B,主观可懂度明显提升
P1-3 锁屏/后台失控 接 Media Session API(锁屏显示标题与播放控制)+ 页面隐藏时降低预取压力而非停摆 锁屏可暂停/续播,进度不丢
P1-4 缓存无治理 tts_cache 现 509 条 / 26MB、无 TTL 无上限(tts_service.py:272-312),加 LRU 上限(如 500MB / 90 天) 表体积受控,命中率不降
P2-1 想跳章节 按 h2 生成章节目录,支持"下一节 / 只听标题" 听 84 段的长文可秒级定位

P0 的落地顺序建议:先做 P0-2(改一行,立刻消除"时间骗人")→ P0-1(消除双重加速)→ P0-3(把静默失败变成可见)→ 埋点 → P0-4 / P0-5(真正解决断断续续)。前三个改动小、体验收益大,可在一次迭代内完成。

---

五、场景化音色预设(可直接落地)

设计原则:用户只选场景,不选参数。每个预设 = {voice, speed, pitch, volume} 四元组,一键应用,并记住"每个场景上次用的配置"。

场景 推荐音色 音色 ID 语速 音高 为什么
--------------------------------------------
🚗 车内 / 嘈杂(男声) 解说小明 zh_male_jieshuoxiaoming_moon_bigtts 1.0 0 解说腔,咬字实、辅音清楚,最抗噪
🚗 车内 / 嘈杂(女声) 爽快思思 / Skye zh_female_shuangkuaisisi_moon_bigtts 1.0 0 中频集中、不飘,不用气声
🚗 车内(偏沉稳) 知性女声 zh_female_zhixingnvsheng_mars_bigtts 1.0 0 语流稳,长句不易糊
🎧 通勤耳机(信息密度优先) 云舟 2.0 zh_male_m191_uranus_bigtts 1.2 0 沉稳清晰,适合 1.2x 快听
🌙 睡前 / 安静 温柔小雅 zh_female_wenrouxiaoya_moon_bigtts 0.9 0 柔和、少齿音,不刺激
🌙 睡前(男声) 温暖阿虎 / Alvin zh_male_wennuanahu_moon_bigtts 0.9 0 温暖低音量友好
📖 边看边听(对照阅读) 渊博小叔 zh_male_yuanboxiaoshu_moon_bigtts 1.0 0 叙述感强,句读清楚
⚠️ 不推荐车噪 默认混音(灿灿 + 奶音甜妹 + 魅力女友) custom_mix_bigtts — — 气声/高频为主,车噪下最先被淹没;桌面安静环境可用

落地形态(前端):

// 预设表:场景 → 参数
var TTS_SCENES = {
  car_male: {label:'🚗 车内·男声', voice:'zh_male_jieshuoxiaoming_moon_bigtts', speed:1.0, pitch:0, volume:1.0},
  car_female:{label:'🚗 车内·女声', voice:'zh_female_shuangkuaisisi_moon_bigtts', speed:1.0, pitch:0, volume:1.0},
  commute:  {label:'🎧 通勤耳机', voice:'zh_male_m191_uranus_bigtts', speed:1.2, pitch:0, volume:1.0},
  bedtime:  {label:'🌙 睡前',     voice:'zh_female_wenrouxiaoya_moon_bigtts', speed:0.9, pitch:0, volume:0.8}
};
// 存储:当前场景 + 每个场景的覆盖值
localStorage['tts_scene'] = 'car_male';
localStorage['tts_scene_overrides'] = JSON.stringify({car_male:{speed:1.1}});

两个细节:

---

六、产品经理的方法论(这件事说明了什么)

用户的原话是:「产品经理要做的事情好像就是这些吧?不同体验产品,提出问题,然后反复去优化,还有什么思维?」以这次听书为例,能抽出 9 条可复用的:

这次的具体行动线(按顺序):

---

七、证据索引(便于复核)

结论 位置
------------
每段一次请求 unified_server.py:5661-5677、:5869
预取仅 2 级且串行 unified_server.py:5823-5829、:5878-5887
服务端非流式 tts_service.py:382-425
单段实测 5067ms logs/slow_api/2026-10-25.jsonl:16
四条静默跳段 unified_server.py:5874、:5890-5896、:5940-5947、:5948-5953
错误提示 5 秒消失 unified_server.py:5721-5727
语速双算 tts_service.py:459-460 + unified_server.py:5904
时长估算忽略语速 unified_server.py:5958、:5963
无音量赋值 / 无响度参数 全文件无 .volume;tts_service.py:352-360
默认气声混音 api_balance_config.json:15-22
音色仅分类无场景 tts_service.py:25-...、unified_server.py:4998-5005
TTS 零埋点 unified_server.py:5721-5727(只弹 toast)、:20164-20166(500 不写日志)
日志最新只到 6 月 logs/tts_errors.log(572 行,全部 EMPTY_AUDIO,2026-06-27/28)
saturn/uranus 不支持混音 tts_service.py:415-419
缓存无 TTL tts_service.py:272-312

▶ 📜 变更历史(1 次提交)

2dc4ca02026-10-25 12:57docs: 新增听书功能体验复盘与改进方案(断句/跳段/语速双算根因 + 场景预设)
🍃quant 知识库
by 伊甸园和枫

🔊 播放设置

勾选音源后调节配比,总和自动归一化
已选 0 个 · 合计:0%
合成中...
00:00/00:00
1.0x

分享图片

生成中...