main
py 145 lines 4.66 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """This script demonstrates how to use the vLLM Realtime WebSocket API to perform
4 audio transcription by uploading an audio file.
5
6 Before running this script, you must start the vLLM server with a realtime-capable
7 model, for example:
8
9 vllm serve mistralai/Voxtral-Mini-4B-Realtime-2602 --enforce-eager
10
11 Requirements:
12 - vllm with audio support
13 - websockets
14 - numpy
15
16 The script:
17 1. Connects to the Realtime WebSocket endpoint
18 2. Converts an audio file to PCM16 @ 16kHz
19 3. Sends audio chunks to the server
20 4. Receives and prints transcription as it streams
21 """
22
23 import argparse
24 import asyncio
25 import json
26
27 import numpy as np
28 import pybase64 as base64
29 import websockets
30
31 from vllm.assets.audio import AudioAsset
32 from vllm.multimodal.media.audio import load_audio
33
34
35 def audio_to_pcm16_base64(audio_path: str) -> str:
36 """Load an audio file and convert it to base64-encoded PCM16 @ 16kHz."""
37 # Load audio and resample to 16kHz mono
38 audio, _ = load_audio(audio_path, sr=16000, mono=True)
39 # Convert to PCM16
40 pcm16 = (audio * 32767).astype(np.int16)
41 # Encode as base64
42 return base64.b64encode(pcm16.tobytes()).decode("utf-8")
43
44
45 async def realtime_transcribe(audio_path: str, host: str, port: int, model: str):
46 """Connect to the Realtime API and transcribe an audio file."""
47 uri = f"ws://{host}:{port}/v1/realtime"
48
49 async with websockets.connect(uri) as ws:
50 # Wait for session.created
51 response = json.loads(await ws.recv())
52 if response["type"] == "session.created":
53 print(f"Session created: {response['id']}")
54 else:
55 print(f"Unexpected response: {response}")
56 return
57
58 # Validate model
59 await ws.send(json.dumps({"type": "session.update", "model": model}))
60
61 # Signal ready to start
62 await ws.send(json.dumps({"type": "input_audio_buffer.commit"}))
63
64 # Convert audio file to base64 PCM16
65 print(f"Loading audio from: {audio_path}")
66 audio_base64 = audio_to_pcm16_base64(audio_path)
67
68 # Send audio in chunks (4KB of raw audio = ~8KB base64)
69 chunk_size = 4096
70 audio_bytes = base64.b64decode(audio_base64)
71 total_chunks = (len(audio_bytes) + chunk_size - 1) // chunk_size
72
73 print(f"Sending {total_chunks} audio chunks...")
74 for i in range(0, len(audio_bytes), chunk_size):
75 chunk = audio_bytes[i : i + chunk_size]
76 await ws.send(
77 json.dumps(
78 {
79 "type": "input_audio_buffer.append",
80 "audio": base64.b64encode(chunk).decode("utf-8"),
81 }
82 )
83 )
84
85 # Signal all audio is sent
86 await ws.send(json.dumps({"type": "input_audio_buffer.commit", "final": True}))
87 print("Audio sent. Waiting for transcription...\n")
88
89 # Receive transcription
90 print("Transcription: ", end="", flush=True)
91 while True:
92 response = json.loads(await ws.recv())
93 if response["type"] == "transcription.delta":
94 print(response["delta"], end="", flush=True)
95 elif response["type"] == "transcription.done":
96 print(f"\n\nFinal transcription: {response['text']}")
97 if response.get("usage"):
98 print(f"Usage: {response['usage']}")
99 break
100 elif response["type"] == "error":
101 print(f"\nError: {response['error']}")
102 break
103
104
105 def main(args):
106 if args.audio_path:
107 audio_path = args.audio_path
108 else:
109 # Use default audio asset
110 audio_path = str(AudioAsset("mary_had_lamb").get_local_path())
111 print(f"No audio path provided, using default: {audio_path}")
112
113 asyncio.run(realtime_transcribe(audio_path, args.host, args.port, args.model))
114
115
116 if __name__ == "__main__":
117 parser = argparse.ArgumentParser(
118 description="Realtime WebSocket Transcription Client"
119 )
120 parser.add_argument(
121 "--model",
122 type=str,
123 default="mistralai/Voxtral-Mini-4B-Realtime-2602",
124 help="Model that is served and should be pinged.",
125 )
126 parser.add_argument(
127 "--audio_path",
128 type=str,
129 default=None,
130 help="Path to the audio file to transcribe.",
131 )
132 parser.add_argument(
133 "--host",
134 type=str,
135 default="localhost",
136 help="vLLM server host (default: localhost)",
137 )
138 parser.add_argument(
139 "--port",
140 type=int,
141 default=8000,
142 help="vLLM server port (default: 8000)",
143 )
144 args = parser.parse_args()
145 main(args)