Merge pull request #4091 from pipecat-ai/mb/gradium-multiplexing-setup

feat: send per-context setup in Gradium TTS multiplexing
This commit is contained in:
Mark Backman
2026-03-23 12:00:53 -04:00
committed by GitHub
2 changed files with 33 additions and 20 deletions

View File

@@ -0,0 +1 @@
- `GradiumTTSService` now sends a per-context `setup` message with `client_req_id` before the first text message for each TTS context, following Gradium's multiplexing protocol. Previously, a single setup message was sent at connection time without a `client_req_id`, which prevented Gradium from associating requests with their sessions when using `close_ws_on_eos=False`.

View File

@@ -140,6 +140,7 @@ class GradiumTTSService(WebsocketTTSService):
# State tracking
self._receive_task = None
self._setup_context_ids: set[str] = set()
def can_generate_metrics(self) -> bool:
"""Check if this service can generate processing metrics.
@@ -166,8 +167,25 @@ class GradiumTTSService(WebsocketTTSService):
self._warn_unhandled_updated_settings(changed)
return changed
def _build_msg(self, text: str = "", context_id: str = "") -> dict:
"""Build JSON message for Gradium API."""
def _build_setup_msg(self, context_id: str) -> dict:
"""Build setup message for Gradium API.
Args:
context_id: Context ID to use as ``client_req_id``.
"""
setup_msg: dict[str, Any] = {
"type": "setup",
"output_format": "pcm",
"voice_id": self._settings.voice,
"close_ws_on_eos": False,
"client_req_id": context_id,
}
if self._json_config is not None:
setup_msg["json_config"] = self._json_config
return setup_msg
def _build_text_msg(self, text: str = "", context_id: str = "") -> dict:
"""Build text message for Gradium API."""
msg = {"text": text, "type": "text", "client_req_id": context_id}
return msg
@@ -236,22 +254,6 @@ class GradiumTTSService(WebsocketTTSService):
headers = {"x-api-key": self._api_key, "x-api-source": "pipecat"}
self._websocket = await websocket_connect(self._url, additional_headers=headers)
setup_msg = {
"type": "setup",
"output_format": "pcm",
"voice_id": self._settings.voice,
"close_ws_on_eos": False,
}
if self._json_config is not None:
setup_msg["json_config"] = self._json_config
await self._websocket.send(json.dumps(setup_msg))
ready_msg = await self._websocket.recv()
ready_msg = json.loads(ready_msg)
if ready_msg["type"] == "error":
raise Exception(f"received error {ready_msg['message']}")
if ready_msg["type"] != "ready":
raise Exception(f"unexpected first message type {ready_msg['type']}")
await self._call_event_handler("on_connected")
except Exception as e:
await self.push_error(error_msg=f"Unknown error occurred: {e}", exception=e)
@@ -269,6 +271,7 @@ class GradiumTTSService(WebsocketTTSService):
finally:
await self.remove_active_audio_context()
self._websocket = None
self._setup_context_ids.clear()
await self._call_event_handler("on_disconnected")
def _get_websocket(self):
@@ -334,10 +337,15 @@ class GradiumTTSService(WebsocketTTSService):
if ctx_id and self.audio_context_available(ctx_id):
await self.add_word_timestamps([(msg["text"], msg["start_s"])], ctx_id)
elif msg["type"] == "ready":
pass
elif msg["type"] == "end_of_stream":
if ctx_id and self.audio_context_available(ctx_id):
await self.add_word_timestamps([("TTSStoppedFrame", 0), ("Reset", 0)], ctx_id)
await self.remove_audio_context(ctx_id)
if ctx_id:
self._setup_context_ids.discard(ctx_id)
await self.stop_all_metrics()
elif msg["type"] == "error":
@@ -363,8 +371,12 @@ class GradiumTTSService(WebsocketTTSService):
await self._connect()
try:
msg = self._build_msg(text=text, context_id=context_id)
await self._get_websocket().send(json.dumps(msg))
ws = self._get_websocket()
if context_id not in self._setup_context_ids:
await ws.send(json.dumps(self._build_setup_msg(context_id)))
self._setup_context_ids.add(context_id)
msg = self._build_text_msg(text=text, context_id=context_id)
await ws.send(json.dumps(msg))
await self.start_tts_usage_metrics(text)
except Exception as e:
yield ErrorFrame(error=f"Unknown error occurred: {e}")