问题现象
STT 修复后,用户开始用口语模式正常对话。但很快发现新问题:AI 的回复稍微长一点,朗读就只读开头几个字,然后就停了。比如 AI 回复了一段 300 字的英文建议,小程序只播放了前两句,后面全没了。用户以为朗读功能坏了,反复点击重试也没用。
对比生成音频文件大小也印证了这一点:一条 480 字符的 AI 回复,生成的音频只有 17KB(约 2 秒),而正常完整朗读应该是 180KB+(约 20 秒)。
排查过程
这个问题是典型的”截断”而非”中断”——不是播放器中途挂掉,而是根本没生成完整的音频。顺着调用链从上往下排查:
第一处截断:前端聊天页
// frontend/src/pages/chat/index.tsx — 旧代码
speak(lastMsg.content.slice(0, 200)) // ← 硬截 200 字符
聊天页在调用 speak() 之前,自己先截了一刀。
第二处截断:speech 模块
// frontend/src/lib/speech.ts — 旧代码
export function speak(text: string): void {
const trimmed = text.slice(0, 200) // ← 又截一刀
// ...请求 TTS 接口
}
即使聊天页传了完整文本,speech.ts 内部还有一道截断。
第三处截断:后端 TTS
# /opt/english-api/routers/tts_router.py — 旧代码
text = text[:200] # ← 第三刀
后端也有硬截断。三层截断叠加,无论前端传多长,最终送到 edge-tts 的文本最多 200 字符。
根因分析
根本原因不是某个单一的 bug,而是前后端各自加了”安全截断”但没有协调。200 字符这个限制的来源是 edge-tts 单次合成的一个经验值——太长的文本会导致合成超时或内存溢出。但正确的做法不是暴力截断然后只读个开头,而是分段合成、队列播放。
修复方案
1. 新增分段函数 splitTextForTTS
在前端 speech.ts 中新增智能分段逻辑,按句子边界切分长文本,每段不超过 500 字符:
export function splitTextForTTS(text: string, maxChars = 500): string[] {
const segments: string[] = []
let remaining = text.trim()
while (remaining.length > 0) {
if (remaining.length <= maxChars) {
segments.push(remaining); break
}
// 在 maxChars 范围内找最后一个句子结束符
let cutAt = maxChars
for (const sep of ['. ', '! ', '? ', '\n', '.', '!', '?']) {
const pos = remaining.lastIndexOf(sep, maxChars)
if (pos > maxChars * 0.6) { cutAt = pos + sep.length; break }
}
segments.push(remaining.slice(0, cutAt).trim())
remaining = remaining.slice(cutAt).trim()
}
return segments.filter(s => s.length > 0)
}
2. 队列式逐段播放
分段后依次请求 TTS 接口,每段播放完毕再播下一段。单段合成失败自动跳过,不中断整段朗读:
export function speak(text: string): void {
const segments = splitTextForTTS(text)
// 清空旧队列,加入新分段(每个分段作为独立任务)
audioQueue.push(...segments.map(seg => ({ text: seg })))
if (!isPlaying) playNext()
}
3. 统一上限为 500
- 前端
splitTextForTTS每段 ≤500 字符 - 后端 TTS 路由上限从 200 提到 500
- 聊天页不再截断,传完整文本给
speak()
验证结果
- 一条 480 字符的 AI 回复,生成 183KB 音频(原来 17KB),完整播放约 20 秒
- 更长的回复会被切成 2~3 段,段间无感知衔接
- 单段 TTS 超时或失败,自动跳过该段继续播后续
经验教训
- 截断不等于降级:暴力截断然后让用户以为功能坏了,不如老实做分段播放
- 前后端的”安全限制”要对齐:三处各自为政的 200 字符限制是最典型的反面教材
- 按句子边界切分体验好很多:在句号/问号处断句,用户听不出是分段播放
- 队列模式天然容错:单段失败不影响整段朗读,比”全成功或全失败”的模式稳健得多
发表回复