Text to Speech API
Convert text into natural speech using a project-scoped API key.
Use cases
Quick start
Set the API base URL and API key first. If you do not have a key, see Get your API key.
All endpoints use the X-API-Key request header. The API key determines the account, project, available capabilities, rate limits, and billing attribution.
export VUILABS_API_BASE_URL="https://api.vuilabs.ai"
export VUILABS_API_KEY="your-api-key"
Before running an example, replace YOUR_PROJECT_VOICE_ID with a voice available to this API key. It is deliberately not a shared demo voice. For a cloned voice, first create it with the Voice Cloning API, then use the same ID here.
Choose a model
| Model | model_id | Delivery | Voice and parameter rules |
|---|---|---|---|
| Luna-TTS | luna-tts | Synchronous or streaming | Supports Luna system voices and cloned voices. Supports speed, text normalization, and expression tags. |
| Luna-TTS Character | luna-tts-character | Synchronous only | Uses only Character system voices. Supports volume, scene, and expressivity. Does not support cloned voices, speed, tags, or streaming. The synchronous HTTP API accepts but ignores text_normalization; omit this field. |
Luna-TTS Character example
{
"model_id": "luna-tts-character",
"text": "Welcome to VUI Labs.",
"voice_id": "jasper",
"volume": 0,
"scene": "normal"
}
curl --request POST \
--url "$VUILABS_API_BASE_URL/v1/text-to-speech" \
--header "X-API-Key: $VUILABS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"model_id": "luna-tts-character",
"text": "Welcome to VUI Labs.",
"voice_id": "jasper",
"volume": 0,
"scene": "normal"
}' \
--output character.wav
- cURL
- JavaScript
- Python
curl --request POST \
--url "$VUILABS_API_BASE_URL/v1/text-to-speech" \
--header "X-API-Key: $VUILABS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"text": "Your order is ready.",
"voice_id": "YOUR_PROJECT_VOICE_ID",
"language": "en",
"speed": 1
}' \
--output speech.wav
import { writeFile } from 'node:fs/promises';
const response = await fetch(`${process.env.VUILABS_API_BASE_URL}/v1/text-to-speech`, {
method: 'POST',
headers: {
'X-API-Key': process.env.VUILABS_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: 'Your order is ready.', voice_id: 'YOUR_PROJECT_VOICE_ID', language: 'en', speed: 1 }),
});
if (!response.ok) throw new Error(await response.text());
await writeFile('speech.wav', Buffer.from(await response.arrayBuffer()));
import os
import requests
response = requests.post(
f"{os.environ['VUILABS_API_BASE_URL']}/v1/text-to-speech",
headers={"X-API-Key": os.environ["VUILABS_API_KEY"]},
json={"text": "Your order is ready.", "voice_id": "YOUR_PROJECT_VOICE_ID", "language": "en", "speed": 1},
)
response.raise_for_status()
with open("speech.wav", "wb") as output:
output.write(response.content)
Specify a voice
Pass a voice_id enabled for the current project. Test it with a short representative text before using it for longer content.
- cURL
- JavaScript
- Python
curl --request POST \
--url "$VUILABS_API_BASE_URL/v1/text-to-speech" \
--header "X-API-Key: $VUILABS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"text": "This request uses a specific voice.",
"voice_id": "YOUR_PROJECT_VOICE_ID",
"language": "en",
"speed": 1
}' \
--output speech.wav
import { writeFile } from 'node:fs/promises';
const response = await fetch(`${process.env.VUILABS_API_BASE_URL}/v1/text-to-speech`, {
method: 'POST',
headers: {
'X-API-Key': process.env.VUILABS_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: 'This request uses a specific voice.',
voice_id: 'YOUR_PROJECT_VOICE_ID',
language: 'en',
speed: 1,
}),
});
if (!response.ok) throw new Error(await response.text());
await writeFile('speech.wav', Buffer.from(await response.arrayBuffer()));
import os
import requests
response = requests.post(
f"{os.environ['VUILABS_API_BASE_URL']}/v1/text-to-speech",
headers={"X-API-Key": os.environ["VUILABS_API_KEY"]},
json={
"text": "This request uses a specific voice.",
"voice_id": "YOUR_PROJECT_VOICE_ID",
"language": "en",
"speed": 1,
},
)
response.raise_for_status()
with open("speech.wav", "wb") as output:
output.write(response.content)
Control delivery
Start with a short representative sentence, then adjust one control at a time so the result is easy to compare.
- Speed: Luna-TTS only. Use a value from
0.6to1.2. - Volume: Luna-TTS and Character support
-5to5. - Character scene: Character only.
normalis the default everyday delivery;whisperis a close whispering delivery. - Expression tags: Luna-TTS only. Put official tags below directly in
text; Character does not support tags.
| Official tags | Official tags | Official tags |
|---|---|---|
[laughs] | [chuckles] | [gasps] |
[sighs] | [inhales] | [breath] |
[exhale] | [coughs] | [clears throat] |
[sniffles] | [snorts] | [hisses] |
[tsks] | [groans] |
API details
Request modes
POST /v1/text-to-speechPlay or save the audio after receiving the complete file.
POST /v1/text-to-speech/streamOnly Luna-TTS supports streaming. Character requests are rejected.
Request headers
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Project API key |
Content-Type | Yes | application/json |
JSON body
| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Text to synthesize. Maximum 500 weighted characters: each Chinese character counts as 2; every other Unicode character counts as 1. |
language | string | No | A language code enabled for the current TTS account. |
voice_id | string | Yes | A voice ID enabled for the current project; the request is rejected if omitted. |
model_id | string | No | luna-tts (default) or luna-tts-character. |
speed | number | No | Luna-TTS only; use 0.6–1.2. |
volume | number | No | Luna-TTS and Character only; use -5 to 5. |
scene | string | No | Character only: normal (default) or whisper. |
expressivity | string | No | Character only: normal (default) or strong. |
text_normalization | boolean | No | Luna-TTS: true (default) enables text normalization before synthesis; false preserves the input text. Character accepts but ignores this field. |
audio_format | string | No | wav (default) or pcm. Both complete and streaming audio support PCM; streaming supports Luna-TTS only. |
Use the voices and languages available to the current project in the Console.
Text normalization
Only Luna-TTS applies text_normalization. Send a JSON boolean, not a quoted string. true is the default and normalizes the text before synthesis; explicitly send false to preserve the input text. Every streaming segment uses the same setting. Character ignores this field. Its synchronous HTTP API accepts boolean true or false, but neither value controls text normalization for Character. Omit this field for Character requests.
{
"text": "Your order total is $36.50.",
"voice_id": "YOUR_PROJECT_VOICE_ID",
"text_normalization": true
}
For Luna-TTS, omit text_normalization or set it to true to normalize text before synthesis. Explicitly set it to false to preserve the input text. In Console Speech Synthesis, the Text Normalization toggle sends this choice with each generation.
Synchronous response formats
POST /v1/text-to-speech returns a complete WAV file with Content-Type: audio/wav when audio_format is omitted or set to wav. For raw PCM, send "audio_format": "pcm" in the JSON body:
{
"text": "Your order is ready.",
"voice_id": "YOUR_PROJECT_VOICE_ID",
"audio_format": "pcm"
}
The PCM response contains signed 16-bit little-endian audio (PCM_S16LE) without a WAV header and uses Content-Type: audio/pcm. Save it with a .pcm extension and configure playback using these response headers:
| Response header | Meaning |
|---|---|
X-Audio-Sample-Rate | Actual sample rate in Hz. |
X-Audio-Channels | Actual number of channels. |
X-Audio-Bit-Depth | Bit depth; 16 for PCM. |
The sample rate and channel count preserve the synthesized audio; the API does not resample it. Do not assume a fixed 24000 Hz rate or mono audio. WAV and PCM work with the voices and models available to the synchronous endpoint. Other formats are rejected.
Streaming generation
POST /v1/text-to-speech/stream supports only model_id: "luna-tts". It returns ordered audio bytes in the HTTP response body, not SSE, JSON events, or base64. Character does not support this endpoint.
audio_format | Content-Type | Audio bytes |
|---|---|---|
Omitted or wav | audio/wav | WAV audio, including its container header; use a WAV-capable decoder. |
pcm | audio/pcm | Raw signed 16-bit little-endian PCM (PCM16LE), without a WAV header. |
For PCM, configure your decoder using the response headers. The sample rate and channel count reflect the generated audio; do not hard-code a sample rate.
| PCM response header | Meaning |
|---|---|
X-Audio-Sample-Rate | Sample rate in Hz. |
X-Audio-Channels | Number of channels. |
X-Audio-Bit-Depth | Bits per sample; 16 for PCM16LE. |
The PCM stream contains audio samples only, including when speech is generated in multiple segments. Consume bytes in order. HTTP chunk boundaries are not audio frame boundaries: retain any incomplete frame for the next read.
Save a PCM stream
Use the API base URL and key configured above, and replace YOUR_PROJECT_VOICE_ID with an enabled Luna-TTS voice. The command saves the raw samples and response headers separately.
curl --fail --silent --show-error --no-buffer \
--request POST \
--url "$VUILABS_API_BASE_URL/v1/text-to-speech/stream" \
--header "X-API-Key: $VUILABS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"model_id": "luna-tts",
"text": "Your order is ready.",
"voice_id": "YOUR_PROJECT_VOICE_ID",
"audio_format": "pcm"
}' \
--dump-header speech-pcm.headers \
--output speech.pcm
Read speech-pcm.headers before playback: a .pcm file does not store its sample rate or channel count. Saving bytes is not a playback example.
Before audio starts, failures use the normal JSON error response and HTTP error status. Once streaming has started, a failure interrupts the audio response; no JSON error is appended to the audio. A 200 status alone does not prove the stream completed successfully. Preserve transport errors and handle incomplete playback.
For both endpoints, an HTTP 200 response body contains audio, not JSON. Content-Type is the actual audio MIME type.