Connect the inference stages

You’ll create app.py to connect the functions from stt.py, translate.py, and tts.py to a local Gradio page. The browser will provide recording and playback controls, and the Python backend will run the three models.

After creating and launching the application, you’ll test it with an English recording and Spanish as the target language. The application will transcribe the recording, display the Spanish translation as it’s generated, then synthesize Spanish speech.

Using a file editor of your choice, create a file named app.py in the ~/voice-translator directory. Copy and paste the following code into the file and save it:

    

        
        
from contextlib import ExitStack

import gradio as gr
import litert_lm
import numpy as np

from languages import LANGUAGES, translation_prompt
from stt import create_recogniser, transcribe
from translate import MODEL_PATH, translation_chunks
from tts import create_tts, synthesise


def normalise_audio(audio):
    if audio is None:
        raise gr.Error("record sentence")

    sample_rate, samples = audio
    samples = np.asarray(samples)

    if np.issubdtype(samples.dtype, np.integer):
        sample_info = np.iinfo(samples.dtype)
        limit = max(abs(sample_info.min), sample_info.max)
        samples = samples.astype(np.float32) / limit
    else:
        samples = samples.astype(np.float32)

    if samples.ndim > 1:
        samples = samples.mean(axis=1)

    return np.ascontiguousarray(samples), int(sample_rate)


def main():
    litert_lm.set_min_log_severity(litert_lm.LogSeverity.ERROR)

    print("Loading STT")
    recogniser = create_recogniser()
    print("Loading TTS")
    tts = create_tts()
    print("Loading Gemma 4")

    with litert_lm.Engine(
        str(MODEL_PATH),
        backend=litert_lm.Backend.GPU(),
        enable_speculative_decoding=True,
    ) as engine:
        conversation_resources = ExitStack()
        conversation = None
        conversation_language = None

        def select_conversation(language):
            nonlocal conversation_resources
            nonlocal conversation
            nonlocal conversation_language

            if language != conversation_language:
                conversation_resources.close()
                conversation_resources = ExitStack()
                system_message = litert_lm.Message.system(
                    translation_prompt(language)
                )
                conversation = conversation_resources.enter_context(
                    engine.create_conversation(messages=[system_message])
                )
                conversation_language = language

            return conversation

        def reset_language(language):
            select_conversation(language)
            return "", "", None, f"Ready to translate to {language}."

        def run_pipeline(audio, language):
            samples, sample_rate = normalise_audio(audio)
            transcript = transcribe(recogniser, samples, sample_rate)

            if not transcript:
                raise gr.Error(
                    "No speech was detected. Please try again."
                )

            yield transcript, "", None, "Translating..."

            translated_text = ""
            active_conversation = select_conversation(language)
            for text_part in translation_chunks(
                active_conversation, transcript
            ):
                translated_text += text_part
                yield transcript, translated_text, None, "Translating..."

            translated_text = translated_text.strip()
            if not translated_text:
                raise gr.Error("Model did not return a translation")

            yield transcript, translated_text, None, "Synthesizing..."
            generated_audio = synthesise(
                tts, translated_text, LANGUAGES[language]
            )
            yield (
                transcript,
                translated_text,
                (
                    generated_audio.sample_rate,
                    np.asarray(generated_audio.samples, dtype=np.float32),
                ),
                "Ready",
            )

        with gr.Blocks(
            title="Voice Translator",
            analytics_enabled=False,
        ) as demo:
            gr.Markdown(
                "# Voice translator\n"
                "Record a sentence and translate it locally."
            )

            with gr.Row():
                microphone = gr.Audio(
                    sources=["microphone"],
                    type="numpy",
                    label="English speech",
                )
                with gr.Column():
                    language = gr.Dropdown(
                        choices=list(LANGUAGES),
                        value="Spanish",
                        label="Translate to",
                    )
                    translate_button = gr.Button(
                        "Translate",
                        variant="primary",
                    )
                    status = gr.Markdown("Ready to translate.")

            with gr.Row():
                transcript_output = gr.Textbox(
                    label="Transcript",
                    interactive=False,
                )
                translation_output = gr.Textbox(
                    label="Translation",
                    interactive=False,
                )

            speech_output = gr.Audio(
                label="Translated speech",
                autoplay=True,
                interactive=False,
            )

            outputs = [
                transcript_output,
                translation_output,
                speech_output,
                status,
            ]
            translate_button.click(
                run_pipeline,
                inputs=[microphone, language],
                outputs=outputs,
                concurrency_limit=1,
                concurrency_id="models",
            )
            language.change(
                reset_language,
                inputs=language,
                outputs=outputs,
                concurrency_limit=1,
                concurrency_id="models",
            )

        try:
            demo.queue(default_concurrency_limit=1).launch(
                inbrowser=True,
                share=False,
                server_name="127.0.0.1",
            )
        finally:
            conversation_resources.close()


if __name__ == "__main__":
    main()

    

The code is arranged as follows:

  1. normalise_audio() prepares browser audio for the recognizer.
  2. main() loads the models and owns their lifetime.
  3. select_conversation() and reset_language() handle the selected language.
  4. run_pipeline() yields the transcript, translation, audio, and status as processing advances.
  5. The Gradio components connect those functions to the recording and translation controls.

Prepare browser audio

normalise_audio() accepts the sample-rate and sample-array pair from Gradio. It scales integer samples to floating-point values, averages multiple channels into mono, and returns a contiguous float32 array. It preserves the sample rate of the recording so that Sherpa-ONNX can resample it.

Keep the models loaded

main() creates the recognizer, synthesizer, and Gemma engine before Gradio starts serving requests. select_conversation() reuses the current conversation while the target language stays the same. When the language changes, it closes that conversation and creates one with a new system instruction. ExitStack handles this replaceable conversation, and the finally block closes it at shutdown.

Update the interface in order

Each yield in run_pipeline() supplies values in the same order as the outputs list: transcript, translation, audio, and status. The audio value remains None until Supertonic has synthesized the complete translation.

Both event handlers use concurrency_id="models" with a limit of one. This serializes translation and language changes, preventing a language change from closing a conversation while a translation is using it. A language change requested during translation waits for that work to finish.

Launch the desktop interface

Continuing in the ~/voice-translator directory with the active virtual environment, start the application:

    

        
        
python app.py

    

The three loading messages appear once, then Gradio prints a local address and opens the application in your default browser. The following screenshot shows the completed result after translating a recording:

Image Alt Text:Gradio voice translator showing the English transcript, Spanish translation, and playable translated speech with Ready statusCompleted English-to-Spanish voice translation

Note

The browser might request microphone access the first time you record. Select Allow.

If recording is unavailable, open System Settings. Select Privacy & Security, select Microphone, and enable access for your browser.

Leave Translate to set to its default of Spanish. Record the following sentence, stop the recording, and select Translate:

    

        
        
Where is the nearest train station?

    

The interface displays the recognized text in Transcript, streams the Spanish translation into Translation, and adds playable audio to Translated speech.

What you’ve accomplished and what’s next

You’ve set up the desktop voice translator by combining functions for transcription, translation, and speech generation. The three command-line stages now run from one browser recording, with their models kept in memory between requests.

Next, you’ll check the transcript, Spanish translation, and spoken Spanish result, then repeat the test without a network connection.

Back
Next