Skip to content

Commit 54eb21e

Browse files
committed
fix(helpers): propagate audio stream producer errors
1 parent be92815 commit 54eb21e

2 files changed

Lines changed: 58 additions & 10 deletions

File tree

src/openai/helpers/local_audio_player.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,26 @@ def callback(
153153
buffer_pos = 0
154154

155155
producer_task = asyncio.create_task(buffer_producer())
156-
157-
with sd.OutputStream(
158-
samplerate=SAMPLE_RATE,
159-
channels=self.channels,
160-
dtype=self.dtype,
161-
callback=callback,
162-
):
163-
await event.wait()
164-
165-
await producer_task
156+
playback_task = asyncio.create_task(event.wait())
157+
158+
try:
159+
with sd.OutputStream(
160+
samplerate=SAMPLE_RATE,
161+
channels=self.channels,
162+
dtype=self.dtype,
163+
callback=callback,
164+
):
165+
done, _ = await asyncio.wait(
166+
(producer_task, playback_task),
167+
return_when=asyncio.FIRST_COMPLETED,
168+
)
169+
if producer_task in done:
170+
producer_task.result()
171+
await playback_task
172+
173+
await producer_task
174+
finally:
175+
for task in (producer_task, playback_task):
176+
if not task.done():
177+
task.cancel()
178+
await asyncio.gather(producer_task, playback_task, return_exceptions=True)

tests/test_local_audio_player.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from typing import Any
5+
from collections.abc import AsyncGenerator
6+
7+
import pytest
8+
9+
from openai.helpers import local_audio_player
10+
11+
12+
class SilentOutputStream:
13+
def __init__(self, **kwargs: Any) -> None:
14+
pass
15+
16+
def __enter__(self) -> SilentOutputStream:
17+
return self
18+
19+
def __exit__(self, *args: Any) -> None:
20+
pass
21+
22+
23+
async def test_play_stream_propagates_producer_failure(monkeypatch: pytest.MonkeyPatch) -> None:
24+
async def broken_stream() -> AsyncGenerator[None, None]:
25+
if asyncio.current_task() is None:
26+
yield None
27+
raise RuntimeError("synthetic producer failure")
28+
29+
monkeypatch.setattr(local_audio_player.sd, "OutputStream", SilentOutputStream)
30+
31+
with pytest.raises(RuntimeError, match="synthetic producer failure"):
32+
await asyncio.wait_for(
33+
local_audio_player.LocalAudioPlayer().play_stream(broken_stream()),
34+
timeout=1,
35+
)

0 commit comments

Comments
 (0)