DEV Community

Harpreet Singh Seehra
Harpreet Singh Seehra

Posted on

Build a Real-Time AI Translation Bridge for Phone Calls in 141 Lines of Python

Connect two callers who speak different languages on the same phone call. Each caller speaks in their own language and hears the other in their own language — live, on the same call, no interpreter in the loop.

No Google Translate. No AWS Translate. No DeepL. No third-party translation API. Just Telnyx Voice Call Control (for the phone call) and Telnyx AI Inference (for the translation). Same API key, same platform, same billing.

141 lines of Python. Here's how it works.

What It Does

You POST two phone numbers and two languages:

curl -X POST http://localhost:5000/bridge \
  -H "Content-Type: application/json" \
  -d '{"number_a": "+13125550001", "lang_a": "English",
       "number_b": "+5215550002", "lang_b": "Spanish"}'
Enter fullscreen mode Exit fullscreen mode

The app calls caller A. When they answer, it calls caller B. When B answers, the translation bridge goes live:

  1. Caller A speaks in English
  2. Telnyx transcribes the speech → sends to your webhook
  3. Your app sends the transcript to AI Inference: "Translate from English to Spanish"
  4. The translated text is spoken to caller B via TTS in Spanish
  5. Caller B speaks in Spanish → transcribed → translated to English → spoken to caller A
  6. Loop until either caller hangs up

Both callers stay on the same phone call. Neither installs anything. Translation happens server-side, transparently.

The Architecture

Everything lives in one Flask file. No database, no Redis, no message queue. Bridge state lives in an in-memory dict, keyed by a bridge ID that travels in Telnyx's client_state field on every webhook event.

POST /bridge → call A → A answers → call B → B answers → bridge active
     ↓
gather A speech → translate → TTS to B → gather B speech → translate → TTS to A → loop
     ↓
hangup → hangup other caller → bridge ended
Enter fullscreen mode Exit fullscreen mode

The client_state field on every Telnyx action carries the bridge ID and which side (a or b) the call belongs to. When a webhook fires, you decode the base64 state, look up the bridge, and know exactly which call leg you're handling. No session store — the call is self-describing.

The Language Code Bug

The original sample had all TTS and speech recognition hardcoded to language_code="en-US" — even for Spanish. The call "worked" (audio flowed, no errors) but the translation was useless:

  • Spanish TTS with en-US: the TTS engine reads Spanish words with English phonemes. Unintelligible.
  • Spanish STT with en-US: the STT engine listens for English sounds. Spanish speech recognition drops.

The fix — a language name → BCP-47 code mapping:

LANG_CODES = {
    "english": "en-US", "spanish": "es-US", "french": "fr-FR",
    "german": "de-DE", "italian": "it-IT", "portuguese": "pt-BR",
    "hindi": "hi-IN", "arabic": "ar-SA", "chinese": "zh-CN",
    "japanese": "ja-JP", "korean": "ko-KR", "russian": "ru-RU",
}

def lang_code(lang):
    return LANG_CODES.get(lang.lower(), "en-US")
Enter fullscreen mode Exit fullscreen mode

Now TTS uses the target language (Spanish text → es-US pronunciation) and STT uses the speaker's language (Spanish speech → es-US recognition).

The Translation Call

Uses Telnyx AI Inference — the OpenAI-compatible /v2/ai/chat/completions endpoint:

def translate(text, from_lang, to_lang):
    resp = requests.post(INFERENCE_URL,
        headers={"Authorization": f"Bearer {TELNYX_API_KEY}"},
        json={
            "model": AI_MODEL,
            "messages": [
                {"role": "system",
                 "content": f"Translate from {from_lang} to {to_lang}. "
                           "Return ONLY the translation, nothing else."},
                {"role": "user", "content": text}
            ],
            "max_tokens": 200, "temperature": 0.1
        })
    return resp.json()["choices"][0]["message"]["content"]
Enter fullscreen mode Exit fullscreen mode

Temperature 0.1 for deterministic translations. 200 max tokens for conversational phrases. The model defaults to moonshotai/Kimi-K2.6 — swap to any model in the Telnyx catalog via the AI_MODEL env var.

No separate translation API account. The same Telnyx API key that makes the phone call does the translation.

Webhook Signature Verification

Telnyx signs every webhook with an Ed25519 key. The app verifies before trusting:

@app.route("/webhooks/voice", methods=["POST"])
def handle_voice():
    try:
        client.webhooks.unwrap(
            request.get_data(as_text=True),
            headers=dict(request.headers)
        )
    except Exception:
        return jsonify({"error": "invalid signature"}), 401
Enter fullscreen mode Exit fullscreen mode

Without this, anyone who knows your webhook URL can inject fake call events. The telnyx SDK's webhooks.unwrap() handles the Ed25519 verification.

The Call Loop

Each Telnyx event triggers the next action:

# Caller finished speaking → we have a transcript
elif event_type == "call.gather.ended" and bridge:
    speech = p.get("speech", {}).get("result", "")
    if speech and bridge.get("state") == "active":
        from_lang = bridge[f"lang_{side}"]
        to_lang = bridge["lang_b"] if side == "a" else bridge["lang_a"]
        other_ccid = bridge["ccids"].get("b" if side == "a" else "a")
        translated = translate(speech, from_lang, to_lang)
        if other_ccid:
            client.calls.actions.speak(other_ccid, payload=translated,
                voice="female", language_code=lang_code(to_lang))
        client.calls.actions.gather(ccid, input_type="speech",
            language_code=lang_code(bridge[f"lang_{side}"]))
Enter fullscreen mode Exit fullscreen mode

When either caller hangs up, the other is hung up too — a translation bridge with one caller is pointless:

elif event_type == "call.hangup" and bridge:
    other_side = "b" if side == "a" else "a"
    other_ccid = bridge.get("ccids", {}).get(other_side)
    if other_ccid:
        requests.post(f".../calls/{other_ccid}/actions/hangup", ...)
    bridge["state"] = "ended"
Enter fullscreen mode Exit fullscreen mode

Try It Yourself

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-real-time-translation-bridge-python
cp .env.example .env   # TELNYX_API_KEY, TELNYX_PUBLIC_KEY, BRIDGE_NUMBER, CONNECTION_ID
pip install -r requirements.txt
python app.py
Enter fullscreen mode Exit fullscreen mode

Then:

ngrok http 5000
Enter fullscreen mode Exit fullscreen mode

Point your Call Control Application webhook URL to https://<id>.ngrok.io/webhooks/voice in the Telnyx Portal.

Trigger a bridge:

curl -X POST http://localhost:5000/bridge \
  -H "Content-Type: application/json" \
  -d '{"number_a": "+13125550001", "lang_a": "English",
       "number_b": "+5215550002", "lang_b": "Spanish"}'
Enter fullscreen mode Exit fullscreen mode

Call from two phones. Speak in different languages. Hear real-time translation.

Key links:

Top comments (0)