Skip to content

feat: add Gradium speech-to-speech translation extension - #2292

Open
TiagoAgora wants to merge 6 commits into
TEN-framework:mainfrom
TiagoAgora:feat/gradium-mllm-translation
Open

feat: add Gradium speech-to-speech translation extension#2292
TiagoAgora wants to merge 6 commits into
TEN-framework:mainfrom
TiagoAgora:feat/gradium-mllm-translation

Conversation

@TiagoAgora

Copy link
Copy Markdown
Contributor

Summary

  • Adds gradium_mllm_python, a new extension implementing the AsyncMLLMBaseExtension contract (same as openai_mllm_python/glm_mllm_python/etc.) to integrate Gradium's real-time speech-to-speech Translation API
  • Wires the extension into voice-assistant-realtime as a second predefined graph (gradium_translate_demo, auto_start: false) alongside the existing default graph, so it doesn't disturb the current demo
  • Protocol (endpoint, auth, setup payload shape, json_config nesting for target_language) confirmed directly against Gradium and verified against their live API
  • Adds a test suite (tests/) mirroring gradium_tts_python's pattern: a real TEN runtime drives the extension lifecycle with only the Gradium websocket client mocked
  • Fixes a real shutdown-deadlock bug found while running the tests: AsyncMLLMBaseExtension.on_start() awaits start_connection() directly, so a start_connection() that blocks for the life of the connection (as this and sibling MLLM extensions did) prevents the framework from ever advancing to on_stop(). Fixed by connecting, then handing the receive loop off to a background task.

Test plan

  • task test-extension EXTENSION=agents/ten_packages/extension/gradium_mllm_python — 11/11 tests pass
  • Verified live against Gradium's real API (wss://us.api.gradium.ai/api/speech/s2s) — session handshake succeeds with real credentials
  • End-to-end verified through the full graph: real speech audio published into an Agora channel was translated by the live Gradium API and the translated text was observed flowing back through main_control
  • Full audio round-trip through the browser playground (blocked in this environment by a bun/QEMU emulation incompatibility unrelated to this extension; not yet tested with a real human speaker)

Tiago Peres de Sousa added 5 commits August 20, 2026 14:44
…xtension

Adds a new mllm extension wiring Gradium's real-time speech-to-speech
translation API into TEN, following the same AsyncMLLMBaseExtension
contract as openai_mllm_python/glm_mllm_python. Auth, host pattern, and
message protocol reuse what's confirmed by the existing gradium_asr_python
and gradium_tts_python extensions; the combined s2s endpoint path is not
yet confirmed and is documented as such in the extension README.

Wires the extension into voice-assistant-realtime as a second predefined
graph (gradium_translate_demo, auto_start: false) alongside the existing
default graph.
…ed protocol

Pratim (Gradium) confirmed the real /api/speech/s2s protocol, fixing a few
assumptions from the initial scaffold:
- model_name is "s2s-translate", paired with stt_model_name: "stt-translate"
  and tts_model_name: "default" (not the placeholder "default"/empty values
  used before)
- target_language is not a top-level setup field, it nests inside
  json_config
- vad is not part of this protocol at all (only ready/audio/text/
  end_of_stream/error) -- removed the dead handling
- text messages carry translated output only, confirming no source-language
  transcript is available from this endpoint
- voice_id must belong to target_language; since there's no voice catalog to
  validate against, on_init now raises if it's unset instead of defaulting
  to a voice borrowed from the (possibly different-language) TTS demo
…r handling

Adds a tests/ suite mirroring gradium_tts_python's pattern: a real TEN
runtime drives the actual extension lifecycle via AsyncExtensionTester,
with only GradiumS2SClient mocked. Covers session-ready plus translated
text/audio routing, server-side error propagation, connect failures, and
missing api_key/voice_id. test_config.py separately unit-tests
GradiumMLLMConfig (no TEN runtime needed), guarding the json_config
nesting for target_language.

Writing the missing-config tests surfaced that on_init raised directly on
missing api_key/voice_id, which would crash the extension process instead
of reporting a clean error Data -- inconsistent with gradium_tts_python's
own on_init pattern. Fixed to catch and report via send_mllm_error, and
added a guard in start_connection() so it doesn't attempt a connection
with already-known-invalid config.
…ning the tests

Running the Tier 2 suite for real (in the TEN dev container, against the
real ten_runtime -- not just syntax/logic checks) surfaced a genuine
architectural bug: AsyncMLLMBaseExtension.on_start() does
`await self.start_connection()` directly. start_connection() looped forever
internally (async for message in self.client.messages()), so on_start()
never completed, and the framework could never advance the extension to
on_stop() -- any clean shutdown attempt hung indefinitely.

Fixed by matching gradium_asr_python's proven pattern: start_connection()
now connects, sends session-ready, then hands the message loop off to a
background asyncio.Task (_receive_loop) and returns immediately.
stop_connection() cancels that task before closing the client. Also fixes
the mock client in gradium_mocks.py, whose messages() generator blocked on
an event that close() never set -- ties close() to the event the generator
waits on, mirroring how closing a real websocket ends the async for loop.

Verified: all 11 tests pass via `task test-extension
EXTENSION=agents/ten_packages/extension/gradium_mllm_python` in the dev
container.
…sh voice

Pratim confirmed both existing Gradium voice IDs already used in this repo
are English: YTpq7expH9539ERJ (Emma) and cLONiZ4hQ8VpQ4Sz (Skyler). Sets
Emma as the default voice_id, matching the default target_language: "en",
so the extension and demo graph work out of the box for an English demo
instead of requiring manual voice_id setup first. Still overridable (env
var default in the demo graph) for other target_language values -- voice_id
and target_language must be changed together.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant