diff --git a/docs/user_guide/examples/online_serving/aura_omni.md b/docs/user_guide/examples/online_serving/aura_omni.md index 8b7f56ac5..2042d0d37 100644 --- a/docs/user_guide/examples/online_serving/aura_omni.md +++ b/docs/user_guide/examples/online_serving/aura_omni.md @@ -6,29 +6,29 @@ ASR -> AURA -> Qwen3-TTS Talker -> Code2Wav ``` -Qwen3-TTS remains two engine stages so the pipeline reuses the existing native -Talker and Code2Wav implementation. +**Supported online path:** Realtime duplex at `/v1/realtime?duplex=1`. + +The default deploy profile (`vllm_omni/deploy/aura_omni.yaml`) sets +`session_mode: duplex`. The `aura_omni` pipeline declares `duplex_plugin`, and +`DuplexOmniEngine` requires that mode (same pattern as MiniCPM-o duplex +deploys). Turn-based `chat/completions` is not a supported AURA online mode +against this profile. + +`async_chunk` defaults to `false` in that file (safer if a turn-based +`OmniOrchestrator` ever loads it). Duplex Realtime Stage2→3 still needs +async chunks — use the smoke deploy below, or set `async_chunk: true` +yourself. ```bash -vllm serve aurateam/AURA \ - --omni \ - --deploy-config vllm_omni/deploy/aura_omni.yaml \ - --served-model-name aurateam/AURA \ - --trust-remote-code +bash examples/online_serving/aura_omni/run_duplex_smoke_serve.sh +python examples/online_serving/aura_omni/smoke_duplex_realtime_client.py ``` -Configure local checkpoints by editing per-stage `model` values in -`vllm_omni/deploy/aura_omni.yaml`. The deploy file sets -`pipeline: aura_omni`, so the four-stage topology is used even if the -command-line `--model` points at one of the component checkpoints. +Configure local checkpoints by editing per-stage `model` values in the deploy +or smoke YAML. The file sets `pipeline: aura_omni`, so the four-stage topology +is used even if the command-line `--model` points at one component checkpoint. -Send requests with `"model": "aurateam/AURA"`. The ASR, AURA, and Qwen3-TTS -checkpoint paths are internal stage models from the deploy YAML, not the -OpenAI-facing served model name. - -The AURA stage can emit `<|silent|>`. Silent outputs are treated as a gate: -they produce no Qwen3-TTS Talker input, so no audio is synthesized for that -turn. +Silent Stage1 outputs (`<|silent|>` / id `151669`) skip TTS for that turn. ## GPU Utilization Recommendation @@ -40,26 +40,21 @@ VRAM each stage can reserve. Start with this split for a single GPU: - Stage 2 (Qwen3-TTS Talker): `0.20` - Stage 3 (Qwen3-TTS Code2Wav): `0.20` -## TTS Modes +## TTS modes (stage extras) -`aura_omni` can pass AURA text to Qwen3-TTS in two task modes: +When the duplex session supplies TTS extras, AURA text can feed Qwen3-TTS as: -- `Base`: voice clone from `tts_ref_audio` with ICL enabled in the AURA - pipeline. Provide both `tts_ref_audio` and `tts_ref_text`. Set - `tts_x_vector_only_mode=true` to disable ICL and use speaker embedding only. -- `CustomVoice`: predefined speaker mode. Use a Qwen3-TTS CustomVoice - checkpoint for stages 2 and 3 in `aura_omni.yaml`, then pass - `tts_task_type=CustomVoice` and `tts_speaker`. +- `Base`: voice clone from `tts_ref_audio` (optional x-vector-only mode) +- `CustomVoice`: predefined speaker (`tts_speaker`) with a CustomVoice checkpoint + on stages 2 and 3 -By default, AURA responses are passed to Qwen3-TTS as text. Set -`additional_information.tts_pass_token_ids=true` to pass AURA-generated -assistant token ids directly instead. Even when token passthrough is disabled, -the stage processor uses AURA token ids when available to estimate the Talker -prefill length, so scheduling does not rely on raw character length. +Optional `tts_pass_token_ids` passes AURA assistant token ids into Talker +instead of detokenized text. -The example client exposes this as: +## Old chat / Gradio / curl scripts (unsupported) -```bash -python examples/online_serving/aura_omni/openai_chat_completion_client.py \ - --tts-pass-token-ids -``` +`examples/online_serving/aura_omni/` still contains chat-completions, curl, and +Gradio helpers from an older turn-based path. They do **not** work against the +shipped duplex profile. That path was an incomplete stand-in before streaming +I/O and session history were available; it is not a supported AURA mode. Use +the Realtime duplex smoke client for online verification. diff --git a/examples/online_serving/aura_omni/README.md b/examples/online_serving/aura_omni/README.md index 5f246c336..903850469 100644 --- a/examples/online_serving/aura_omni/README.md +++ b/examples/online_serving/aura_omni/README.md @@ -6,139 +6,89 @@ Qwen3-ASR -> AURA/Qwen3-VL -> Qwen3-TTS Talker -> Qwen3-TTS Code2Wav ``` -The pipeline has three semantic modules, but four engine stages because the -existing Qwen3-TTS implementation is natively split into Talker and Code2Wav. +**AURA's supported online path is Realtime duplex** (`/v1/realtime?duplex=1`). +The default deploy profile sets `session_mode: duplex` because the pipeline +declares `duplex_plugin` and `DuplexOmniEngine` requires that mode. -Start the server with the deploy profile: +This is AURA v1 / Qwen3-VL (silent / ChatML ids `151669` / `151645`), not +AURA v2 / Qwen3.5-VL. + +## Duplex Realtime (only supported online path) + +Stage2→3 codec handoff needs `async_chunk: true`. The default +`vllm_omni/deploy/aura_omni.yaml` keeps `async_chunk: false` as a safer +baseline (turn-based `OmniOrchestrator` never sees the duplex async-chunk +gate). For Realtime, use the smoke deploy (already `async_chunk: true`) or +set that flag yourself: ```bash +bash examples/online_serving/aura_omni/run_duplex_smoke_serve.sh +python examples/online_serving/aura_omni/smoke_duplex_realtime_client.py +``` + +Smoke deploy file: `examples/online_serving/aura_omni/aura_omni_duplex_smoke.yaml`. + +Equivalent serve with the default topology and async chunks enabled: + +```bash +# Copy or edit so async_chunk: true, then: vllm serve aurateam/AURA \ --omni \ --port 8091 \ - --deploy-config vllm_omni/deploy/aura_omni.yaml \ + --deploy-config examples/online_serving/aura_omni/aura_omni_duplex_smoke.yaml \ --served-model-name aurateam/AURA \ --trust-remote-code ``` -The deploy file sets per-stage model repos: +Connect clients to `/v1/realtime?duplex=1`. Silent Stage1 outputs gate TTS (no +audio for that turn). Overlapped input and vision-follow are AURA duplex +capabilities. + +### Browser UI + +AURA has no client VAD: control is **push-to-talk** on the shared shell from +[#7585](https://github.com/vllm-project/vllm-omni/pull/7585) +([`examples/online_serving/realtime_web/`](../realtime_web/README.md)), profile +`aura-ptt` (hold = `is_speech=true` PCM + sticky frames; release = `commit`; +silent+frames for vision-follow). Thin wrapper: + +```bash +python -m examples.online_serving.aura_omni.realtime_web \ + --ws-backend ws://127.0.0.1:8099 --model aurateam/AURA --port 7862 +``` + +Do **not** fork a separate `aura_omni/realtime_web` asset tree; MiniCPM / Qwen +profiles are unchanged and hide the PTT control. + +### Per-stage models + +Edit `model` on each stage in `vllm_omni/deploy/aura_omni.yaml` (or the smoke +YAML) for local checkpoints: - Stage 0 ASR: `Qwen/Qwen3-ASR-1.7B` - Stage 1 AURA: `aurateam/AURA` -- Stage 2/3 TTS: `Qwen/Qwen3-TTS-12Hz-1.7B-Base` +- Stage 2/3 TTS: `Qwen/Qwen3-TTS-12Hz-1.7B-Base` (or CustomVoice) -For local weights, edit the `model` value on each stage in -`vllm_omni/deploy/aura_omni.yaml`. The deploy profile includes -`pipeline: aura_omni`, so the server uses this four-stage topology even when -the command-line model path points at one component checkpoint. +### GPU utilization -Expected request shape: - -- Send microphone audio as the Stage 0 multimodal audio input. -- Include video frames in the original request `multi_modal_data`; the - `asr2aura` processor carries them forward to AURA. -- Optional `additional_information` keys: - - `aura_system_prompt` - - `tts_task_type` - - `tts_language` - - `tts_speaker` - - `tts_instruct` - - `tts_ref_audio` - - `tts_ref_text` - - `tts_x_vector_only_mode` - - `tts_pass_token_ids` - -If AURA emits `<|silent|>`, the `aura2tts` processor returns no TTS request, so -the TTS stages are skipped for that turn. - -## GPU Utilization Recommendation - -Tune `gpu_memory_utilization` per stage in `vllm_omni/deploy/aura_omni.yaml`. -Recommended baseline on one GPU for H200 +Tune `gpu_memory_utilization` per stage. Baseline on one large GPU: - Stage 0 (ASR): `0.10` -- Stage 1 (AURA): `0.4` -- Stage 2 (Qwen3-TTS Talker): `0.20` -- Stage 3 (Qwen3-TTS Code2Wav): `0.20` +- Stage 1 (AURA): `0.40` +- Stage 2 (Talker): `0.20` +- Stage 3 (Code2Wav): `0.20` -## Python Client +## Old chat / Gradio / curl scripts (unsupported) -```bash -python examples/online_serving/aura_omni/openai_chat_completion_client.py \ - --host localhost \ - --port 8091 \ - --model aurateam/AURA \ - --modalities text,audio -``` +`openai_chat_completion_client.py`, `run_curl_multimodal_generation.sh`, and +`run_gradio_demo.sh` target the older turn-based Omni path. They do **not** +work against the shipped `session_mode: duplex` profile. -Use local media: - -```bash -python examples/online_serving/aura_omni/openai_chat_completion_client.py \ - --audio-path /path/to/input.wav \ - --video-path /path/to/video.mp4 \ - --output-dir output_aura_omni_online -``` - -Base voice clone mode (default, recommended as x-vector while debugging ICL): - -```bash -python examples/online_serving/aura_omni/openai_chat_completion_client.py \ - --tts-task-type Base \ - --tts-ref-audio vllm-omni/tests/assets/qwen3_tts/clone_2.wav \ - --tts-ref-text "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." -``` - -Enable AURA token-id passthrough explicitly: - -```bash -python examples/online_serving/aura_omni/openai_chat_completion_client.py \ - --tts-pass-token-ids -``` - -CustomVoice mode requires stages 2 and 3 in `aura_omni.yaml` to point at a -Qwen3-TTS CustomVoice checkpoint: - -```bash -python examples/online_serving/aura_omni/openai_chat_completion_client.py \ - --tts-task-type CustomVoice \ - --tts-speaker Vivian -``` - -By default, AURA responses are passed to Qwen3-TTS as text. Set -`tts_pass_token_ids=true` to pass AURA-generated assistant token ids directly -to Qwen3-TTS instead. The processor still uses AURA token ids, when available, -to estimate the Talker prompt length in the default text path. - -## Curl - -```bash -cd examples/online_serving/aura_omni -bash run_curl_multimodal_generation.sh -``` - -Set `PORT`, `MODEL`, or `OUTPUT_DIR` to override defaults: - -```bash -PORT=8666 MODEL=aurateam/AURA bash run_curl_multimodal_generation.sh -TTS_PASS_TOKEN_IDS=true PORT=8666 MODEL=aurateam/AURA bash run_curl_multimodal_generation.sh -``` - -## Gradio - -Launch the server and Gradio UI together: - -```bash -cd examples/online_serving/aura_omni -bash run_gradio_demo.sh -``` - -If the server is already running: - -```bash -python examples/online_serving/aura_omni/gradio_demo.py \ - --model aurateam/AURA \ - --api-base http://localhost:8091/v1 -``` +That turn-based path was an incomplete June-era stand-in: vLLM-Omni did not +yet support streaming I/O for this stack, and adding session history then would +have required a much larger orchestrator change. It is not a supported AURA +mode. Do not run those scripts against the default deploy. Prefer the duplex +smoke client above. ## Offline diff --git a/examples/online_serving/aura_omni/aura_omni_duplex_smoke.yaml b/examples/online_serving/aura_omni/aura_omni_duplex_smoke.yaml new file mode 100644 index 000000000..2be7ea0e7 --- /dev/null +++ b/examples/online_serving/aura_omni/aura_omni_duplex_smoke.yaml @@ -0,0 +1,119 @@ +# AURA duplex Realtime smoke (local weights, session_mode: duplex). +# Stage1 = /workspace/models/AURA + AuraQwen3VLForConditionalGeneration (AURA v1 / Qwen3-VL). +pipeline: aura_omni +# Qwen3-TTS Stage2→3 codec handoff requires async_chunk=true (same as native / +# duplex explore). async_chunk=false reaches talker/code_predictor then never +# delivers audio to Stage3/client. +async_chunk: true +session_mode: duplex +duplex_session: + max_sessions: 1 + +connectors: + connector_of_shared_memory: + name: SharedMemoryConnector + extra: + codec_streaming: true + connector_get_sleep_s: 0.01 + connector_get_max_wait_first_chunk: 3000 + connector_get_max_wait: 300 + codec_chunk_frames: 25 + codec_left_context_frames: 72 + # Match native/duplex explore. initial=1 can stall Code2Wav on the first + # undersized chunk (observed hang after "Code2Wav codec: frames=1"). + initial_codec_chunk_frames: 8 + decode_cudagraph_capture_sizes: [25, 73, 97, 169, 325] + +stages: + - stage_id: 0 + max_num_seqs: 1 + gpu_memory_utilization: 0.08 + enforce_eager: true + trust_remote_code: true + enable_prefix_caching: true + async_scheduling: true + max_model_len: 4096 + max_num_batched_tokens: 2048 + devices: "0" + model: /workspace/models/Qwen3-ASR-1.7B + default_sampling_params: + temperature: 0.0 + top_p: 1.0 + top_k: -1 + max_tokens: 256 + + - stage_id: 1 + max_num_seqs: 1 + gpu_memory_utilization: 0.55 + enforce_eager: true + trust_remote_code: true + enable_prefix_caching: true + async_scheduling: true + max_model_len: 32768 + max_num_batched_tokens: 8192 + devices: "0" + model: /workspace/models/AURA + model_arch: AuraQwen3VLForConditionalGeneration + default_sampling_params: + temperature: 0.0 + top_p: 1.0 + top_k: -1 + max_tokens: 256 + repetition_penalty: 1.0 + stop_token_ids: [151669, 151645] + + - stage_id: 2 + # Allow prior TTS to drain while the next Talker starts (not assumed 1). + max_num_seqs: 2 + gpu_memory_utilization: 0.12 + enforce_eager: true + trust_remote_code: true + enable_prefix_caching: false + async_scheduling: true + max_num_batched_tokens: 4096 + max_model_len: 4096 + devices: "0" + # Stage1→2 is orchestrator-fed via aura2tts (custom_process_input_func). + # A from_stage_1 SHM edge parks Talker in WAITING_FOR_CHUNK and clears + # additional_information.text → Missing Qwen3-TTS text conditioning. + # Only Stage2→3 codec handoff needs a connector. + output_connectors: + to_stage_3: connector_of_shared_memory + model: /workspace/models/Qwen3-TTS-12Hz-1.7B-CustomVoice + default_sampling_params: + temperature: 0.5 + top_k: 50 + # 12Hz × 240 ≈ 20s. Stay under the smoke 25s audio gate so a missed + # 2150 (English/quotes humming) cannot overlap the next vision-follow. + max_tokens: 240 + min_tokens: 8 + repetition_penalty: 1.05 + # Qwen3-TTS Talker codec EOS — without this Talker can run to max_tokens + # (~26s garbage audio while Stage1 text was correct). + stop_token_ids: [2150] + subtalker_sampling_params: + do_sample: true + temperature: 0.5 + top_k: 50 + top_p: 1.0 + + - stage_id: 3 + max_num_seqs: 2 + gpu_memory_utilization: 0.12 + enforce_eager: true + trust_remote_code: true + enable_prefix_caching: false + async_scheduling: true + max_num_batched_tokens: 16384 + max_model_len: 16384 + devices: "0" + input_connectors: + from_stage_2: connector_of_shared_memory + model: /workspace/models/Qwen3-TTS-12Hz-1.7B-CustomVoice + default_sampling_params: + temperature: 0.0 + top_p: 1.0 + top_k: -1 + max_tokens: 16384 + seed: 42 + repetition_penalty: 1.0 diff --git a/examples/online_serving/aura_omni/realtime_web/__init__.py b/examples/online_serving/aura_omni/realtime_web/__init__.py new file mode 100644 index 000000000..d305da15b --- /dev/null +++ b/examples/online_serving/aura_omni/realtime_web/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""AURA push-to-talk browser call UI.""" diff --git a/examples/online_serving/aura_omni/realtime_web/__main__.py b/examples/online_serving/aura_omni/realtime_web/__main__.py new file mode 100644 index 000000000..1f89c5d41 --- /dev/null +++ b/examples/online_serving/aura_omni/realtime_web/__main__.py @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +from examples.online_serving.realtime_web.server import main + +if __name__ == "__main__": + main(default_profile="aura-ptt") diff --git a/examples/online_serving/aura_omni/run_duplex_smoke_serve.sh b/examples/online_serving/aura_omni/run_duplex_smoke_serve.sh new file mode 100755 index 000000000..ed8f3dc40 --- /dev/null +++ b/examples/online_serving/aura_omni/run_duplex_smoke_serve.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Minimal AURA duplex Realtime server for smoke. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "$REPO_ROOT" + +PORT="${PORT:-8099}" +MODEL="${MODEL:-/workspace/models/AURA}" +DEPLOY_CONFIG="${DEPLOY_CONFIG:-$REPO_ROOT/examples/online_serving/aura_omni/aura_omni_duplex_smoke.yaml}" + +# AURA v1 / Qwen3-VL ChatML ids (do not use AURA v2 248070/248046 here). +export VLLM_AURA_SILENT_TOKEN_ID="${VLLM_AURA_SILENT_TOKEN_ID:-151669}" +export VLLM_AURA_IM_END_TOKEN_ID="${VLLM_AURA_IM_END_TOKEN_ID:-151645}" +export VLLM_AURA_IM_START_TOKEN_ID="${VLLM_AURA_IM_START_TOKEN_ID:-151644}" +export VLLM_AURA_ASSISTANT_TOKEN_ID="${VLLM_AURA_ASSISTANT_TOKEN_ID:-77091}" +# Multi-stage colocated init can make earlier stages' Process.sentinel spuriously +# readable; prefer spawn for stage engine subprocesses (override if needed). +export VLLM_WORKER_MULTIPROC_METHOD="${VLLM_WORKER_MULTIPROC_METHOD:-spawn}" +# Do NOT set TORCHDYNAMO_DISABLE here: it breaks Stage0/1 AOT when those +# stages are not enforce_eager. Stage2/3 use enforce_eager; code_predictor +# honors that and skips its own torch.compile. + +exec "$REPO_ROOT/.venv/bin/vllm-omni" serve "$MODEL" \ + --omni \ + --deploy-config "$DEPLOY_CONFIG" \ + --trust-remote-code \ + --host 0.0.0.0 \ + --port "$PORT" diff --git a/examples/online_serving/aura_omni/smoke_duplex_realtime_client.py b/examples/online_serving/aura_omni/smoke_duplex_realtime_client.py new file mode 100755 index 000000000..431e13d90 --- /dev/null +++ b/examples/online_serving/aura_omni/smoke_duplex_realtime_client.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Minimal AURA duplex Realtime smoke via DuplexClient.""" + +from __future__ import annotations + +import argparse +import asyncio +import base64 +import json +import sys +import time +from io import BytesIO +from pathlib import Path + +from PIL import Image + +REPO_ROOT = Path(__file__).resolve().parents[3] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from vllm_omni.clients.duplex import ( # noqa: E402 + DuplexClient, + EventCollector, + SessionConfig, + read_pcm16_wav, +) + +PROGRESS_TYPES = { + "response.created", + "response.done", + "response.audio.delta", + "response.output_audio.delta", + "response.audio_transcript.delta", + "response.output_text.delta", + "response.text.delta", + "response.listen", + "response.model_listen", + "error", +} + + +def _tiny_jpeg_b64() -> str: + img = Image.new("RGB", (64, 64), color=(200, 40, 40)) + buf = BytesIO() + img.save(buf, format="JPEG", quality=85) + return base64.b64encode(buf.getvalue()).decode("ascii") + + +async def run(url: str, model: str, wav: Path, timeout_s: float) -> int: + pcm16 = read_pcm16_wav(wav) + if not pcm16: + print("empty wav", file=sys.stderr) + return 2 + + frame = _tiny_jpeg_b64() + collector = EventCollector() + config = SessionConfig( + modalities=("text", "audio"), + instructions="You are AURA. Reply briefly in Chinese.", + extra_body={ + "aura_system_prompt": "You are AURA. Reply briefly in Chinese.", + "tts_task_type": "CustomVoice", + "tts_language": "Chinese", + "tts_speaker": "Vivian", + }, + ) + + started = time.time() + client = DuplexClient( + url, + model=model, + config=config, + reconnect=None, + heartbeat_interval_s=None, + handshake_timeout_s=min(60.0, timeout_s), + ) + + async with client: + consume_task = asyncio.create_task(collector.consume(client)) + collector.add({"type": "session.created", "session": getattr(client, "session_info", {})}) + + await client.append_audio(pcm16, is_speech=True, video_frames=[frame]) + await client.commit(create_response=True) + + progress = False + got_done = False + while time.time() - started < timeout_s: + types = [e.get("type") for e in collector.events] + if any(t in PROGRESS_TYPES for t in types): + progress = True + if "response.done" in types or "response.completed" in types: + got_done = True + progress = True + break + for e in collector.events: + meta = e.get("metadata") if isinstance(e.get("metadata"), dict) else {} + if meta.get("model_listen") or meta.get("listen_source") == "aura_silent": + progress = True + break + if collector.count("error") > 0 or collector.count("session.closed") > 0: + break + # Keep draining until response.done so TTS is not truncated. + await asyncio.sleep(0.2) + + try: + await client.close(timeout_s=30.0) + except Exception as exc: # noqa: BLE001 + print(f"close_warn={exc}", file=sys.stderr) + try: + await asyncio.wait_for(consume_task, timeout=5.0) + except (TimeoutError, asyncio.CancelledError): + consume_task.cancel() + + event_types = [e.get("type") for e in collector.events] + errors = [json.dumps(e, ensure_ascii=False)[:500] for e in collector.errors()] + result = { + "ok": progress and (got_done or collector.count("response.done") > 0), + "got_response_progress": progress, + "got_response_done": got_done or collector.count("response.done") > 0, + "errors": errors, + "event_types": event_types, + "n_events": len(event_types), + "audio_bytes": len(collector.audio_bytes()), + "llm_text": collector.response_text(collector.response_ids[0]) if collector.response_ids else "", + "elapsed_s": round(time.time() - started, 2), + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + log_path = Path("/tmp/aura_duplex_smoke/client_events.jsonl") + log_path.parent.mkdir(parents=True, exist_ok=True) + log_path.write_text( + "".join(json.dumps(e, ensure_ascii=False) + "\n" for e in collector.events), + encoding="utf-8", + ) + print(f"events_log={log_path}", file=sys.stderr) + return 0 if progress else 1 + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--url", default="ws://127.0.0.1:8099/v1/realtime") + p.add_argument("--model", default="/workspace/models/AURA") + p.add_argument( + "--wav", + default=str(REPO_ROOT / "tests/assets/minicpmo_4_5/response_required_16k.wav"), + ) + p.add_argument("--timeout-s", type=float, default=300.0) + args = p.parse_args() + return asyncio.run(run(args.url, args.model, Path(args.wav), args.timeout_s)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/online_serving/realtime_web/README.md b/examples/online_serving/realtime_web/README.md index 854aa4718..7aba2ee10 100644 --- a/examples/online_serving/realtime_web/README.md +++ b/examples/online_serving/realtime_web/README.md @@ -9,6 +9,7 @@ This implements [RFC #7222](https://github.com/vllm-project/vllm-omni/issues/722 | `minicpm-native` | Model-controlled listen/speak, continuous audio input | Frames accompany audio | Yes | | `qwen3-turn --stt` (default) | User presses **Send turn** | No | No | | `qwen3-turn --vad` | Server detects trailing silence; speech interrupts replies | Sampled frames with each spoken turn | Yes | +| `aura-ptt` | Hold **Hold to talk**; release commits. Hold also stops local playback | Sticky frames with speech; vision-follow every 2 frames only while unlocked (locked from PTT release until text-final / listen) | Yes | Qwen3 VAD uses an engine-owned duplex plugin: microphone upload continues while replies stream, and speech can interrupt generation and playback. The @@ -29,6 +30,38 @@ The compatibility wrapper serves the shared assets with `minicpm-native`. The current native query, `extra_body`, reference voice, continuous microphone upload, camera frames and playback acknowledgements are retained. +## AURA: push-to-talk duplex + +AURA has no client VAD. Start the duplex serve, then the thin wrapper: + +```bash +bash examples/online_serving/aura_omni/run_duplex_smoke_serve.sh + +python -m examples.online_serving.aura_omni.realtime_web \ + --ws-backend ws://127.0.0.1:8099 --model aurateam/AURA --port 7862 +``` + +Or select the profile on the shared host: + +```bash +python -m examples.online_serving.realtime_web --profile aura-ptt \ + --ws-backend ws://127.0.0.1:8099 --model aurateam/AURA --port 7862 +``` + +Open the UI, start a session, optionally enable **Camera**, then **hold** +**Hold to talk** to stream `is_speech=true` PCM (with sticky camera frames). +**Release** to `input_audio_buffer.commit`. Holding the button stops playback +in the browser only; the in-flight response is not cancelled on the server. +With the camera on, frames are still captured at 2 fps. While the button is +up they are committed once per two frames (`is_speech=false` + silent PCM). +Both frames of that commit are sent and packed as one video (`<|video_pad|>`). +Turns still open from this client commit, not from a server-side auto trigger. +but only after the prior spoken turn unlocks (text-final or `response.listen`). +Release itself locks vision immediately so a follow-up frame cannot abort the +just-committed speech turn on Stage0/1 (`max_num_seqs=1`). Audio still playing +does not hold the next vision turn once text is done. +MiniCPM / Qwen profiles do not set `pushToTalk`, so the PTT control stays hidden. + ## Qwen3: explicit-turn STT adapter Start the backend, then the UI in a second terminal: @@ -49,6 +82,7 @@ accepts only model selection, so system-prompt controls are hidden in this mode. The adapter follows the shipped `qwen3_omni/openai_realtime_client.py` and `vllm_omni/entrypoints/openai/realtime_connection.py`: + 1. Explicit `duplex=0` selects the legacy STT handler on a turn deployment. It does not enable STT on a duplex deployment. 2. Send `{type: "session.update", model: ...}` and `commit(final=false)`. @@ -181,7 +215,7 @@ conversation API. This demo does not add WebRTC, semantic VAD, or tool calling. ## Shared host options -The shared entry point can select either profile explicitly: +The shared entry point can select a profile explicitly: ```bash python -m examples.online_serving.realtime_web --profile qwen3-turn \ @@ -193,7 +227,7 @@ python -m examples.online_serving.realtime_web --profile qwen3-turn \ - `--public-realtime-url`: optional browser-visible WebSocket URL; otherwise the static host proxies `/v1/realtime` on the same origin. - `--host`, `--port`: UI bind address and port (default port 7862). -- `--ref-audio`: required for MiniCPM; rejected for Qwen3. +- `--ref-audio`: required for MiniCPM; rejected for Qwen3 and AURA. Microphone and camera access require `localhost` or HTTPS. For a remote backend, use an SSH tunnel to the UI host or an HTTPS reverse proxy with WebSocket support. diff --git a/examples/online_serving/realtime_web/app/index.html b/examples/online_serving/realtime_web/app/index.html index a38a731e4..d4662d5a1 100644 --- a/examples/online_serving/realtime_web/app/index.html +++ b/examples/online_serving/realtime_web/app/index.html @@ -22,6 +22,7 @@