Skip to content

Commit 745aebe

Browse files
Merge pull request #338 from dvonthenen/make-async-like-sync-client
Make AsyncLiveClient Similar to LiveClient
2 parents 2a7eaeb + d02b9ba commit 745aebe

File tree

6 files changed

+269
-125
lines changed

6 files changed

+269
-125
lines changed

deepgram/audio/microphone/microphone.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,10 @@ def __init__(
4040
self.format = pyaudio.paInt16
4141
self.channels = channels
4242
self.input_device_index = input_device_index
43+
self.push_callback_org = push_callback
44+
4345
self.asyncio_loop = None
4446
self.asyncio_thread = None
45-
46-
if inspect.iscoroutinefunction(push_callback):
47-
self.logger.verbose("async/await callback - wrapping")
48-
# Run our own asyncio loop.
49-
self.asyncio_thread = threading.Thread(target=self._start_asyncio_loop)
50-
self.asyncio_thread.start()
51-
52-
self.push_callback = lambda data: asyncio.run_coroutine_threadsafe(
53-
push_callback(data), self.asyncio_loop
54-
).result()
55-
else:
56-
self.logger.verbose("regular threaded callback")
57-
self.push_callback = push_callback
58-
5947
self.stream = None
6048

6149
def _start_asyncio_loop(self) -> None:
@@ -105,6 +93,19 @@ def start(self) -> bool:
10593
stream_callback=self._callback,
10694
)
10795

96+
if inspect.iscoroutinefunction(self.push_callback_org):
97+
self.logger.verbose("async/await callback - wrapping")
98+
# Run our own asyncio loop.
99+
self.asyncio_thread = threading.Thread(target=self._start_asyncio_loop)
100+
self.asyncio_thread.start()
101+
102+
self.push_callback = lambda data: asyncio.run_coroutine_threadsafe(
103+
self.push_callback_org(data), self.asyncio_loop
104+
).result()
105+
else:
106+
self.logger.verbose("regular threaded callback")
107+
self.push_callback = self.push_callback_org
108+
108109
self.exit.clear()
109110
self.stream.start_stream()
110111

@@ -150,16 +151,17 @@ def finish(self) -> bool:
150151
self.logger.notice("signal exit")
151152
self.exit.set()
152153

154+
# Stop the stream.
153155
if self.stream is not None:
154156
self.stream.stop_stream()
155157
self.stream.close()
156158
self.stream = None
157159

160+
# clean up the thread
158161
if self.asyncio_thread is not None:
159162
self.asyncio_loop.call_soon_threadsafe(self.asyncio_loop.stop)
160-
self.asyncio_thread.join() # Clean up.
161-
self.asyncio_thread = None
162-
163+
self.asyncio_thread.join()
164+
self.asyncio_thread = None
163165
self.logger.notice("stream/recv thread joined")
164166

165167
self.logger.notice("finish succeeded")

deepgram/clients/live/v1/async_client.py

Lines changed: 104 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self, config: DeepgramClientOptions):
5151
self._socket = None
5252
self._event_handlers = {event: [] for event in LiveTranscriptionEvents}
5353
self.websocket_url = convert_to_websocket_url(self.config.url, self.endpoint)
54+
self.exit_event = None
5455

5556
# starts the WebSocket connection for live transcription
5657
async def start(
@@ -61,10 +62,10 @@ async def start(
6162
**kwargs,
6263
) -> bool:
6364
self.logger.debug("AsyncLiveClient.start ENTER")
64-
self.logger.info("kwargs: %s", options)
65+
self.logger.info("options: %s", options)
6566
self.logger.info("addons: %s", addons)
6667
self.logger.info("members: %s", members)
67-
self.logger.info("options: %s", kwargs)
68+
self.logger.info("kwargs: %s", kwargs)
6869

6970
if isinstance(options, LiveOptions) and not options.check():
7071
self.logger.error("options.check failed")
@@ -83,7 +84,7 @@ async def start(
8384
if members is not None:
8485
self.__dict__.update(members)
8586

86-
# add "kwargs" as members of the class
87+
# set kwargs as members of the class
8788
if kwargs is not None:
8889
self.kwargs = kwargs
8990
else:
@@ -101,6 +102,8 @@ async def start(
101102
self.logger.debug("combined_options: %s", combined_options)
102103

103104
url_with_params = append_query_params(self.websocket_url, combined_options)
105+
self.exit_event = asyncio.Event()
106+
104107
try:
105108
self._socket = await _socket_connect(url_with_params, self.config.headers)
106109

@@ -135,8 +138,24 @@ async def _emit(self, event: LiveTranscriptionEvents, *args, **kwargs) -> None:
135138
async def _listening(self) -> None:
136139
self.logger.debug("AsyncLiveClient._listening ENTER")
137140

138-
try:
139-
async for message in self._socket:
141+
while True:
142+
try:
143+
if self.exit_event.is_set():
144+
self.logger.notice("_listening exiting gracefully")
145+
self.logger.debug("AsyncLiveClient._listening LEAVE")
146+
return
147+
148+
if self._socket is None:
149+
self.logger.warning("socket is empty")
150+
self.logger.debug("AsyncLiveClient._listening LEAVE")
151+
return
152+
153+
message = await self._socket.recv()
154+
155+
if message is None:
156+
self.logger.spam("message is None")
157+
continue
158+
140159
data = json.loads(message)
141160
response_type = data.get("type")
142161
self.logger.debug("response_type: %s, data: %s", response_type, data)
@@ -206,48 +225,48 @@ async def _listening(self) -> None:
206225
)
207226
await self._emit(LiveTranscriptionEvents.Error, error=error)
208227

209-
except websockets.exceptions.ConnectionClosedOK as e:
210-
self.logger.notice(f"_listening({e.code}) exiting gracefully")
211-
self.logger.debug("AsyncLiveClient._listening LEAVE")
212-
return
213-
214-
except websockets.exceptions.WebSocketException as e:
215-
error: ErrorResponse = {
216-
"type": "Exception",
217-
"description": "WebSocketException in _listening",
218-
"message": f"{e}",
219-
"variant": "",
220-
}
221-
self.logger.notice(
222-
f"WebSocket exception in _listening with code {e.code}: {e.reason}"
223-
)
224-
await self._emit(LiveTranscriptionEvents.Error, error)
225-
226-
self.logger.debug("AsyncLiveClient._listening LEAVE")
227-
228-
if (
229-
"termination_exception" in self.options
230-
and self.options["termination_exception"] == "true"
231-
):
232-
raise
233-
234-
except Exception as e:
235-
error: ErrorResponse = {
236-
"type": "Exception",
237-
"description": "Exception in _listening",
238-
"message": f"{e}",
239-
"variant": "",
240-
}
241-
self.logger.error("Exception in _listening: %s", str(e))
242-
await self._emit(LiveTranscriptionEvents.Error, error)
243-
244-
self.logger.debug("AsyncLiveClient._listening LEAVE")
245-
246-
if (
247-
"termination_exception" in self.options
248-
and self.options["termination_exception"] == "true"
249-
):
250-
raise
228+
except websockets.exceptions.ConnectionClosedOK as e:
229+
self.logger.notice(f"_listening({e.code}) exiting gracefully")
230+
self.logger.debug("AsyncLiveClient._listening LEAVE")
231+
return
232+
233+
except websockets.exceptions.WebSocketException as e:
234+
error: ErrorResponse = {
235+
"type": "Exception",
236+
"description": "WebSocketException in AsyncLiveClient._listening",
237+
"message": f"{e}",
238+
"variant": "",
239+
}
240+
self.logger.notice(
241+
f"WebSocket exception in AsyncLiveClient._listening with code {e.code}: {e.reason}"
242+
)
243+
await self._emit(LiveTranscriptionEvents.Error, error)
244+
245+
self.logger.debug("AsyncLiveClient._listening LEAVE")
246+
247+
if (
248+
"termination_exception" in self.options
249+
and self.options["termination_exception"] == "true"
250+
):
251+
raise
252+
253+
except Exception as e:
254+
error: ErrorResponse = {
255+
"type": "Exception",
256+
"description": "Exception in AsyncLiveClient._listening",
257+
"message": f"{e}",
258+
"variant": "",
259+
}
260+
self.logger.error("Exception in AsyncLiveClient._listening: %s", str(e))
261+
await self._emit(LiveTranscriptionEvents.Error, error)
262+
263+
self.logger.debug("AsyncLiveClient._listening LEAVE")
264+
265+
if (
266+
"termination_exception" in self.options
267+
and self.options["termination_exception"] == "true"
268+
):
269+
raise
251270

252271
# keep the connection alive by sending keepalive messages
253272
async def _keep_alive(self) -> None:
@@ -259,6 +278,11 @@ async def _keep_alive(self) -> None:
259278
counter += 1
260279
await asyncio.sleep(ONE_SECOND)
261280

281+
if self.exit_event.is_set():
282+
self.logger.notice("_keep_alive exiting gracefully")
283+
self.logger.debug("AsyncLiveClient._keep_alive LEAVE")
284+
return
285+
262286
if self._socket is None:
263287
self.logger.notice("socket is None, exiting keep_alive")
264288
self.logger.debug("AsyncLiveClient._keep_alive LEAVE")
@@ -270,25 +294,22 @@ async def _keep_alive(self) -> None:
270294
and self.config.options.get("keepalive") == "true"
271295
):
272296
self.logger.verbose("Sending KeepAlive...")
273-
try:
274-
await self.send(json.dumps({"type": "KeepAlive"}))
275-
except websockets.exceptions.WebSocketException as e:
276-
self.logger.error("KeepAlive failed: %s", e)
297+
await self.send(json.dumps({"type": "KeepAlive"}))
277298

278299
except websockets.exceptions.ConnectionClosedOK as e:
279300
self.logger.notice(f"_keep_alive({e.code}) exiting gracefully")
280301
self.logger.debug("AsyncLiveClient._keep_alive LEAVE")
281302
return
282303

283-
except websockets.exceptions.ConnectionClosedError as e:
304+
except websockets.exceptions.WebSocketException as e:
284305
error: ErrorResponse = {
285306
"type": "Exception",
286-
"description": "ConnectionClosedError in _keep_alive",
307+
"description": "WebSocketException in AsyncLiveClient._keep_alive",
287308
"message": f"{e}",
288309
"variant": "",
289310
}
290311
self.logger.error(
291-
f"WebSocket connection closed in _keep_alive with code {e.code}: {e.reason}"
312+
f"WebSocket connection closed in AsyncLiveClient._keep_alive with code {e.code}: {e.reason}"
292313
)
293314
await self._emit(LiveTranscriptionEvents.Error, error)
294315

@@ -308,8 +329,10 @@ async def _keep_alive(self) -> None:
308329
"message": f"{e}",
309330
"variant": "",
310331
}
332+
self.logger.error(
333+
"Exception in AsyncLiveClient._keep_alive: %s", str(e)
334+
)
311335
await self._emit(LiveTranscriptionEvents.Error, error)
312-
self.logger.error("Exception in _keep_alive: %s", str(e))
313336

314337
self.logger.debug("AsyncLiveClient._keep_alive LEAVE")
315338

@@ -323,31 +346,45 @@ async def _keep_alive(self) -> None:
323346
self.logger.debug("AsyncLiveClient._keep_alive LEAVE")
324347

325348
# sends data over the WebSocket connection
326-
async def send(self, data: Union[str, bytes]) -> int:
349+
async def send(self, data: Union[str, bytes]) -> bool:
327350
"""
328351
Sends data over the WebSocket connection.
329352
"""
330353
self.logger.spam("AsyncLiveClient.send ENTER")
331-
self.logger.spam("data: %s", data)
332354

333355
if self._socket is not None:
334-
cnt = await self._socket.send(data)
335-
self.logger.spam(f"send() succeeded. bytes: {cnt}")
356+
try:
357+
await self._socket.send(data)
358+
except websockets.exceptions.WebSocketException as e:
359+
self.logger.error("send() failed - WebSocketException: %s", str(e))
360+
self.logger.spam("AsyncLiveClient.send LEAVE")
361+
return False
362+
except Exception as e:
363+
self.logger.error("send() failed - Exception: %s", str(e))
364+
self.logger.spam("AsyncLiveClient.send LEAVE")
365+
return False
366+
367+
self.logger.spam(f"send() succeeded")
336368
self.logger.spam("AsyncLiveClient.send LEAVE")
337-
return cnt
369+
return True
338370

339371
self.logger.error("send() failed. socket is None")
340372
self.logger.spam("AsyncLiveClient.send LEAVE")
341-
return 0
373+
return False
342374

343375
async def finish(self) -> bool:
344376
"""
345377
Closes the WebSocket connection gracefully.
346378
"""
347379
self.logger.debug("AsyncLiveClient.finish ENTER")
348380

349-
if self._socket:
350-
self.logger.notice("send CloseStream...")
381+
# signal exit
382+
self.exit_event.set()
383+
384+
# close the stream
385+
self.logger.verbose("closing socket...")
386+
if self._socket is not None:
387+
self.logger.verbose("send CloseStream...")
351388
await self._socket.send(json.dumps({"type": "CloseStream"}))
352389

353390
await asyncio.sleep(0.5)
@@ -358,14 +395,14 @@ async def finish(self) -> bool:
358395
CloseResponse(type=LiveTranscriptionEvents.Close.value),
359396
)
360397

361-
self.logger.notice("socket.wait_closed...")
398+
self.logger.verbose("socket.wait_closed...")
362399
try:
363400
await self._socket.wait_closed()
364401
except websockets.exceptions.WebSocketException as e:
365402
self.logger.error("socket.wait_closed failed: %s", e)
366-
self.logger.notice("socket.wait_closed succeeded")
403+
self._socket = None
367404

368-
self.logger.notice("cancelling tasks...")
405+
self.logger.verbose("cancelling tasks...")
369406
try:
370407
# Before cancelling, check if the tasks were created
371408
if self._listen_thread is not None:
@@ -380,13 +417,7 @@ async def finish(self) -> bool:
380417
except asyncio.CancelledError as e:
381418
self.logger.error("tasks cancelled error: %s", e)
382419

383-
if self._socket is not None:
384-
self.logger.notice("closing socket...")
385-
await self._socket.close()
386-
387-
self._socket = None
388-
389-
self.logger.notice("finish succeeded")
420+
self.logger.info("finish succeeded")
390421
self.logger.debug("AsyncLiveClient.finish LEAVE")
391422
return True
392423

0 commit comments

Comments
 (0)