fix(inworld): close context at end of turn instead of relying on idle timeout

The Inworld WS TTS plugin previously relied on the base TTS service's 3-second AUDIO_CONTEXT_TIMEOUT to detect when audio was done, then sent close_context in on_audio_context_completed. This added unnecessary latency before TTSStoppedFrame was emitted.

The original implementation likely borrowed this idea from the 11labs' impelementation. But it's likely better to mirror the Cartesia plugin pattern where on_audio_context_completed is a no-op because the server signals completion directly.

Now close_context is sent in on_turn_context_completed (right after flush_context), so the server responds with contextClosed immediately after the last audio byte. The existing receive handler already calls remove_audio_context on contextClosed, which exits the audio context handler cleanly.
This commit is contained in:
Ian Lee
2026-03-12 21:04:18 -07:00
parent 02791cd503
commit 3e5be23bd8
2 changed files with 11 additions and 4 deletions

View File

@@ -0,0 +1 @@
- Modeified `InworldTTSService` to close context at end of turn instead of relying on idle timeout

View File

@@ -774,14 +774,20 @@ class InworldTTSService(WebsocketTTSService):
self._cumulative_time = 0.0
self._generation_end_time = 0.0
async def on_turn_context_completed(self):
"""Close the server-side context at end of turn.
Sends close_context so contextClosed arrives immediately after the
last audio byte.
"""
ctx_id = self._turn_context_id
await super().on_turn_context_completed()
await self._close_context(ctx_id)
async def on_audio_context_interrupted(self, context_id: str):
"""Callback invoked when an audio context has been interrupted."""
await self._close_context(context_id)
async def on_audio_context_completed(self, context_id: str):
"""Callback invoked when an audio context has been completed."""
await self._close_context(context_id)
def _get_websocket(self):
"""Get the websocket for the Inworld WebSocket TTS service.