Text to Speech
Send text, get audio. 322 neural voices across 75 languages, streamed as MP3 as it is synthesised, with the voice following the language of the text.
If you send us text, we send you audio back. POST /api/public/tts turns an answer into speech and streams the MP3 as it is synthesised, in the language of the text — so a Bengali answer is read by a Bengali voice rather than by an English one sounding out Bengali letters.
POST https://zyraxonai.lovable.app/api/public/tts — send text, get streamed MP3.
Voices: GET https://zyraxonai.lovable.app/api/public/tts/voices — 322 voices across 75 languages.
No key: both are public.The simplest call there is
curl -s https://zyraxonai.lovable.app/api/public/tts \
-H "Content-Type: application/json" \
-d '{"text":"Hello from ZYRAXON.","locale":"en"}' \
-o answer.mp3
# Play it
ffplay -nodisp -autoexit answer.mp3 # macOS: afplay Windows: startThe audio starts arriving before the sentence is finished being synthesised, so a client can begin playing almost immediately rather than waiting for a whole file. The response is Content-Type: audio/mpeg, with X-ZYRAXON-Voice naming the voice that actually spoke.
The voice follows the language
The endpoint reads the script of the text you send and picks a voice for it. Bengali text is read by a Bengali voice, Devanagari by a Hindi one, Arabic by an Arabic one, and Latin script falls back to English. This is what makes a Bengali answer audible at all: an English voice reading Bengali does not pronounce the letters.
# Bengali
curl -s https://zyraxonai.lovable.app/api/public/tts -H "Content-Type: application/json" \
-d '{"text":"আমি একটি ZYRAXON মডেল।","locale":"bn"}' -o bn.mp3
# Hindi, naming the voice exactly
curl -s https://zyraxonai.lovable.app/api/public/tts -H "Content-Type: application/json" \
-d '{"text":"नमस्ते","voice":"hi-IN-SwaraNeural"}' -o hi.mp3
# Arabic
curl -s https://zyraxonai.lovable.app/api/public/tts -H "Content-Type: application/json" \
-d '{"text":"مرحبا","locale":"ar"}' -o ar.mp3| Field | Required | What it does |
|---|---|---|
text | Yes | What to speak. Up to 5000 characters. Markdown is stripped first. |
voice | No | An exact voice name from the voices list. Wins over locale. |
locale | No | A language or region, e.g. bn, bn-IN, en-US. Picks a voice in that language. |
voice skips the voice-catalogue search, so the first byte of audio arrives sooner. It is worth doing in a loop where the language never changes.Which voices exist
curl -s https://zyraxonai.lovable.app/api/public/tts/voices | jq '{count, languages: (.languages|length)}'
# {"count": 322, "languages": 75}
# Every Bengali voice
curl -s https://zyraxonai.lovable.app/api/public/tts/voices | jq -r '.data[] | select(.locale | startswith("bn")) | .name'
# bn-BD-NabanitaNeural
# bn-BD-PradeepNeural
# bn-IN-BashkarNeural
# bn-IN-TanishaaNeural
# Every language the catalogue covers
curl -s https://zyraxonai.lovable.app/api/public/tts/voices | jq -r '.locales[]' | sort -u | head -20Speaking a streaming answer
For a live conversation the audio must start before the answer is finished. Synthesising the whole reply first means seconds of silence. The pattern is to split the answer into sentences and speak each one as it is ready, queued in order so nothing is reordered.
// Speak an answer while it is still being written.
// A reply arrives token by token; feed the growing text and speak each
// completed sentence as it appears. The first sentence is heard while the
// model is still writing the second.
const TTS = "https://zyraxonai.lovable.app/api/public/tts";
const queue: string[] = [];
let buffer = "";
let speaking = false;
async function speak(text: string) {
const res = await fetch(TTS, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }), // the locale is detected from the text
});
const audio = new Audio(URL.createObjectURL(await res.blob()));
await new Promise<void>((done) => {
audio.onended = () => done();
audio.onerror = () => done();
audio.play();
});
}
async function pump() {
if (speaking) return;
speaking = true;
while (queue.length) await speak(queue.shift()!);
speaking = false;
}
// Call this on every streamed token.
function feed(chunk: string) {
buffer += chunk;
// Cut after each sentence terminator, including the Bengali danda.
let at = -1;
for (let i = 0; i < buffer.length; i++) if (/[.!?।॥]/.test(buffer[i]!)) at = i;
if (at >= 0) {
queue.push(buffer.slice(0, at + 1).trim());
buffer = buffer.slice(at + 1).trimStart();
void pump();
}
}
// When the answer ends, speak whatever is left.
function finish() {
if (buffer.trim()) queue.push(buffer.trim());
buffer = "";
void pump();
}। (U+0964) and sometimes ॥. A splitter that only looks for . will hand the whole paragraph to the endpoint as one sentence and defeat the point of streaming. The sample above includes both.What gets spoken
The endpoint speaks what a person would say, not the raw Markdown. A reply containing a code block is not read character by character — that would be unusable — so a fence becomes a short spoken note, links become their label, and the punctuation goes.
| In the answer | What is spoken |
|---|---|
| A fenced code block | “A code block, 12 lines.” |
[the docs](/docs/getting-started) | “the docs” |
| A bare URL | “a link” |
**important** | “important” |
## Heading | “Heading” |
Which engine answers
Synthesis runs on Microsoft Edge's Read Aloud engine — the same one behind the browser feature — through the same protocol. That is what gives 322 neural voices in 75 languages with no key to buy and no per-character bill. Audio is MP3 at 24 kHz mono.
src/lib/tts/edge-speech.server.ts). If the service changes its contract, a second backend is added beside the first and the fallback order changes — no caller is touched.Errors
| Status | Means | What to do |
|---|---|---|
400 | text is missing, or nothing remained after markup was removed. | Send the answer's text. |
413 | The text is over 5000 characters. | Split it and speak it in parts. |
502 | The speech engine refused the connection. | Retry; it is usually transient. |