Files
vllm-omni/examples/online_serving/joyvl_interaction/bridges/asr_bridge.py
T
e241005a71 [Feature] JoyAI-VL-Interaction streaming interaction serving layer (#4575)
Signed-off-by: lishunyang12 <125541396+lishunyang12@users.noreply.github.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Signed-off-by: lishunyang <lishunyang12@163.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
2026-06-21 07:42:51 +00:00

132 lines
4.2 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import argparse
import base64
import io
import json
import struct
import wave
import aiohttp
from aiohttp import web
_HEADER = struct.Struct(">iii")
_SAMPLE_RATE = 16000
_DEFAULT_PROMPT = "Transcribe the audio."
_ASR_TEXT_TAG = "<asr_text>"
def _clean_transcript(raw: str) -> str:
"""Qwen3-ASR emits ``language <Lang><asr_text><transcript>``; keep the transcript."""
text = raw or ""
marker = text.rfind(_ASR_TEXT_TAG)
if marker != -1:
text = text[marker + len(_ASR_TEXT_TAG) :]
return text.replace("</asr_text>", "").strip()
def _pcm_to_wav_b64(pcm: bytes, sample_rate: int) -> str:
buf = io.BytesIO()
with wave.open(buf, "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(sample_rate)
wav.writeframes(pcm)
return base64.b64encode(buf.getvalue()).decode()
def _result(text: str, *, final: bool) -> dict:
return {
"asr_response": {
"event_type": "IS_FINAL" if final else "IS_PARTIAL",
"recognition_result": {"hypothesis": [{"text": text, "confidence": None}]},
},
"mid": "",
"code": 0,
"msg": "ok",
}
async def _transcribe(cfg: dict, pcm: bytes) -> str:
if not pcm:
return ""
wav_b64 = _pcm_to_wav_b64(pcm, cfg["sample_rate"])
body = {
"model": cfg["model"],
"messages": [
{
"role": "user",
"content": [
{"type": "input_audio", "input_audio": {"data": wav_b64, "format": "wav"}},
{"type": "text", "text": cfg["prompt"]},
],
}
],
"temperature": 0.0,
"max_tokens": cfg["max_tokens"],
}
url = cfg["backend_url"].rstrip("/") + "/v1/chat/completions"
async with aiohttp.ClientSession() as session:
async with session.post(url, json=body) as resp:
payload = await resp.json()
choices = payload.get("choices") or []
if not choices:
return ""
return _clean_transcript(choices[0].get("message", {}).get("content") or "")
async def _handle(request: web.Request) -> web.WebSocketResponse:
ws = web.WebSocketResponse(max_msg_size=0)
await ws.prepare(request)
cfg = request.app["cfg"]
pcm = bytearray()
try:
async for msg in ws:
if msg.type == aiohttp.WSMsgType.BINARY and len(msg.data) >= _HEADER.size:
seqid, _, _ = _HEADER.unpack(msg.data[: _HEADER.size])
pcm.extend(msg.data[_HEADER.size :])
if seqid < 0:
text = await _transcribe(cfg, bytes(pcm))
await ws.send_str(json.dumps(_result(text, final=True), ensure_ascii=False))
pcm.clear()
elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
break
finally:
if not ws.closed:
await ws.close()
return ws
def create_app(backend_url: str, model: str, sample_rate: int, prompt: str, max_tokens: int) -> web.Application:
app = web.Application()
app["cfg"] = {
"backend_url": backend_url,
"model": model,
"sample_rate": sample_rate,
"prompt": prompt,
"max_tokens": max_tokens,
}
app.router.add_get("/v1/asr", _handle)
return app
def main() -> None:
parser = argparse.ArgumentParser(description="JoyVL webui <-> Qwen3-ASR (chat input_audio) bridge")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8093)
parser.add_argument("--backend-url", default="http://127.0.0.1:8094")
parser.add_argument("--model", default="Qwen/Qwen3-ASR-1.7B")
parser.add_argument("--sample-rate", type=int, default=_SAMPLE_RATE)
parser.add_argument("--prompt", default=_DEFAULT_PROMPT)
parser.add_argument("--max-tokens", type=int, default=256)
args = parser.parse_args()
app = create_app(args.backend_url, args.model, args.sample_rate, args.prompt, args.max_tokens)
web.run_app(app, host=args.host, port=args.port)
if __name__ == "__main__":
main()