Text to speech
Turn text into audio — Persian on one engine, twenty-three other languages on another, chosen for you automatically.
POST /v1/audio/speech takes text and returns audio bytes, in the same shape OpenAI's endpoint uses. Behind it are two different engines picked by language, not one model that speaks everything.
/v1/audio/speech| Parameter | Type | Description |
|---|---|---|
| input Required | string | The text to speak. 8000 characters max. |
| model Required | string | The only deployment name, larsa-tts. Required — the gateway uses it to route the request, and a call that omits it fails.Default: larsa-tts |
| voice Required | string | Required, even though the value itself has no effect — each engine speaks with exactly one voice right now, so any value you send, including OpenAI voice names like alloy, is accepted and ignored. Omitting the field is not the same as sending an ignored value: the request fails.Default: default |
| language Optional | string | Which engine, and how the text is pronounced — see below. Auto-detected from the text when omitted, which is reliable for Persian and risky for everything else. |
| response_format Optional | string | mp3 · opus · aac · flac · wav.Default: mp3 |
| guard Optional | boolean | Synthesise, transcribe the result back, and regenerate if the words don't match closely enough — see below. Costs a full extra round trip through speech-to-text. Default: false |
| max_retries Optional | integer | Only matters with guard: true — the best of up to max_retries + 1 attempts is returned.Default: 2 |
model and voice must both be present in the JSON body — not just input. Leave either one out and the response is {"error": {"message": "Internal server error"}}, 76 bytes, with nothing to say which field was missing. There is no way to work it out from the response itself; every sample below sends both, every time.curl -s https://api.console.larsa.larsima.com/v1/audio/speech \
-H "Authorization: Bearer $LARSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "larsa-tts",
"voice": "default",
"input": "Se dictará sentencia en un plazo máximo de veinte días.",
"language": "es",
"response_format": "mp3"
}' --output speech.mp3
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.console.larsa.larsima.com/v1",
api_key=os.environ["LARSA_API_KEY"])
resp = client.audio.speech.create(
model="larsa-tts",
voice="default", # required; the value itself has no effect -- see below
input="Se dictará sentencia en un plazo máximo de veinte días.",
response_format="mp3",
extra_body={"language": "es"},
)
resp.stream_to_file("speech.mp3")
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.console.larsa.larsima.com/v1",
apiKey: process.env.LARSA_API_KEY,
});
const resp = await client.audio.speech.create({
model: "larsa-tts",
voice: "default", // required; the value itself has no effect -- see below
input: "Se dictará sentencia en un plazo máximo de veinte días.",
response_format: "mp3",
language: "es",
});
fs.writeFileSync("speech.mp3", Buffer.from(await resp.arrayBuffer()));
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.console.larsa.larsima.com/v1",
apiKey: process.env.LARSA_API_KEY,
});
const resp = await client.audio.speech.create({
model: "larsa-tts",
voice: "default" as any, // required; the value itself has no effect -- see below
input: "Se dictará sentencia en un plazo máximo de veinte días.",
response_format: "mp3",
// @ts-expect-error -- gateway-specific field, not in the SDK's types yet
language: "es",
});
fs.writeFileSync("speech.mp3", Buffer.from(await resp.arrayBuffer()));
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]any{
"model": "larsa-tts",
"voice": "default",
"input": "Se dictará sentencia en un plazo máximo de veinte días.",
"language": "es",
"response_format": "mp3",
})
req, _ := http.NewRequest("POST",
"https://api.console.larsa.larsima.com/v1/audio/speech", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("LARSA_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := os.Create("speech.mp3")
defer out.Close()
io.Copy(out, resp.Body)
}
// Cargo.toml: reqwest = { version = "0.12", features = ["json"] }
// serde_json = "1", tokio = { version = "1", features = ["full"] }
use serde_json::json;
use std::{env, fs};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = env::var("LARSA_API_KEY")?;
let bytes = reqwest::Client::new()
.post("https://api.console.larsa.larsima.com/v1/audio/speech")
.bearer_auth(key)
.json(&json!({
"model": "larsa-tts",
"voice": "default",
"input": "Se dictará sentencia en un plazo máximo de veinte días.",
"language": "es",
"response_format": "mp3"
}))
.send()
.await?
.bytes()
.await?;
fs::write("speech.mp3", bytes)?;
Ok(())
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class TextToSpeech {
public static void main(String[] args) throws Exception {
String body = "{\"model\":\"larsa-tts\",\"voice\":\"default\","
+ "\"input\":\"Se dictar\u00e1 sentencia en un plazo m\u00e1ximo de veinte d\u00edas.\","
+ "\"language\":\"es\",\"response_format\":\"mp3\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.console.larsa.larsima.com/v1/audio/speech"))
.header("Authorization", "Bearer " + System.getenv("LARSA_API_KEY"))
.header("Content-Type", "application/json; charset=utf-8")
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<byte[]> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofByteArray());
Files.write(Path.of("speech.mp3"), response.body());
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var payload = new {
model = "larsa-tts",
voice = "default",
input = "Se dictará sentencia en un plazo máximo de veinte días.",
language = "es",
response_format = "mp3",
};
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("LARSA_API_KEY"));
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.console.larsa.larsima.com/v1/audio/speech", content);
await File.WriteAllBytesAsync("speech.mp3", await response.Content.ReadAsByteArrayAsync());
<?php
$payload = [
"model" => "larsa-tts",
"voice" => "default",
"input" => "Se dictará sentencia en un plazo máximo de veinte días.",
"language" => "es",
"response_format" => "mp3",
];
$ch = curl_init("https://api.console.larsa.larsima.com/v1/audio/speech");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("LARSA_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
file_put_contents("speech.mp3", curl_exec($ch));
require "net/http"
require "json"
require "uri"
uri = URI("https://api.console.larsa.larsima.com/v1/audio/speech")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('LARSA_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = {
model: "larsa-tts",
voice: "default",
input: "Se dictará sentencia en un plazo máximo de veinte días.",
language: "es",
response_format: "mp3",
}.to_json
response = http.request(request)
File.binwrite("speech.mp3", response.body)
Two engines, chosen by language
No open engine covers Persian and the European languages at usable quality together, so this deployment doesn't pretend one does — it routes.
| Language | Engine | Note |
|---|---|---|
| Persian | Aava (Orpheus 3B, Apache-2.0) | The only engine used for Persian. |
| 23 other languages | Chatterbox Multilingual (MIT) | Arabic, Chinese, Danish, Dutch, English, Finnish, French, German, Greek, Hebrew, Hindi, Italian, Japanese, Korean, Malay, Norwegian, Polish, Portuguese, Russian, Spanish, Swahili, Swedish, Turkish. Not Persian. |
Say the language — don't rely on guessing
Without language, the engine is picked by counting Persian characters: more than a quarter of the text and it goes to Aava; otherwise it goes to Chatterbox pronouncing English — regardless of what language the text is actually in. Spanish text sent with no language field is read aloud with English phonemes. Set language to the two-letter code (es, fr, de, …) for anything that isn't Persian or English, every time.
Response headers
| Header | Meaning |
|---|---|
X-Lardad-Engine | aava or chatterbox. |
X-Lardad-Language | The language actually used to pick the engine. |
X-Lardad-Duration | Length of the audio, in seconds. |
X-Lardad-Elapsed | Wall-clock time the request took, in seconds. |
X-Lardad-Guard-Score | Only present with guard: true — the fraction of input words the readback recognised, 0 to 1. |
Measured quality, and the failure both engines share
Quality here means round-trip word error rate: synthesise a sentence, transcribe the audio back with the speech-to-text service, and compare to the original. Aava scored 22.0% on the speaker's own recordings — clearer than the human reading the same text, which scored 41.5%. Chatterbox on Spanish scored 2.3%. On French it was inconsistent: 0.0% on one sentence and 113% on another, where it read the sentence correctly and then kept going, inventing words that were never in the input.
guard: true is the check: it listens to what it just generated, and regenerates when the words don't come back close enough. It costs a second or two more; for anything a person hears unattended, it's worth every millisecond.curl -s https://api.console.larsa.larsima.com/v1/audio/speech \
-H "Authorization: Bearer $LARSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "larsa-tts",
"voice": "default",
"input": "مهلت تجدیدنظرخواهی بیست روز از تاریخ ابلاغ است.",
"guard": true,
"max_retries": 2
}' --output speech.mp3 -D -
# X-Lardad-Engine: aava
# X-Lardad-Language: fa
# X-Lardad-Guard-Score: 0.94