Skip to content

Commit bdb33d6

Browse files
committed
bug fixes and QoL improvements
1 parent 2d39717 commit bdb33d6

5 files changed

Lines changed: 44 additions & 45 deletions

File tree

src/elevenlabs/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class _AsyncSpeechEngineAccessor:
7373
def __init__(self, client_wrapper: typing.Any) -> None:
7474
self._client_wrapper = client_wrapper
7575

76-
async def get(self, engine_id: str) -> "SpeechEngineResource":
76+
async def get(self, engine_id: str) -> typing.Any:
7777
from .speech_engine.resource import SpeechEngineResource # noqa: E402
7878

7979
return SpeechEngineResource(

src/elevenlabs/speech_engine/server.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
"""SpeechEngineServer — standalone WebSocket server for Speech Engine."""
22

33
import asyncio
4-
import logging
54
import os
65
import typing
76

8-
from .session import SpeechEngineSession, _wire_handlers
7+
from .session import SpeechEngineSession, _make_log, _wire_handlers
98
from .types import WebSocketLike
109

11-
logger = logging.getLogger("elevenlabs.speech_engine")
12-
1310

1411
class SpeechEngineServer:
1512
"""Standalone WebSocket server that produces :class:`SpeechEngineSession`
@@ -44,17 +41,9 @@ def __init__(
4441
self._api_key = api_key
4542
self._debug = debug
4643
self._handlers = handlers
47-
self._stop_event = asyncio.Event()
44+
self._stop_event = None # type: typing.Optional[asyncio.Event]
4845
self._server = None # type: typing.Any
49-
50-
if debug:
51-
logger.setLevel(logging.DEBUG)
52-
if not logger.handlers:
53-
handler = logging.StreamHandler()
54-
handler.setFormatter(
55-
logging.Formatter("[SpeechEngine] %(message)s")
56-
)
57-
logger.addHandler(handler)
46+
self._log = _make_log(debug)
5847

5948
def handle_connection(self, ws: WebSocketLike) -> SpeechEngineSession:
6049
"""Wrap *ws* in a :class:`SpeechEngineSession` with the server's
@@ -64,7 +53,7 @@ def handle_connection(self, ws: WebSocketLike) -> SpeechEngineSession:
6453
individual connections. The returned session's :meth:`run` must
6554
still be awaited by the caller.
6655
"""
67-
logger.debug("creating new session")
56+
self._log("creating new session")
6857
session = SpeechEngineSession(ws, debug=self._debug)
6958
_wire_handlers(session, self._handlers)
7059
return session
@@ -83,6 +72,8 @@ async def serve(self) -> None:
8372
"environment variable."
8473
)
8574

75+
self._stop_event = asyncio.Event()
76+
8677
async def _handler(websocket: typing.Any, *_args: typing.Any) -> None:
8778
if self._path is not None and websocket.request.path != self._path:
8879
await websocket.close(4000, "not found")
@@ -92,7 +83,7 @@ async def _handler(websocket: typing.Any, *_args: typing.Any) -> None:
9283
"x-elevenlabs-speech-engine-authorization"
9384
)
9485
if not header_value:
95-
logger.debug(
86+
self._log(
9687
"rejected connection — missing "
9788
"X-Elevenlabs-Speech-Engine-Authorization header"
9889
)
@@ -104,11 +95,11 @@ async def _handler(websocket: typing.Any, *_args: typing.Any) -> None:
10495
try:
10596
verify_speech_engine_jwt(header_value, api_key)
10697
except ValueError as e:
107-
logger.debug("rejected connection — %s", e)
98+
self._log("rejected connection — %s", e)
10899
await websocket.close(4001, str(e))
109100
return
110101

111-
logger.debug("verified connection, accepting WebSocket")
102+
self._log("verified connection, accepting WebSocket")
112103
session = self.handle_connection(websocket)
113104
await session.run()
114105

@@ -117,7 +108,7 @@ async def _handler(websocket: typing.Any, *_args: typing.Any) -> None:
117108
"",
118109
self._port,
119110
)
120-
logger.debug("speech engine server listening on port %d", self._port)
111+
self._log("speech engine server listening on port %d", self._port)
121112
try:
122113
await self._stop_event.wait()
123114
finally:
@@ -126,4 +117,5 @@ async def _handler(websocket: typing.Any, *_args: typing.Any) -> None:
126117

127118
async def stop(self) -> None:
128119
"""Signal the server to shut down gracefully."""
129-
self._stop_event.set()
120+
if self._stop_event is not None:
121+
self._stop_event.set()

src/elevenlabs/speech_engine/session.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99

1010
logger = logging.getLogger("elevenlabs.speech_engine")
1111

12+
13+
def _make_log(
14+
debug: bool,
15+
) -> typing.Callable[..., None]:
16+
"""Return a per-instance log function, mirroring the JS SDK pattern."""
17+
if debug:
18+
def _log(msg: str, *args: typing.Any) -> None:
19+
print("[SpeechEngine]", msg % args if args else msg)
20+
return _log
21+
return lambda *_args, **_kw: None
22+
1223
Callback = typing.Callable[..., typing.Any]
1324

1425

@@ -170,15 +181,7 @@ def __init__(
170181
self._closed = False
171182
self._event_handlers = {} # type: typing.Dict[str, typing.List[Callback]]
172183
self._once_handlers = {} # type: typing.Dict[str, typing.List[Callback]]
173-
174-
if debug:
175-
logger.setLevel(logging.DEBUG)
176-
if not logger.handlers:
177-
handler = logging.StreamHandler()
178-
handler.setFormatter(
179-
logging.Formatter("[SpeechEngine] %(message)s")
180-
)
181-
logger.addHandler(handler)
184+
self._log = _make_log(debug)
182185

183186
# ------------------------------------------------------------------
184187
# Event emitter interface
@@ -238,7 +241,7 @@ async def run(self) -> None:
238241
except asyncio.CancelledError:
239242
raise
240243
except Exception:
241-
logger.debug("WebSocket connection lost")
244+
self._log("WebSocket connection lost")
242245
break
243246

244247
try:
@@ -290,15 +293,15 @@ async def send_response(
290293
return
291294

292295
if isinstance(response, str):
293-
logger.debug(
296+
self._log(
294297
'sending string response: "%s", event_id=%s',
295298
response,
296299
self._current_event_id,
297300
)
298301
await self._send_agent_response(response, False)
299302
await self._send_agent_response("", True)
300303
else:
301-
logger.debug(
304+
self._log(
302305
"starting streamed response, event_id=%s",
303306
self._current_event_id,
304307
)
@@ -325,7 +328,7 @@ async def _handle_message(self, msg: typing.Dict[str, typing.Any]) -> None:
325328

326329
if msg_type == "init":
327330
self._conversation_id = msg.get("conversation_id")
328-
logger.debug(
331+
self._log(
329332
"session initialized, conversation_id=%s",
330333
self._conversation_id,
331334
)
@@ -339,7 +342,7 @@ async def _handle_message(self, msg: typing.Dict[str, typing.Any]) -> None:
339342
and self._current_task is not None
340343
and not self._current_task.done()
341344
):
342-
logger.debug(
345+
self._log(
343346
"skipping duplicate transcript, event_id=%s",
344347
incoming_event_id,
345348
)
@@ -351,7 +354,7 @@ async def _handle_message(self, msg: typing.Dict[str, typing.Any]) -> None:
351354
)
352355
await self._cancel_current_and_wait()
353356
if was_active:
354-
logger.debug(
357+
self._log(
355358
"interrupted: cancelling previous response "
356359
"(event_id=%s) for new transcript (event_id=%s)",
357360
self._current_event_id,
@@ -360,16 +363,20 @@ async def _handle_message(self, msg: typing.Dict[str, typing.Any]) -> None:
360363

361364
self._current_event_id = incoming_event_id
362365
transcript_data = msg.get("user_transcript", [])
363-
logger.debug(
366+
self._log(
364367
"received transcript, event_id=%s, messages=%d",
365368
self._current_event_id,
366369
len(transcript_data),
367370
)
368371

369-
transcript = [
370-
ConversationMessage(role=m["role"], content=m["content"])
371-
for m in transcript_data
372-
]
372+
try:
373+
transcript = [
374+
ConversationMessage(role=m["role"], content=m["content"])
375+
for m in transcript_data
376+
]
377+
except (KeyError, TypeError) as e:
378+
await self._emit("error", e)
379+
return
373380

374381
handlers = list(
375382
self._event_handlers.get("user_transcript", [])
@@ -419,7 +426,7 @@ async def _stream_response(self, stream: typing.Any) -> None:
419426
try:
420427
async for chunk in stream:
421428
if self._closed:
422-
logger.debug(
429+
self._log(
423430
"stream stopped: session closed after %d chunks, "
424431
"event_id=%s",
425432
chunks,
@@ -431,7 +438,7 @@ async def _stream_response(self, stream: typing.Any) -> None:
431438
chunks += 1
432439
await self._send_agent_response(text, False, event_id)
433440
if not self._closed:
434-
logger.debug(
441+
self._log(
435442
"stream complete: %d chunks sent, event_id=%s",
436443
chunks,
437444
event_id,

tests/test_speech_engine_resource.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
class MockWebSocket:
2020
def __init__(self) -> None:
21-
self._inbox = asyncio.Queue() # type: asyncio.Queue[str]
21+
self._inbox = asyncio.Queue() # type: asyncio.Queue[typing.Any]
2222
self.sent = [] # type: typing.List[str]
2323
self.closed = False
2424

tests/test_speech_engine_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class MockWebSocket:
2626
"""In-memory WebSocket stand-in backed by an asyncio.Queue."""
2727

2828
def __init__(self) -> None:
29-
self._inbox = asyncio.Queue() # type: asyncio.Queue[str]
29+
self._inbox = asyncio.Queue() # type: asyncio.Queue[typing.Any]
3030
self.sent = [] # type: typing.List[str]
3131
self.closed = False
3232

0 commit comments

Comments
 (0)