From 54eb21ec7eebd0bb71cc188d5b9a143f078d381a Mon Sep 17 00:00:00 2001 From: HughhhhCoder Date: Tue, 8 Sep 2026 12:38:03 +0800 Subject: [PATCH] fix(helpers): propagate audio stream producer errors --- src/openai/helpers/local_audio_player.py | 33 +++++++++++++++------- tests/test_local_audio_player.py | 35 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 tests/test_local_audio_player.py diff --git a/src/openai/helpers/local_audio_player.py b/src/openai/helpers/local_audio_player.py index 8f12c27a56..59214fdc84 100644 --- a/src/openai/helpers/local_audio_player.py +++ b/src/openai/helpers/local_audio_player.py @@ -153,13 +153,26 @@ def callback( buffer_pos = 0 producer_task = asyncio.create_task(buffer_producer()) - - with sd.OutputStream( - samplerate=SAMPLE_RATE, - channels=self.channels, - dtype=self.dtype, - callback=callback, - ): - await event.wait() - - await producer_task + playback_task = asyncio.create_task(event.wait()) + + try: + with sd.OutputStream( + samplerate=SAMPLE_RATE, + channels=self.channels, + dtype=self.dtype, + callback=callback, + ): + done, _ = await asyncio.wait( + (producer_task, playback_task), + return_when=asyncio.FIRST_COMPLETED, + ) + if producer_task in done: + producer_task.result() + await playback_task + + await producer_task + finally: + for task in (producer_task, playback_task): + if not task.done(): + task.cancel() + await asyncio.gather(producer_task, playback_task, return_exceptions=True) diff --git a/tests/test_local_audio_player.py b/tests/test_local_audio_player.py new file mode 100644 index 0000000000..ee528642ae --- /dev/null +++ b/tests/test_local_audio_player.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import asyncio +from typing import Any +from collections.abc import AsyncGenerator + +import pytest + +from openai.helpers import local_audio_player + + +class SilentOutputStream: + def __init__(self, **kwargs: Any) -> None: + pass + + def __enter__(self) -> SilentOutputStream: + return self + + def __exit__(self, *args: Any) -> None: + pass + + +async def test_play_stream_propagates_producer_failure(monkeypatch: pytest.MonkeyPatch) -> None: + async def broken_stream() -> AsyncGenerator[None, None]: + if asyncio.current_task() is None: + yield None + raise RuntimeError("synthetic producer failure") + + monkeypatch.setattr(local_audio_player.sd, "OutputStream", SilentOutputStream) + + with pytest.raises(RuntimeError, match="synthetic producer failure"): + await asyncio.wait_for( + local_audio_player.LocalAudioPlayer().play_stream(broken_stream()), + timeout=1, + )