Guide · streaming
Live translation and spoken translations
Translate a stream as it is spoken, and have each translated sentence spoken back in a voice. Text frames and audio clips, over the same socket.
A realtime session (and every recording, for its listeners) can translate what is said into other languages as it goes, and speak the translation: each finished sentence is synthesised in the language's voice and sent as a short audio clip. Translations follow the transcript by a second or two; a spoken sentence follows its translation.
- You send: options: translate ["es"], sentences { enabled: true }, voice ["es"]
- VocalStack sends: transcript.committed (the words as spoken)
- VocalStack sends: transcript.sentence (open) (the sentence so far, a provisional translation)
- VocalStack sends: transcript.sentence (closed) (the sentence and its final translation)
- VocalStack sends: transcript.voice (that sentence, spoken in Spanish (Ogg/Opus))
Pick the languages
GET /v1/languageslists what can be transcribed (transcription), translated into (translation) and spoken (voice), with names.GET /v1/voices/available?lang=eslists the voices that speak a language, best first;defaultmarks the one used when none is named, and with a key your own cloned voices are there too.Ask for translations
translate: ["es", "fr"]on the session's options. Without sentences, each commit is followed by atranscript.translatedframe naming itsseq: attachtranslationsto that segment. You can change the list mid-stream (session.update,translate: […]); it applies to what follows.Read them by sentence
With
sentences: { enabled: true }, the server also assembles commits into sentences and translates whole sentences, which reads far better.transcript.sentenceis a full replacement of one sentence, keyed bysentenceId:statusgoesopen→closed(ordropped: discard it),seqslists the commits it covers, andtranslationsare provisional while it is open and final once it closes. Upsert bysentenceId; never append.Speak them
voice: ["es"](it needssentences) speaks each closed sentence's Spanish translation in the language's default voice;"es:<voice id>"names a voice from the catalogue, andvoiceByLang: { "es": "<voice id>" }sets one per language. WithvoiceCaston (the default), each speaker gets a distinct voice of matching gender. Each clip arrives as atranscript.voiceframe:sentenceId,lang,durationS, andaudio, the clip itself (base64 Ogg/Opus, 24 kHz mono). A long sentence comes in pieces (partofparts) so the first can play while the rest are made: play them back to back, in order.{"type":"session.ready"} {"type":"transcript.committed","segment":{"seq":0,"start":0,"end":1.4,"text":"Good morning, everyone.","speaker":0,"words":[]}} {"type":"transcript.sentence","sentence":{"sentenceId":0,"status":"open","text":"Good morning, everyone.","seqs":[0],"speaker":0,"start":0,"end":1.4,"translations":{"es":"Buenos días a todos."}}} {"type":"transcript.sentence","sentence":{"sentenceId":0,"status":"closed","text":"Good morning, everyone.","seqs":[0],"speaker":0,"start":0,"end":1.4,"translations":{"es":"Buenos días a todos."}}} {"type":"transcript.voice","sentenceId":0,"generation":0,"lang":"es","voice":"es_ES-davefx-medium","format":"ogg","durationS":1.3,"synthS":0.4,"part":0,"parts":1,"voiceKey":"","audio":"T2dnUwACAAAA…"}For a recording's listeners
A listener on the subscribe socket asks with
lang=es(translations filtered to Spanish) andvoice=<voice id>(Listen); a clip reaches every listener on that language and voice, synthesised once. On a recording,transcript.voicecarries aurlinstead ofaudio: fetch it withGET /v1/recordings/{id}/voice/{sentence}?lang=es(Ogg/Opus;format=caffor Apple).transcript.voiceStatussays when a voice is stillpreparing(its clips are held and will come) orunavailable. After the recording,GET /v1/recordings/{id}/voicelists every clip left behind, language by language.
TypeScript SDK
import { createReadStream } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { VocalStack, applySentence, type SentenceSegment } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: 'https://api.vocalstack.com', apiKey: process.env.VOCALSTACK_API_KEY });
const { voices } = await vs.voices.available('es');
const voice = voices.find((v) => v.default) ?? voices[0];
// A file at full speed here; a microphone works the same (Stream a microphone).
const session = await vs.realtime.connect(
{
input: { kind: 'file' },
spokenLanguage: 'en',
translate: ['es'],
sentences: { enabled: true },
voice: [`es:${voice.id}`],
},
{
// The clips arrive as transcript.voice frames: onFrame sees every frame.
onFrame: (f) => {
if (f.type === 'transcript.voice' && f.audio) {
void writeFile(`es-${f.sentenceId}-${f.part ?? 0}.ogg`, Buffer.from(f.audio, 'base64'));
}
},
},
);
void session.sendFile(createReadStream('./talk.mp3'));
let sentences: SentenceSegment[] = [];
for await (const ev of session) {
if (ev.type !== 'transcript.sentence') continue;
sentences = applySentence(sentences, ev.sentence); // upsert by sentenceId
if (ev.sentence.status === 'closed') console.log(`${ev.sentence.text}\n es: ${ev.sentence.translations?.es}`);
}
console.log(`${sentences.length} sentences; the Spanish clips are es-<sentence>-<part>.ogg`);
Python SDK
import asyncio
import base64
import os
from vocalstack import VocalStack
async def main() -> None:
async with VocalStack("https://api.vocalstack.com", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
catalogue = await vs.voices.available("es")
voice = next((v for v in catalogue.voices if v.default), catalogue.voices[0])
# A file at full speed here; a microphone works the same (Stream a microphone).
session = await vs.realtime.connect(
input={"kind": "file"},
spoken_language="en",
translate=["es"],
sentences={"enabled": True},
voice=[f"es:{voice.id}"],
)
async with session:
sender = asyncio.create_task(session.send_file("./talk.mp3"))
async for event in session:
if event.type == "transcript.sentence" and event.sentence.status == "closed":
print(event.sentence.text)
print(" es:", (event.sentence.translations or {}).get("es"))
elif event.type == "transcript.voice" and event.audio:
with open(f"es-{event.sentence_id}-{event.part or 0}.ogg", "wb") as f:
f.write(base64.b64decode(event.audio))
await sender
asyncio.run(main())
HTTP + WebSocket
// No SDK: fetch for the session, the built-in WebSocket for the stream (Node 22 or newer).
import { createReadStream } from 'node:fs';
import { writeFile } from 'node:fs/promises';
const API = 'https://api.vocalstack.com';
const auth = { Authorization: `Bearer ${process.env.VOCALSTACK_API_KEY}` };
const cat = (await (await fetch(`${API}/v1/voices/available?lang=es`, { headers: auth })).json()) as { voices: { id: string; default: boolean }[] };
const voice = cat.voices.find((v) => v.default) ?? cat.voices[0];
const res = await fetch(`${API}/v1/realtime/sessions`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
input: { kind: 'file' },
spokenLanguage: 'en',
translate: ['es'],
sentences: { enabled: true },
voice: [`es:${voice.id}`],
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const ws = new WebSocket(((await res.json()) as { streamUrl: string }).streamUrl);
ws.addEventListener('message', async (m: MessageEvent) => {
if (typeof m.data !== 'string') return;
const f = JSON.parse(m.data);
if (f.type === 'session.ready') {
for await (const chunk of createReadStream('./talk.mp3', { highWaterMark: 256 * 1024 })) ws.send(new Uint8Array(chunk as Buffer));
ws.send(JSON.stringify({ type: 'eof' }));
}
if (f.type === 'transcript.sentence' && f.sentence.status === 'closed') console.log(`${f.sentence.text}\n es: ${f.sentence.translations?.es}`);
if (f.type === 'transcript.voice' && f.audio) await writeFile(`es-${f.sentenceId}-${f.part ?? 0}.ogg`, Buffer.from(f.audio, 'base64'));
if (f.type === 'session.done') ws.close();
});