From f92aea11386d2b8b589da15688c66b20b0d61f64 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:50:31 -0700 Subject: [PATCH 1/2] FIX: Avoid blocking WAV reads in realtime target Move WAV loading to asyncio.to_thread so realtime audio sends do not block the event loop. Add coverage for the offloaded read and preserved payload metadata. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c85c49c-2d6c-4050-b3e3-ce085c97c5ec --- .../openai/openai_realtime_target.py | 19 +++++++----- .../target/test_realtime_target.py | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/pyrit/prompt_target/openai/openai_realtime_target.py b/pyrit/prompt_target/openai/openai_realtime_target.py index 3894ddc623..3f612ce511 100644 --- a/pyrit/prompt_target/openai/openai_realtime_target.py +++ b/pyrit/prompt_target/openai/openai_realtime_target.py @@ -837,14 +837,7 @@ async def send_audio_async( """ connection = self._get_connection(conversation_id=conversation_id) - with wave.open(filename, "rb") as wav_file: - # Read WAV parameters - num_channels = wav_file.getnchannels() - sample_width = wav_file.getsampwidth() # Should be 2 bytes for PCM16 - frame_rate = wav_file.getframerate() - num_frames = wav_file.getnframes() - - audio_content = wav_file.readframes(num_frames) + audio_content, num_channels, sample_width, frame_rate = await asyncio.to_thread(self._read_wav_file, filename) receive_tasks = asyncio.create_task(self.receive_events_async(conversation_id=conversation_id)) @@ -883,3 +876,13 @@ async def _construct_message_from_response_async(self, response: Any, request: A This implementation exists to satisfy the abstract base class requirement. """ raise NotImplementedError("RealtimeTarget uses receive_events for message construction") + + @staticmethod + def _read_wav_file(filename: str) -> tuple[bytes, int, int, int]: + with wave.open(filename, "rb") as wav_file: + return ( + wav_file.readframes(wav_file.getnframes()), + wav_file.getnchannels(), + wav_file.getsampwidth(), + wav_file.getframerate(), + ) diff --git a/tests/unit/prompt_target/target/test_realtime_target.py b/tests/unit/prompt_target/target/test_realtime_target.py index 6a4959e76d..61913c18a4 100644 --- a/tests/unit/prompt_target/target/test_realtime_target.py +++ b/tests/unit/prompt_target/target/test_realtime_target.py @@ -903,6 +903,36 @@ def _write_wav( return str(path) +async def test_send_audio_async_reads_wav_off_event_loop(target, tmp_path): + connection = AsyncMock() + target._existing_conversation["conv"] = connection + target.receive_events_async = AsyncMock( + return_value=RealtimeTargetResult(audio_bytes=b"response", transcripts=["transcript"]) + ) + target.send_response_create_async = AsyncMock() + target.save_audio_async = AsyncMock(return_value="output.wav") + + pcm = b"\x01\x02" * 8 + wav_path = _write_wav(tmp_path / "input.wav", pcm=pcm) + with patch( + "pyrit.prompt_target.openai.openai_realtime_target.asyncio.to_thread", + new_callable=AsyncMock, + wraps=asyncio.to_thread, + ) as to_thread_mock: + output_path, _ = await target.send_audio_async(filename=wav_path, conversation_id="conv") + + assert output_path == "output.wav" + assert to_thread_mock.await_args.args[1] == wav_path + connection.conversation.item.create.assert_awaited_once_with( + item={ + "type": "message", + "role": "user", + "content": [{"type": "input_audio", "audio": base64.b64encode(pcm).decode("utf-8")}], + } + ) + target.save_audio_async.assert_awaited_once_with(b"response", 1, 2, 24000) + + async def test_send_prompt_audio_path_calls_send_audio_async(target, tmp_path): """An audio_path message is routed through the atomic send_audio_async path.""" wav_path = _write_wav(tmp_path / "in.wav") From 9df077553174313f8a6c008063ff7962158cac2e Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:53:24 -0700 Subject: [PATCH 2/2] Add docstring to _read_wav_file for consistency with sibling helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyrit/prompt_target/openai/openai_realtime_target.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyrit/prompt_target/openai/openai_realtime_target.py b/pyrit/prompt_target/openai/openai_realtime_target.py index 3f612ce511..46264da6ed 100644 --- a/pyrit/prompt_target/openai/openai_realtime_target.py +++ b/pyrit/prompt_target/openai/openai_realtime_target.py @@ -879,6 +879,16 @@ async def _construct_message_from_response_async(self, response: Any, request: A @staticmethod def _read_wav_file(filename: str) -> tuple[bytes, int, int, int]: + """ + Read raw audio frames and format metadata from a WAV file. + + Args: + filename (str): Path to the WAV file to read. + + Returns: + tuple[bytes, int, int, int]: The raw audio frames, number of channels, + sample width in bytes, and frame rate. + """ with wave.open(filename, "rb") as wav_file: return ( wav_file.readframes(wav_file.getnframes()),