154 lines
5.0 KiB
Python
Executable File
154 lines
5.0 KiB
Python
Executable File
#!/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())
|