Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions pyrit/prompt_target/openai/openai_realtime_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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(),
)
30 changes: 30 additions & 0 deletions tests/unit/prompt_target/target/test_realtime_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down