Some checks are pending
CI / ruff (push) Waiting to run
CI / mypy (push) Waiting to run
CI / pytest (push) Waiting to run
CI / package (push) Waiting to run
CI / Install smoke (${{ matrix.label }}) (linux, ubuntu-latest) (push) Blocked by required conditions
CI / Install smoke (${{ matrix.label }}) (macos-arm64, macos-14) (push) Blocked by required conditions
219 lines
6.0 KiB
Python
219 lines
6.0 KiB
Python
from queue import Queue
|
|
from threading import Event, Thread
|
|
|
|
from openai.types.realtime.realtime_response_create_params import RealtimeResponseCreateParams
|
|
|
|
from speech_to_speech.LLM.lm_output_processor import LMOutputProcessor
|
|
from speech_to_speech.pipeline.messages import EndOfResponse, LLMResponseChunk, TTSInput
|
|
from speech_to_speech.pipeline.speculative_turns import SpeculativeTurnTracker
|
|
|
|
|
|
def _processor(tracker: SpeculativeTurnTracker) -> LMOutputProcessor:
|
|
processor = LMOutputProcessor.__new__(LMOutputProcessor)
|
|
processor.setup(text_output_queue=Queue(), speculative_turns=tracker)
|
|
return processor
|
|
|
|
|
|
def test_stale_end_of_response_is_not_forwarded_to_tts():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 1)
|
|
processor = _processor(tracker)
|
|
|
|
outputs = list(processor.process(EndOfResponse(turn_id="turn_1", turn_revision=0)))
|
|
|
|
assert outputs == []
|
|
|
|
|
|
def test_latest_end_of_response_is_forwarded_to_tts():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 1)
|
|
processor = _processor(tracker)
|
|
|
|
outputs = list(processor.process(EndOfResponse(turn_id="turn_1", turn_revision=1)))
|
|
|
|
assert len(outputs) == 1
|
|
assert outputs[0].turn_id == "turn_1"
|
|
assert outputs[0].turn_revision == 1
|
|
|
|
|
|
def test_cancel_generation_is_forwarded_to_tts():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 0)
|
|
processor = _processor(tracker)
|
|
|
|
outputs = list(
|
|
processor.process(
|
|
LLMResponseChunk(
|
|
text="hello",
|
|
turn_id="turn_1",
|
|
turn_revision=0,
|
|
cancel_generation=7,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert len(outputs) == 1
|
|
assert outputs[0].cancel_generation == 7
|
|
|
|
|
|
def test_text_only_chunk_is_not_forwarded_to_tts():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 0)
|
|
processor = _processor(tracker)
|
|
|
|
outputs = list(
|
|
processor.process(
|
|
LLMResponseChunk(
|
|
text="hello",
|
|
turn_id="turn_1",
|
|
turn_revision=0,
|
|
response=RealtimeResponseCreateParams(output_modalities=["text"]),
|
|
)
|
|
)
|
|
)
|
|
|
|
assert outputs == []
|
|
# The assistant text still reaches clients even when TTS is skipped.
|
|
event = processor.text_output_queue.get_nowait()
|
|
assert event.text == "hello"
|
|
|
|
|
|
def test_audio_chunk_is_forwarded_to_tts():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 0)
|
|
processor = _processor(tracker)
|
|
|
|
outputs = list(
|
|
processor.process(
|
|
LLMResponseChunk(
|
|
text="hello",
|
|
turn_id="turn_1",
|
|
turn_revision=0,
|
|
response=RealtimeResponseCreateParams(output_modalities=["audio"]),
|
|
)
|
|
)
|
|
)
|
|
|
|
assert len(outputs) == 1
|
|
assert isinstance(outputs[0], TTSInput)
|
|
assert outputs[0].text == "hello"
|
|
|
|
|
|
def test_empty_modalities_is_forwarded_to_tts():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 0)
|
|
processor = _processor(tracker)
|
|
|
|
outputs = list(
|
|
processor.process(
|
|
LLMResponseChunk(
|
|
text="hello",
|
|
turn_id="turn_1",
|
|
turn_revision=0,
|
|
response=RealtimeResponseCreateParams(output_modalities=[]),
|
|
)
|
|
)
|
|
)
|
|
|
|
assert len(outputs) == 1
|
|
assert isinstance(outputs[0], TTSInput)
|
|
|
|
|
|
def test_pending_reopen_holds_assistant_chunk_until_cancelled():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 0)
|
|
candidate_revision = tracker.begin_reopen_candidate("turn_1", 0)
|
|
processor = _processor(tracker)
|
|
done = Event()
|
|
outputs = []
|
|
|
|
def run_processor():
|
|
outputs.extend(
|
|
processor.process(
|
|
LLMResponseChunk(
|
|
text="hello",
|
|
turn_id="turn_1",
|
|
turn_revision=0,
|
|
)
|
|
)
|
|
)
|
|
done.set()
|
|
|
|
thread = Thread(target=run_processor)
|
|
thread.start()
|
|
|
|
assert not done.wait(0.05)
|
|
tracker.cancel_reopen_candidate("turn_1", candidate_revision)
|
|
assert done.wait(1.0)
|
|
thread.join(timeout=1.0)
|
|
|
|
assert len(outputs) == 1
|
|
assert outputs[0].text == "hello"
|
|
event = processor.text_output_queue.get_nowait()
|
|
assert event.text == "hello"
|
|
|
|
|
|
def test_reopen_grace_holds_assistant_chunk_until_elapsed():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 0)
|
|
tracker.start_reopen_grace("turn_1", 0, grace_s=0.08)
|
|
processor = _processor(tracker)
|
|
done = Event()
|
|
outputs = []
|
|
|
|
def run_processor():
|
|
outputs.extend(
|
|
processor.process(
|
|
LLMResponseChunk(
|
|
text="hello",
|
|
turn_id="turn_1",
|
|
turn_revision=0,
|
|
)
|
|
)
|
|
)
|
|
done.set()
|
|
|
|
thread = Thread(target=run_processor)
|
|
thread.start()
|
|
|
|
assert not done.wait(0.02)
|
|
assert done.wait(1.0)
|
|
thread.join(timeout=1.0)
|
|
|
|
assert len(outputs) == 1
|
|
assert outputs[0].text == "hello"
|
|
event = processor.text_output_queue.get_nowait()
|
|
assert event.text == "hello"
|
|
|
|
|
|
def test_confirmed_reopen_drops_held_assistant_chunk():
|
|
tracker = SpeculativeTurnTracker()
|
|
tracker.observe("turn_1", 0)
|
|
candidate_revision = tracker.begin_reopen_candidate("turn_1", 0)
|
|
processor = _processor(tracker)
|
|
done = Event()
|
|
outputs = []
|
|
|
|
def run_processor():
|
|
outputs.extend(
|
|
processor.process(
|
|
LLMResponseChunk(
|
|
text="hello",
|
|
turn_id="turn_1",
|
|
turn_revision=0,
|
|
)
|
|
)
|
|
)
|
|
done.set()
|
|
|
|
thread = Thread(target=run_processor)
|
|
thread.start()
|
|
|
|
assert not done.wait(0.05)
|
|
assert tracker.confirm_reopen_candidate("turn_1", 0, candidate_revision)
|
|
assert done.wait(1.0)
|
|
thread.join(timeout=1.0)
|
|
|
|
assert outputs == []
|
|
assert processor.text_output_queue.empty()
|