vLLM-Omni 全双工打断(Barge-in)实战:基于barge_in_client.py的语音对话流程深度解析
【免费下载链接】vllm-omniA framework for efficient model inference with omni-modality models项目地址: https://gitcode.com/GitHub_Trending/vl/vllm-omni
本文基于仓库文档 examples/online_serving/barge_in_client_flow.md 编写。
在真实的全双工语音对话中,打断(barge-in)是衡量系统自然度的关键能力:用户提问后,助手已经开始回答,但用户不等说完就再次开口。模型能否在被"抢话"时得体应对——要么果断掐断当前回答、要么把新问题排在轮次末尾——直接决定了对话体验。vLLM-Omni 仓库中的 barge_in_client.py 正是演示这一场景的可运行示例:它既可以连接远端/v1/realtime?duplex=1WebSocket 服务,也可以借助--inline在进程内驱动DuplexOmni,完整复现"提问 → 中途被打断 → 模型处理重叠 → 回答追问"的全过程,并将每次回答的音频与状态落盘。
读完本文,你将掌握:barge-in 场景的完整时序与两种运行方式、脚本全部命令行参数与输入输出约定、DuplexClient/InlineDuplexClient客户端 API 与模型会话预设(preset)的用法,以及重叠策略(overlap policy)、播放确认(playback ack)等全双工底层机制在源码中的实现依据。
场景概述:一次"日常对话"式的中途打断
barge_in_client.py演示的是一个高度贴近真实生活的对话模式:问一个问题 → 回答中途被"抢话" → 看模型如何应对。脚本对模型完全中立(model-neutral),它不依赖任何特定模型内部实现,而是通过公开的客户端 API 驱动任意全双工(duplex)模型:
- 默认走网络路径:通过
vllm_omni.clients.duplex.DuplexClient连接ws://<host>/v1/realtime?duplex=1; - 或者使用
--inline:通过vllm_omni.clients.inline_duplex.InlineDuplexClient在进程内驱动vllm_omni.entrypoints.duplex_omni.DuplexOmni(无需起服务端,模型直接加载在当前进程)。
两种模式下,对话内容完全一致。完整的高层流程如下(精确的线上事件序列见 barge_in_client.py 的 docstring):
输出结果一目了然:输出目录会明确显示走了哪条分支——被打断掐断的第一个回答保存为response_1_cancelled.wav,完整讲完的保存为response_1_completed.wav,而summary.json逐条记录了每次回答的状态、文本与时长。
运行方式一:连接远端全双工服务
启动服务端
以 MiniCPM-o 4.5 为例,仓库提供了现成的服务端启动脚本 barge_in_serve.sh:
MODEL="${MODEL:-openbmb/MiniCPM-o-4_5}" PORT="${PORT:-8099}" exec vllm-omni serve "$MODEL" \ --omni \ --deploy-config vllm_omni/deploy/minicpmo_4_5.yaml \ --trust-remote-code \ --host 0.0.0.0 --port "$PORT"要点拆解:
--omni启用 vLLM-Omni 的 omni 模式;--deploy-config vllm_omni/deploy/minicpmo_4_5.yaml使用 MiniCPM-o 4.5 的三阶段部署配置(Thinker / Talker / Code2Wav);--trust-remote-code:MiniCPM-o 自带自定义 HF 代码(pipeline),必须信任远端代码才能解析;- 该 deploy 配置声明了
session_mode: duplex,因此vllm-omni serve会把模型以DuplexOmni方式运行,服务端仅挂载ws://<host>:8099/v1/realtime?duplex=1(别名/v1/duplex)、POST /v1/chat/completions、/v1/models与/health,其余基于轮次(turn-based)的 HTTP 路由(speech、batch、embeddings、video 等)在 duplex 模型上不提供服务。
关于会话容量的关键事实:vllm_omni/deploy/minicpmo_4_5.yaml中duplex_session.max_sessions: 4(与 Stage 0/1 的max_num_seqs对齐),每个请求在其整个生命周期内都占用一个准入名额,因此该上限同时约束 WebSocket 会话与 HTTP 并发;超出后请求以 HTTP 503 被拒绝(WebSocket 侧对应resource_exhausted错误,可重试)。会话的其他运行时默认值包括:空闲 TTL 300 秒、断线宽限(disconnect grace)30 秒、每个会话待处理输入上限 16 MiB。仓库还提供 2-GPU、3-GPU、8×4090 等多卡变体配置(vllm_omni/deploy/minicpmo_4_5_2gpu.yaml 等),可按硬件替换。
运行客户端
服务端就绪后,执行(也可直接cd examples/online_serving后运行):
python examples/online_serving/barge_in_client.py \ --url ws://127.0.0.1:8099/v1/realtime \ --model openbmb/MiniCPM-o-4_5 \ --ref-audio /path/to/reference_voice.wav \ --question-wav question_16k.wav \ --interrupt-wav follow_up_16k.wav \ --output-dir ./duplex_out脚本会依次完成握手(GREET)、流式上传问题音频并提交轮次(ASK)、等待模型开口、监听约 2 秒后在回答播放中流式上传打断音频并提交(INTERRUPT)、等待针对打断的新回答结束(ANSWER AGAIN)、回报播放进度并优雅关闭会话(HANG UP)。
运行方式二:--inline进程内模式(免服务端)
不需要独立服务端时,使用--inline让模型直接加载在当前进程内:
python barge_in_client.py --inline \ --model /path/to/MiniCPM-o-4_5 \ --ref-audio /path/to/reference_voice.wav \ --question-wav question_16k.wav \ --interrupt-wav follow_up_16k.wav--model此时传本地模型路径而非服务端模型名;--deploy-config可选,默认使用模型自带的 duplex 部署 profile;- 该路径下,脚本会惰性导入
InlineDuplexClient与DuplexOmni,在当前进程创建引擎并加载模型(这正是 WebSocket 路径所不需要的重型依赖),并以trust_remote_code=True解析带自定义 HF 代码的模型 pipeline。
从源码实现看(barge_in_client.py 的_make_client),--inline构造的DuplexOmni(**omni_kwargs)被包装进InlineDuplexClient,且客户端不持有引擎——脚本退出时会通过client.owned_omni.shutdown()关闭进程内引擎,避免遗留 GPU 资源。由于内联客户端没有传输层,它天然不具备重连、会话恢复、心跳与事件确认机制,这是与 WebSocket 客户端的本质差异。
命令行参数全览
脚本通过argparse暴露以下参数(默认值与含义均来自源码 barge_in_client.py 的main()):
| 参数 | 默认值 | 说明 |
|---|---|---|
--url | ws://127.0.0.1:8099/v1/realtime | WebSocket 服务地址(--inline时忽略) |
--model | openbmb/MiniCPM-o-4_5 | 服务端模型名,或--inline时的本地模型路径 |
--inline | 关闭 | 进程内运行:DuplexOmni+InlineDuplexClient |
--deploy-config | None | --inline使用的 deploy YAML(默认取模型自带 profile) |
--preset | minicpmo_4_5 | 会话预设,可选minicpmo_4_5/personaplex/none |
--ref-audio | 无 | 参考人声 WAV(MiniCPM-o preset 必填,缺失时会话以ref_audio_required被拒) |
--question-wav | 必填 | 问题音频,mono 16 kHz PCM16 |
--interrupt-wav | 必填 | 在回答中途说出的追问音频,mono 16 kHz PCM16 |
--output-dir | ./duplex_out | 输出目录(自动创建) |
--chunk-ms | 依 preset | append 分块大小;默认按 preset 对齐模型单元(minicpmo_4_5为 100 ms,personaplex为 80 ms,none为 100 ms) |
--listen-s | 2.0 | 开始打断前"聆听"的秒数 |
--timeout-s | 120.0 | 握手、等待事件等各阶段的超时上限 |
输入音频格式约束:--question-wav与--interrupt-wav必须是mono 16 kHz PCM16。脚本内部read_pcm16_wav会校验单声道与非压缩 PCM,随后_convert_to_session_format依据会话协商的输入格式(config.input_audio)做转换:当 preset 的会话采用不同采集格式(如 PersonaPlex preset 为 24 kHz float32)时,音频会被重采样并转码后再流式上传。MiniCPM-o 的会话输入为 16 kHzpcm16、输出为 24 kHzpcm16。
输出产物与判定逻辑
脚本结束时会在--output-dir下生成三类产物:
- 每个回答一个 WAV:命名规则为
response_<序号>_<status>.wav,其中<status>只有两种取值:cancelled—— 该回答被 barge-in 掐断(半途而废);completed—— 该回答完整讲完。
summary.json:逐条记录每次回答的response_id、status、audio_s(音频时长秒数)、text(累积的转写文本)与wav文件名;events.jsonl:本次运行收到的全部服务端事件的原始 JSON 逐行落盘,便于事后排查线上事件序列。
判定逻辑在源码_fold_responses中:脚本监听三类转写增量事件(response.output_audio_transcript.delta、response.output_text.delta、response.text.delta)累积文本;response.done事件携带status,若为cancelled则标记为 cancelled,否则标记为 completed;此外audio.cancelled事件会把仍在in_progress的回答强制标记为 cancelled。这与服务端response.done的status_details.reason(如barge_in)语义一致——见 docs/serving/realtime_duplex_api.md 的事件目录。
脚本 stderr 最后会打印形如N responses (M interrupted); outputs in <dir>的汇总,其中 M 即被打断(cancelled)的回答数,可直接用于验证本次打断是否真的触发了 barge-in 分支。
底层机制:模型自主决定 vs VAD、重叠策略与播放确认
barge_in_client.py演示的会话走的是model-native 通道:不启用任何 VAD(turn_detection为 null),模型自己决定何时聆听、何时开口。脚本提交问题轮次时调用await client.commit(create_response=False),即把听/说决策完全交给模型本身。在 model-native 通道中,音频在 commit 之前就已经边流边喂给模型,因此模型可能在没有任何 commit 的情况下就开始回答或发出聆听决策。
当用户在回答中插入追问时,决定权在服务端的重叠策略(overlap policy):
- MiniCPM-o 4.5 preset 的默认策略为
overlap_policy="listen_only"(见 vllm_omni/clients/minicpmo_4_5.py 的create_duplex_session_config); - 服务端根据重叠语音的量级做出两类决策,并以
overlap.decision事件上报:- 打断内容足够实质(如重叠时长超过阈值)→barge-in:正在进行的回答被掐断(
response.done状态为cancelled,status_details.reason为barge_in),追问随后被回答; - 只是短暂插话 → 推迟到轮次末尾再处理:先讲完第一个回答,再回应追问。
- 打断内容足够实质(如重叠时长超过阈值)→barge-in:正在进行的回答被掐断(
以 MiniCPM-o 4.5 为例,session.created返回的会话对象中可见重叠相关默认阈值:overlap_short_ack_ms: 700、overlap_barge_in_ms: 1200、overlap_silence_rms: 0.003。能力协商(capabilities)中supports_barge_in: true表示该模型支持打断(PersonaPlex 与 Nemotron VoiceChat 当前不支持,见 docs/serving/realtime_duplex_api.md 的能力表)。
播放确认(playback ack)是保证打断后历史真实性的关键:MiniCPM-o preset 的playback_commit_policy="ack_only"意味着助手的回答只有被客户端确认播放到的部分才进入会话历史。脚本在 HANG UP 阶段调用acknowledge_collected_playback(client, collector)回报全部收集到的播放进度,使历史与真实播放内容对齐。若客户端不回报,服务端历史最多只会按commit_all_on_done的默认保守口径提交。这一设计正是"打断后模型知道自己说到哪、没说什么"的基础。
客户端 API 与模型预设
演示脚本建立在 vllm_omni/clients/duplex.py 之上,这是一个仅依赖标准库、pybase64与websockets的客户端侧包,服务端代码从不导入它。核心组成:
DuplexClient:异步上下文管理器,进入时连接并完成session.update握手、等待session.created;退出时发送session.close并等待session.closed。支持ReconnectPolicy(默认 5 次尝试、0.25–4 秒抖动退避)与 30 秒心跳租约;stream_pcm(pcm, chunk_ms=...):将 PCM 切成chunk_ms毫秒的 append 并按实时节奏发送(realtime=False则全速发送);commit(create_response=False):把缓冲语音封装成用户条目并结束一个轮次,轮次只能通过 commit 结束;responses():把事件流按回答解复用为ResponseHandle(含decision、transcript、played_ms、audio()、wait()等);EventCollector:累积事件用于断言与延迟统计(timing_summary可报告首字/首音频延迟、音频节奏,以及请求extra_body.return_stage_metrics=True时的 Stage 0 token 指标ttft_ms/tpot_ms/itls_ms);ack_playback(played_ms, ...):报告累计播放位置,是ack_only策略下保持历史诚实的入口。
会话形态由SessionConfig决定,模型特有参数放在其extra_body中,因此客户端保持模型中立,每个模型各提供一个 preset:
| Preset | 输入 / 输出音频 | 设定内容 |
|---|---|---|
vllm_omni.clients.minicpmo_4_5.create_duplex_session_config(ref_audio=...) | 16 kHzpcm16/ 24 kHzpcm16 | force_listen_count=0、overlap_policy="listen_only"、playback_commit_policy="ack_only";ref_audio为助手音色片段 |
vllm_omni.clients.personaplex.create_duplex_session_config(voice="NATF2.pt", persona="") | 24 kHzpcm_f32le/ 24 kHzpcm16 | 内置.pt音色提示与以instructions传入的人设 |
SessionConfig(...) | 16 kHzpcm16/ 24 kHzpcm16 | 模型中立默认值;自行传extra_body、turn_detection、overlap_policy、playback_commit_policy、instructions、voice、temperature |
关键词覆盖会替换 preset 中对应SessionConfig字段,例如create_duplex_session_config(ref_audio=..., temperature=0.6)。ref_audio通过audio_data_url()编码为data:audio/wav;base64,...数据 URL 后随会话下发。
与全双工运行时架构的关系
barge_in_client.py只是全双工能力的"前台"示例,其底层架构与完整线上协议分别记录在两份配套文档中:
- 运行时架构:docs/design/fullduplex.md 描述 MiniCPM-o 4.5 全双工运行时(DuplexOmni、会话运行器、重叠与播放确认的规范性契约);
- 线上协议与客户端 API:docs/serving/realtime_duplex_api.md 完整规定了
/v1/realtime?duplex=1的线协议(21 个客户端→服务端事件、42 个服务端→客户端事件、三类 OpenAI Realtime 兼容层级、每个事件的消息示例),DuplexClient讲的正是这套契约;InlineDuplexClient则把DuplexOmni包装在同样的DuplexClientAPI 之后,实现"同一份应用代码,进程内或跨网络两用"。
写自己的全双工客户端时,建议先跑通本文演示脚本、检查events.jsonl理解事件序列,再对照 realtime_duplex_api.md 的事件目录逐类实现:模型自主听说的会话关掉 VAD(turn_detection: null),需要服务器 VAD 打断的会话则设置turn_detection.server_vad并配合overlap_policy="barge_in_on_speech"(注意interrupt_response=false会被拒绝);工具调用、摄像机帧、会话恢复等能力均以session.created返回的capabilities标志为准,按标志分支而不是按模型名硬编码。
【免费下载链接】vllm-omniA framework for efficient model inference with omni-modality models项目地址: https://gitcode.com/GitHub_Trending/vl/vllm-omni
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考