Keep realtime conversation items ordered across turns
This commit is contained in:
@@ -31,8 +31,8 @@ class Microphone(MediaStreamTrack):
|
||||
|
||||
async def recv(self):
|
||||
await asyncio.sleep(max(0, self.start + self.count * .02 - time.monotonic()))
|
||||
# 0.6 s speech followed by silence to trigger server VAD.
|
||||
if 4 <= self.count < 44:
|
||||
# Two utterances separated by enough time for the first TTS reply.
|
||||
if 4 <= self.count < 44 or 750 <= self.count < 790:
|
||||
index = np.arange(FRAME_SAMPLES) + self.count * FRAME_SAMPLES
|
||||
samples = (8000 * np.sin(2 * np.pi * 440 * index / SAMPLE_RATE)).astype("<i2")
|
||||
else:
|
||||
@@ -132,16 +132,20 @@ class VoiceSmokeTest(unittest.IsolatedAsyncioTestCase):
|
||||
voice_runner, voice_url = await serve(create_app())
|
||||
pc = RTCPeerConnection()
|
||||
events = []
|
||||
event_times = []
|
||||
done = asyncio.Event()
|
||||
audio_received = asyncio.Event()
|
||||
completed_responses = 0
|
||||
function_calls = 0
|
||||
channel = pc.createDataChannel("oai-events")
|
||||
pc.addTrack(Microphone())
|
||||
microphone = Microphone()
|
||||
pc.addTrack(microphone)
|
||||
|
||||
@pc.on("track")
|
||||
def on_track(track):
|
||||
if track.kind == "audio":
|
||||
async def consume():
|
||||
for _ in range(150):
|
||||
for _ in range(600):
|
||||
frame = await track.recv()
|
||||
if np.max(np.abs(frame.to_ndarray())) > 100:
|
||||
audio_received.set()
|
||||
@@ -154,11 +158,14 @@ class VoiceSmokeTest(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
@channel.on("message")
|
||||
def on_message(raw):
|
||||
nonlocal completed_responses, function_calls
|
||||
event = json.loads(raw)
|
||||
events.append(event)
|
||||
event_times.append((round(time.monotonic() - microphone.start, 1), event.get("type")))
|
||||
if event.get("type") == "response.done":
|
||||
output = event.get("response", {}).get("output", [])
|
||||
if output and output[0].get("type") == "function_call":
|
||||
function_calls += 1
|
||||
call = output[0]
|
||||
channel.send(json.dumps({"type": "conversation.item.create", "item": {
|
||||
"type": "function_call_output", "call_id": call["call_id"],
|
||||
@@ -166,7 +173,9 @@ class VoiceSmokeTest(unittest.IsolatedAsyncioTestCase):
|
||||
}}))
|
||||
channel.send(json.dumps({"type": "response.create"}))
|
||||
elif output and output[0].get("type") == "message":
|
||||
done.set()
|
||||
completed_responses += 1
|
||||
if completed_responses == 2:
|
||||
done.set()
|
||||
|
||||
try:
|
||||
await pc.setLocalDescription(await pc.createOffer())
|
||||
@@ -186,13 +195,29 @@ class VoiceSmokeTest(unittest.IsolatedAsyncioTestCase):
|
||||
answer = await response.text()
|
||||
await pc.setRemoteDescription(RTCSessionDescription(sdp=answer, type="answer"))
|
||||
try:
|
||||
await asyncio.wait_for(done.wait(), timeout=20)
|
||||
await asyncio.wait_for(done.wait(), timeout=25)
|
||||
except asyncio.TimeoutError:
|
||||
self.fail(f"No completed voice response; events: {[e.get('type') for e in events]}")
|
||||
self.fail(f"No completed voice response; state={pc.connectionState}, "
|
||||
f"mic_frames={microphone.count}, events: {event_times}")
|
||||
await asyncio.wait_for(audio_received.wait(), timeout=5)
|
||||
self.assertEqual(function_calls, 2)
|
||||
self.assertTrue(any(event.get("type") ==
|
||||
"conversation.item.input_audio_transcription.completed"
|
||||
for event in events))
|
||||
user_items = [event for event in events
|
||||
if event.get("type") == "input_audio_buffer.committed"]
|
||||
assistant_items = [event for event in events
|
||||
if event.get("type") == "conversation.item.created"
|
||||
and event.get("item", {}).get("role") == "assistant"]
|
||||
assistant_transcripts = [event for event in events
|
||||
if event.get("type") == "response.output_audio_transcript.done"]
|
||||
self.assertEqual(len(user_items), 2)
|
||||
self.assertEqual(len(assistant_items), 2)
|
||||
self.assertEqual(len(assistant_transcripts), 2)
|
||||
self.assertEqual(assistant_items[0]["previous_item_id"], user_items[0]["item_id"])
|
||||
self.assertEqual(assistant_transcripts[0]["item_id"], assistant_items[0]["item"]["id"])
|
||||
self.assertEqual(user_items[1]["previous_item_id"], assistant_items[0]["item"]["id"])
|
||||
self.assertEqual(assistant_items[1]["previous_item_id"], user_items[1]["item_id"])
|
||||
committed = next((index, event) for index, event in enumerate(events)
|
||||
if event.get("type") == "input_audio_buffer.committed")
|
||||
completed = next((index, event) for index, event in enumerate(events)
|
||||
|
||||
Reference in New Issue
Block a user