Skip to content

Commit 585fb73

Browse files
Wyoming fix (#39)
* update laptop client to use wyoming * fix wyoming laptop client * Fix InputMicStream usgage
1 parent 5778832 commit 585fb73

1 file changed

Lines changed: 78 additions & 16 deletions

File tree

backends/advanced-backend/scripts/laptop_client.py

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import websockets
88
import websockets.exceptions
99
from easy_audio_interfaces.extras.local_audio import InputMicStream
10+
from wyoming.audio import AudioStart, AudioChunk, AudioStop
1011

1112
logger = logging.getLogger(__name__)
1213
logging.basicConfig(level=logging.INFO)
@@ -16,6 +17,8 @@
1617
DEFAULT_PORT = 8000
1718
DEFAULT_ENDPOINT = "/ws_pcm"
1819

20+
# Audio format will be determined from the InputMicStream instance
21+
1922

2023
def build_websocket_uri(
2124
host: str, port: int, endpoint: str, token: str | None = None, device_name: str = "laptop"
@@ -91,6 +94,39 @@ def validate_auth_args(args):
9194
)
9295

9396

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+
94130
async def main():
95131
# Parse command line arguments
96132
parser = argparse.ArgumentParser(
@@ -149,22 +185,48 @@ async def main():
149185
print("Connected to WebSocket")
150186

151187
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}")
168230

169231
async def receive_messages():
170232
"""Receive any messages from the WebSocket server"""

0 commit comments

Comments
 (0)