fix(realtime): keep a Muse turn active for turnless partials after speechEnd

Muse partials carry no turnId and belong to the most recent speechStart,
and the docs say the model may keep post processing a turn after speechEnd
until speechComplete. Releasing the active turn on speechEnd made any
partial arriving in that window raise and get dropped in ENDPOINTING mode.
The turn now stays active until its speechComplete or final transcript.
This commit is contained in:
ryan-crabbe-berri 2026-09-12 15:13:44 -07:00
parent a2e383a1a5
commit 4647cd1215
2 changed files with 19 additions and 1 deletions

View file

@ -517,7 +517,6 @@ class MuseEventTransformer:
def _speech_end(self, message: Mapping[str, JsonValue]) -> _TurnState:
turn: Final = self._turn(_required_turn_id(message, "speechEnd"))
turn.stopped = True
self._release_active(turn)
return turn
def _speech_complete(self, message: Mapping[str, JsonValue]) -> _TurnState:

View file

@ -241,6 +241,25 @@ def test_speech_end_then_speech_complete_emits_stopped_then_completed():
assert completed[0]["transcript"] == "done"
def test_turnless_partial_between_speech_end_and_speech_complete_stays_on_that_turn():
transformer = MuseEventTransformer()
transformer.transform(json.loads(_event("speechStart", turnId="turn-1")))
transformer.transform(json.loads(_event("transcript", transcript="what is", final=False)))
transformer.transform(json.loads(_event("speechEnd", turnId="turn-1")))
post_processed = transformer.transform(
json.loads(_event("transcript", transcript="what is the weather", final=False))
)
completed = transformer.transform(
json.loads(_event("speechComplete", turnId="turn-1", transcript="What is the weather?"))
)
assert _typed(post_processed) == [("conversation.item.input_audio_transcription.delta", "turn-1")]
assert post_processed[0]["delta"] == " the weather"
assert _typed(completed) == [("conversation.item.input_audio_transcription.completed", "turn-1")]
assert completed[0]["transcript"] == "What is the weather?"
def _typed(events: tuple[dict[str, object], ...]) -> list[tuple[object, object]]:
return [(event["type"], event["item_id"]) for event in events]