第10章 优化性能:实现稳定低延迟唇形同步
Optimizing Performance
讲解如何通过稳定、低延迟的唇形同步来输入音频并播放帧,以及 Python SDK 和 Pipecat 如何为你处理大部分底层逻辑。
你在使用 Python SDK 或 Pipecat 吗?它们已经实现了本页的所有功能,包括音频输入、播放时钟、缓冲以及音视频同步。如果你希望调优这些组件,或是直接基于原始 WebSocket API 开发,请继续阅读。
以下每个部分都带有标签:
🟢 SDK-handled(使用 SDK 或 Pipecat 时自动处理,仅在直接使用原始 WebSocket API 时才需你关注)
🟠 Your setup(无论集成、部署、网络还是凭证,均由你负责)
输入音频以实现稳定、低延迟的唇形同步
🟢 SDK-handled。OjinSTVClient 会自动整形输入流;下方的契约规则仅适用于原始 WebSocket API。
对于 Presence 模式,至少先提供 320 ms 的音频;对于 Portrait 模式,则为 200 ms,随后随着 TTS 生成持续发送音频。
推荐的流式处理模式如下:
1. 每轮对话开始时,Presence 模式提供 500 ms,Portrait 模式提供 400 ms,其余部分随 TTS 实时到达即流式传输。
2. 尽量发送最大的数据块:将排队的片段合并为约 400 ms 的发送单元(保持在 512 KB 的消息上限内)。
3. 保持真正的实时性:切勿等待整个句子生成完毕后再开始发送。
OjinSTVClient 会为你整形输入。随着 TTS 生成音频,直接送入即可。若需使用推荐的初始时长,请将 STVConfig.server_feed_initial_chunk_ms 设置为 Presence 模式的 500 或 Portrait 模式的 400。SDK 会自动处理批处理。TTS 或网络的中断仍可能打断播放。参考 ojin-client README 中 STVConfig 的 server_feed_* 字段进行调优。上述契约仅在你自行驱动原始 WebSocket API 时重要。
# 使用 SDK 时,音频到达即送入;整形操作是自动的。
await client.start_turn()
async for chunk in tts_stream: # 例如 40 ms 的 ElevenLabs / Cartesia 数据块
await client.send_tts_audio(chunk.pcm, sample_rate=chunk.rate, num_channels=1)
按实时速率播放
🟢 SDK-handled:output_stream() 已为你完成节奏控制和缓冲。
模型以实时 25 fps 的速率输出帧,因此你的缓冲区不会自行增长。你只需保留一个小型缓冲区以吸收网络抖动(几帧即可)。消费方式取决于你的集成方案:
SDK / Pipecat:output_stream() 已按实时 25 fps 节奏控制,且音视频同步,客户端为你维护小型抖动缓冲区(initial_buffer_frames)。你无需管理缓冲。
原始 WebSocket:在开始播放前,保留几帧的小型抖动缓冲区。如果帧发生积压(例如短暂网络停顿后突发大量数据),应裁剪空闲帧(frame_type == 0)以恢复同步,切勿丢弃语音(1)、语音起始(3)或淡出(2)帧。仅当刚展示的帧和即将丢弃的帧均为空闲帧时才执行丢弃,并在积压消失后立即停止:跨语音转换边界裁剪会导致明显的画面断层。详见 API Reference 中的“缓冲管理”和“音视频同步”。
不要自行重新同步 SDK 的输出。output_stream() 已按 25 fps 节奏控制且音视频对齐,只需在每帧到达时立即呈现,切勿为了等待视频帧而保留音频:
直接播放(例如在浏览器中):音频和视频帧到达即播放。无需音频时钟或音视频重新同步逻辑。
转发至媒体传输层(例如 WebRTC):每个 STVAudioFrame 和 STVVideoFrame 都携带 pts 时间戳,直接透传即可,让传输层利用它进行同步。不要在其之上构建自制的同步层。
若是基于原始 WebSocket 构建自己的循环?每帧都携带对应的音频,因此请将音频播放与图像渲染同步进行。服务器已保持二者对齐,故你也不需要音频主时钟。详见 API Reference 中的“音视频同步”。
运行位置
🟠 Your setup:部署由你负责,无论使用 SDK 还是原始 API。
实时 API 是专为服务器间通过稳定连接传输而构建的 WebSocket。它不适用于在不可靠的 Wi-Fi 或移动网络下运行的终端用户设备上。
1. 客户端应运行在后端服务器上,而非浏览器或用户设备上。这也确保你的 API Key 不会暴露在客户端。
2. 部署在 US East(美国东部),即 Ojin 实时端点所在区域,以实现最低的往返延迟。
3. 通过为多变网络环境而设计的实时传输协议(通常是 WebRTC)将最终媒体交付给终端用户,而不是向他们暴露原始 WebSocket。
4. 如果用户已在 WebRTC 环境中,SDK 可让 Ojin 将音视频直接发布到你的房间(使用 OjinSTVWebRTCClient 和 WebRTCSettings),而无需你手动抽取帧并中继。
直接发布到 LiveKit 或 Daily
🟠 Your setup:当观众位于 LiveKit 或 Daily 房间时选择此方案。
如果观众在 LiveKit 或 Daily 房间中,请使用直接 WebRTC。Ojin 将虚拟人直接发布到房间,去除了通过你后端的中继环节,从而为观众提供最低延迟。
1. 将 audio_sample_rate 设置为与 TTS 输出速率匹配(例如 24000),以避免音频在到达房间时被重采样。
2. 设置较宽松的 webrtc_join_timeout_s,生产环境建议约 90 秒。需覆盖房间加入和模型冷启动过程,后者单独耗时可能超过 30 秒。
播放更高质量的音频
🟢 SDK-handled:SDK 会自动播放你的原始音频。
在 WebSocket 传输中,OjinSTVClient 会播放你输入的原始音频。它仅将单独的 16 kHz 副本重采样后发送给模型用于唇形同步。听众听到的是你的原始音频,而非 16 kHz 的往返音频。客户端可能会在轮次边界裁剪或填充几帧以保持唇形同步,并在调用 interrupt() 时淡出尾部。
因此,你可以输入更高质量的 TTS(例如 24 kHz 而非 16 kHz)以获得更好的音质,同时唇形同步仍基于 16 kHz 副本正常工作。这是 SDK 侧的功能:在原始 WebSocket API 中,发送的音频必须是 16 kHz 单声道 PCM int16。确保播放器运行在你输入的采样率——STVAudioFrame.sample_rate 报告每帧的速率,但首个 send_tts_audio() 调用前输出的帧默认为 16 kHz,因此不要仅基于第一帧配置播放器。
使用直接 WebRTC 时没有本地播放:将 audio_sample_rate 设置为 TTS 速率,房间将直接接收该音频而无需重采样。
中继帧而不重新编码
🟢 SDK-handled:SDK 为你暴露原始 JPEG(source_bytes)。
如果将视频帧转发到自行解码 JPEG 的浏览器或媒体传输层,可跳过解码:
默认解码器填充 STVVideoFrame.rgb(解码后的 RGB),同时在 STVVideoFrame.source_bytes 中保留原始 JPEG。
在保持的心跳(held tick,即该次无新服务器帧)中,source_bytes 为空(b""),此时应跳过而非中继零字节负载。rgb 仍携带上一帧图像,若需在每次心跳都有内容可取用。
不要使用 PassthroughDecoder 来跳解码。它使客户端完全不输出视频帧,导致你的中继循环静默失效。其 decode() 对每帧返回 None,而客户端仅在拥有可附加的 RGB 图像时才输出视频帧,因此会丢弃所有心跳。请保留默认解码器并读取 source_bytes。
保持低延迟
🟢 SDK-handled · 🟠 Your setup(部署)
在允许的网络条件下,保持缓冲区尽可能小;大缓冲区会增加延迟。在原始 API 中,通过播放期间监控缓冲区大小来调整目标缓冲区大小:足够低以最小化延迟,足够高以吸收抖动且不饿死播放。使用 SDK 时,相关旋钮位于 STVConfig(initial_buffer_frames, max_buffered_video_frames)——完整列表见 ojin-client README。
下一步
故障排除
症状、原因及修复方案。
Python SDK 最佳实践
用于浏览器输出和 WebRTC 后端的成熟流水线。
上一页:Direct WebRTC (LiveKit & Daily)
下一页:Troubleshooting
最后更新于 11 天前。本页是否有用?