AI语音播报流式优化|火山引擎实时推送 + 浏览器边收边播

🤖 AI模型调优实验笔记 📅 2026-07-01
流式TTS StreamingResponse 火山引擎 浏览器音频 用户体验优化

一、问题背景

上一轮优化解决了 TTS 阻塞 chat 接口的问题,但用户点击 🔊 按钮或语音识别后自动播放时,从点击到听到声音仍需等待 6~16 秒。深入分析发现,整个链路存在严重的串行瓶颈:

旧架构数据流:
用户点🔊 → 前端 fetch → 后端等待所有音频块收集完 → 返回完整MP3 → 前端 blob → URL.createObjectURL → audio.load → audio.play

每一步都在等上一步完成,尤其是"后端等所有音频块收集完"这一步,即使自适应超时已经缩短到了 3~5s,前后端 encore 各环节叠加后总耗时仍达 6~16s。

二、流式架构设计

核心思路:打破"等全部收集完再返回"的串行模式,改为"收到一块发一块"的流式架构。


新架构数据流:
用户点🔊 → audio.src=url → 浏览器开始缓冲 → ~0.5s 首个音频块到达 → 立即开始播放

涉及三层改造:

  1. tts.py 新增流式生成器 _volcano_stream()火山引擎 WebSocket 每收到一个 AudioOnlyServer 块就立即 yield,不等待所有块收集完毕。
  2. tts.py 新增 synthesize_stream() 统一入口:命中缓存时分块 yield 模拟流式;否则走火山流式→讯飞→Edge-TTS 的降级链,合成完成后自动缓存到内存 LRU。
  3. stt_api.py /tts 端点改为 StreamingResponse:HTTP 流式推送音频块,首块 15s 内不到则返回 500 让前端降级。

三、关键代码实现

火山引擎流式生成器(tts.py):

async def _volcano_stream(self, text, voice, speed, volume, pitch):
    """每收到一个音频块就立即 yield,不等待"""
    ws = await websockets.connect(VOLCANO_TTS_WS_URL, ...)
    # 连接 → 会话 → 任务请求...
    chunks_count = 0
    while True:
        msg = await asyncio.wait_for(receive_message(ws), timeout=recv_timeout)
        if msg.type == MsgType.AudioOnlyServer and msg.payload:
            chunks_count += 1
            yield msg.payload  # ← 实时发送,不等待收集
        elif msg.event == EventType.TTSSentenceEnd and chunks_count:
            break

StreamingResponse 端点(stt_api.py):

@router.get("/tts")
async def text_to_speech(text: str, ...):
    stream = tts_service.synthesize_stream(text, ...)
    first = await asyncio.wait_for(stream.__anext__(), timeout=15)
    
    async def audio_generator():
        yield first
        async for chunk in stream:
            yield chunk
    
    return StreamingResponse(audio_generator(), media_type="audio/mpeg")

前端简化(chat.html):

// 之前:fetch → blob → URL.createObjectURL → play
const resp = await fetch(ttsUrl);
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
audio.src = url;
await audio.play();

// 之后:直接设 src,浏览器原生流式播放
audio.src = ttsUrl;
audio.load();
await audio.play();  // 首块 ~0.5s 到达即开始播放

四、代码重构亮点

此次优化不仅仅是"加功能",还顺手清理了之前的代码冗余:

  1. 消除重复逻辑:原来的 _synthesize_volcano() 包含 ~80 行完整的 WebSocket 连接+收集逻辑,与新增的 _volcano_stream() 高度重复。重构后 _synthesize_volcano() 成为消费 _volcano_stream() 的薄封装(仅 5 行代码),一份逻辑两处复用。
  2. 补全缓存逻辑:synthesize() 的火山引擎路径原本遗漏了缓存写入,现在前端流式 API 和后台 TTS 预热可共享同一份缓存,减少重复合成。
  3. 浏览器原生能力利用:HTML5 <audio> 元素天然支持流式 MP3 边收边播,前端代码从 ~30 行的 fetch+blob+URL 操作简化为 3 行。

五、优化效果对比

指标优化前优化后提升
首音延迟(点🔊到听见声音)6~16s~0.5s10~30x
前端代码复杂度fetch+blob+URL (~30行)src+play (3行)10x
WebSocket 空等时间15s0s(收到即发)
chat 接口响应被 TTS 拖慢秒级返回-

此次流式改造彻底解决了 TTS 播报延迟问题,用户体验从"点完等十几秒"变为"点了就响",与主流 AI 语音助手的体验差距大幅缩小。

六、技术沉淀与可复用经验

  1. WebSocket 流式数据处理范式:不要先收集再返回,用 async generator + yield 逐块推送,配合 FastAPI StreamingResponse 即可实现端到端流式传输。此模式可复用于任何实时数据推送场景(视频流、传感器数据等)。
  2. 浏览器原生能力优先:<audio> 元素的 src 直接指向流式 URL 即可边收边播,无需自定义 fetch+blob+URL 的前端播放栈。
  3. 缓存与流式共存:缓存命中时分块 yield 模拟流式行为,保证缓存和非缓存场景的前端行为一致。
  4. 降级链设计:火山流式→讯飞→Edge-TTS 的三级降级保障,即使主力引擎异常也能保证基础功能可用。
训练优化中
持续迭代
本文为个人原创实验记录,版权归作者所有,禁止商用转载。
如需技术交流,欢迎通过博客留言或邮件联系。
← 返回AI专栏 返回首页 →

💬 技术交流

📋 留言规则: 本留言板仅用于技术学习交流,严禁发布广告、外部引流链接、涉政、色情、赌博、 营销推广及一切违法违规内容。本站为个人非经营性网站,不提供任何商业接单、付费咨询服务。 所有留言需经人工审核后展示。

📧 技术交流邮箱:

留言提交功能将在公安网安备案全部通过后开放,感谢理解。

💬 过往技术交流

加载中...