第二章:AI 语音朗读(TTS)— 长文本分段播放与分片上限调整

作者:

问题现象

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 字符限制是最典型的反面教材
  • 按句子边界切分体验好很多:在句号/问号处断句,用户听不出是分段播放
  • 队列模式天然容错:单段失败不影响整段朗读,比”全成功或全失败”的模式稳健得多

← 返回目录

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注