|
7 | 7 | import websockets |
8 | 8 | import websockets.exceptions |
9 | 9 | from easy_audio_interfaces.extras.local_audio import InputMicStream |
| 10 | +from wyoming.audio import AudioStart, AudioChunk, AudioStop |
10 | 11 |
|
11 | 12 | logger = logging.getLogger(__name__) |
12 | 13 | logging.basicConfig(level=logging.INFO) |
|
16 | 17 | DEFAULT_PORT = 8000 |
17 | 18 | DEFAULT_ENDPOINT = "/ws_pcm" |
18 | 19 |
|
| 20 | +# Audio format will be determined from the InputMicStream instance |
| 21 | + |
19 | 22 |
|
20 | 23 | def build_websocket_uri( |
21 | 24 | host: str, port: int, endpoint: str, token: str | None = None, device_name: str = "laptop" |
@@ -91,6 +94,39 @@ def validate_auth_args(args): |
91 | 94 | ) |
92 | 95 |
|
93 | 96 |
|
| 97 | +async def send_wyoming_event(websocket, wyoming_event): |
| 98 | + """Send a Wyoming protocol event over WebSocket. |
| 99 | + |
| 100 | + Based on how the backend processes Wyoming events, they expect: |
| 101 | + 1. JSON header line ending with \n |
| 102 | + 2. Optional binary payload if payload_length > 0 |
| 103 | + |
| 104 | + This replicates Wyoming's async_write_event behavior for WebSocket transport. |
| 105 | + """ |
| 106 | + # Get the event data from Wyoming event |
| 107 | + event_data = wyoming_event.event() |
| 108 | + |
| 109 | + # Build event dict like Wyoming's async_write_event does |
| 110 | + event_dict = event_data.to_dict() |
| 111 | + event_dict["version"] = "1.0.0" # Wyoming adds version |
| 112 | + |
| 113 | + # Add payload_length if payload exists (critical for audio chunks!) |
| 114 | + if event_data.payload: |
| 115 | + event_dict["payload_length"] = len(event_data.payload) |
| 116 | + |
| 117 | + # Send JSON header |
| 118 | + json_header = json.dumps(event_dict) + '\n' |
| 119 | + await websocket.send(json_header) |
| 120 | + logger.debug(f"Sent Wyoming event: {event_data.type} (payload_length: {event_dict.get('payload_length', 0)})") |
| 121 | + |
| 122 | + # Send binary payload if exists |
| 123 | + if event_data.payload: |
| 124 | + await websocket.send(event_data.payload) |
| 125 | + logger.debug(f"Sent audio payload: {len(event_data.payload)} bytes") |
| 126 | + |
| 127 | + |
| 128 | + |
| 129 | + |
94 | 130 | async def main(): |
95 | 131 | # Parse command line arguments |
96 | 132 | parser = argparse.ArgumentParser( |
@@ -149,22 +185,48 @@ async def main(): |
149 | 185 | print("Connected to WebSocket") |
150 | 186 |
|
151 | 187 | async def send_audio(): |
152 | | - """Capture audio from microphone and send raw PCM bytes over WebSocket""" |
153 | | - async with InputMicStream(chunk_size=512) as stream: |
154 | | - while True: |
155 | | - try: |
156 | | - data = await stream.read() |
157 | | - if data and data.audio: |
158 | | - # Send raw PCM bytes directly to WebSocket |
159 | | - await websocket.send(data.audio) |
160 | | - logger.debug(f"Sent audio chunk: {len(data.audio)} bytes") |
161 | | - await asyncio.sleep(0.01) # Small delay to prevent overwhelming |
162 | | - except websockets.exceptions.ConnectionClosed: |
163 | | - logger.info("WebSocket connection closed during audio sending") |
164 | | - break |
165 | | - except Exception as e: |
166 | | - logger.error(f"Error sending audio: {e}") |
167 | | - break |
| 188 | + """Capture audio from microphone and send Wyoming protocol audio events""" |
| 189 | + try: |
| 190 | + # Send audio-start event to begin session |
| 191 | + async with InputMicStream(chunk_size=512) as stream: |
| 192 | + audio_start = AudioStart( |
| 193 | + rate=stream.sample_rate, |
| 194 | + width=stream.sample_width, |
| 195 | + channels=stream.channels |
| 196 | + ) |
| 197 | + await send_wyoming_event(websocket, audio_start) |
| 198 | + logger.info(f"Sent audio-start event (rate={stream.sample_rate}, width={stream.sample_width}, channels={stream.channels})") |
| 199 | + while True: |
| 200 | + try: |
| 201 | + data = await stream.read() |
| 202 | + if data and data.audio: |
| 203 | + # Create Wyoming AudioChunk with format from stream |
| 204 | + audio_chunk = AudioChunk( |
| 205 | + audio=data.audio, |
| 206 | + rate=stream.sample_rate, |
| 207 | + width=stream.sample_width, |
| 208 | + channels=stream.channels |
| 209 | + ) |
| 210 | + await send_wyoming_event(websocket, audio_chunk) |
| 211 | + logger.debug(f"Sent audio chunk: {len(data.audio)} bytes") |
| 212 | + await asyncio.sleep(0.01) # Small delay to prevent overwhelming |
| 213 | + except websockets.exceptions.ConnectionClosed: |
| 214 | + logger.info("WebSocket connection closed during audio sending") |
| 215 | + break |
| 216 | + except Exception as e: |
| 217 | + logger.error(f"Error sending audio: {e}") |
| 218 | + break |
| 219 | + |
| 220 | + except Exception as e: |
| 221 | + logger.error(f"Error in audio session: {e}") |
| 222 | + finally: |
| 223 | + # Send audio-stop event to end session |
| 224 | + try: |
| 225 | + audio_stop = AudioStop() |
| 226 | + await send_wyoming_event(websocket, audio_stop) |
| 227 | + logger.info("Sent audio-stop event") |
| 228 | + except Exception as e: |
| 229 | + logger.error(f"Error sending audio-stop: {e}") |
168 | 230 |
|
169 | 231 | async def receive_messages(): |
170 | 232 | """Receive any messages from the WebSocket server""" |
|
0 commit comments