Processing files
The Speech-to-Speech API is a real-time service: audio is consumed at the speed of speech, and you cannot push a file faster than real time (audio messages are rate-limited to ~25 packets per second, and the pipeline itself — voice activity detection, segment confirmation, endpointing — is driven by the audio timeline). Translating a file therefore takes about as long as the file's duration.
Even so, feeding pre-recorded files through the API is very useful:
- Accuracy benchmarks — run a reference corpus through the pipeline and measure WER / translation quality of different
asr_model/translation_modelvalues on your domain. - Regression testing — verify that a settings change (glossaries, splitter options, silence thresholds) still produces the transcripts and translations you expect.
- Reproducible debugging — replay the exact same audio while investigating an issue.
The flow
- Connect to the WebSocket endpoint and send
set_task(see Translation management API). - Send the file's PCM in 320 ms chunks paced to real time via
input_audio_data. - After the last byte is sent, send
end_task. No new audio is accepted from this point on — that's why you send it only once the whole file has been streamed. - The server finishes processing everything it received (the phrase in progress is finalized even if the file ends abruptly, with no trailing silence), delivers the final
validated_transcription/translated_transcriptionmessages and the trailing TTS audio, then sends anend_of_streammessage and closes the connection automatically.
It can lead to the last segment missing in the translation.
Quick way: translate_file from the Python client
The palabra-ai client does all of the above for you — real-time pacing, end_task at the end, and collecting the results:
pip install palabra-ai
from palabra_ai import Palabra
palabra = Palabra() # set PALABRA_API_KEY via ENV, or pass api_key=
results = palabra.translate_file(
"speech_es.wav",
source="es",
targets="en",
output="speech_en.wav", # translated audio, one WAV per target
on_transcript=print, # partial/validated transcripts and translations
)
translate_file returns {language: pcm_s16le_24k_mono} and takes about as long as the audio itself. To collect transcripts for a benchmark, gather them in on_transcript (each Transcript has .text, .language and .is_validated).
Full example: raw WebSocket with send_audio
The same flow without the client library — useful if you need full control over the task settings or want to log every message for scoring:
import asyncio
import base64
import json
import wave
import websockets
WS_URL = "wss://<STREAMING_SERVER>.palabra.ai/streaming-api/v1/speech-to-speech/stream"
TOKEN = "<publisher JWT from the session>" # see Session Management
RATE = 16000
CHUNK = int(RATE * 0.32) * 2 # 320 ms of pcm_s16le mono
TASK = {
"input_stream": {
"content_type": "audio",
"source": {"type": "ws", "format": "pcm_s16le", "sample_rate": RATE, "channels": 1},
},
"output_stream": {
"content_type": "audio",
"target": {"type": "ws", "format": "pcm_s16le"},
},
"pipeline": {
"transcription": {"source_language": "es", "asr_model": "auto"},
"translations": [{"target_language": "en", "speech_generation": {}}],
"allowed_message_types": [
"partial_transcription",
"validated_transcription",
"translated_transcription",
],
},
}
async def send(ws, message_type, data):
await ws.send(json.dumps({"message_type": message_type, "data": data}))
async def send_audio(ws, chunk: bytes):
await send(ws, "input_audio_data", {"data": base64.b64encode(chunk).decode()})
async def main():
with wave.open("speech_es.wav") as w: # must match the task: mono 16 kHz s16le
pcm = w.readframes(w.getnframes())
validated, translated = [], []
done = asyncio.Event()
async with websockets.connect(f"{WS_URL}?token={TOKEN}", max_size=None) as ws:
async def reader():
try:
async for raw in ws:
msg = json.loads(raw)
data = msg.get("data") or {}
if isinstance(data, str):
data = json.loads(data)
mt = msg.get("message_type")
if mt == "validated_transcription":
validated.append(data["transcription"]["text"])
elif mt == "translated_transcription":
translated.append(data["transcription"]["text"])
elif mt == "end_of_stream":
print("end_of_stream received — tail is fully processed")
except websockets.ConnectionClosed:
pass # server closes the connection after end_task
finally:
done.set()
reader_task = asyncio.create_task(reader())
await send(ws, "set_task", TASK)
# stream the file at REAL-TIME speed: one 320 ms chunk every 320 ms.
# Sending faster is pointless — audio messages are rate-limited and
# the pipeline is paced by the audio timeline anyway.
loop = asyncio.get_running_loop()
start = loop.time()
for i in range(0, len(pcm), CHUNK):
await send_audio(ws, pcm[i : i + CHUNK])
await asyncio.sleep(max(0.0, start + (i // CHUNK + 1) * 0.32 - loop.time()))
# end_task: no new audio is accepted from here on; the server finishes
# processing everything it received (the pending phrase is finalized
# even though the file ended abruptly with no trailing silence), sends
# `end_of_stream` and closes the socket.
await send(ws, "end_task", {})
await asyncio.wait_for(done.wait(), timeout=30)
reader_task.cancel()
print("TRANSCRIPT:", " ".join(validated))
print("TRANSLATION:", " ".join(translated))
asyncio.run(main())
Notes:
- Send
end_taskonly after the last byte of the file — the server stops accepting audio as soon as it receives the command. - The final
validated_transcriptionfor an abruptly-cut file typically arrives within a second or two afterend_task— always wait untilend_of_streambefore scoring the run. - Output TTS audio arrives as
output_audio_datamessages; ignore them (or dropoutput_stream/speech_generationfrom the task) if you only need text for the benchmark.