Skip to main content

Text to Speech API

Convert text into natural speech using a project-scoped API key.

Use cases​

Voiceovers and narrationUse for product demos, videos, stories, and long-form content.
Conversational experiencesGenerate assistant replies as audio, or stream them as they are generated.
Accessibility and IVRRead content aloud and build phone menus and voice guidance.
Product notificationsTurn alerts, status updates, and personalized messages into speech.

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​

Modelmodel_idDeliveryVoice and parameter rules
Luna-TTSluna-ttsSynchronous or streamingSupports Luna system voices and cloned voices. Supports speed, text normalization, and expression tags.
Luna-TTS Characterluna-tts-characterSynchronous onlyUses 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 --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

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 --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

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.6 to 1.2.
  • Volume: Luna-TTS and Character support -5 to 5.
  • Character scene: Character only. normal is the default everyday delivery; whisper is a close whispering delivery.
  • Expression tags: Luna-TTS only. Put official tags below directly in text; Character does not support tags.
Official tagsOfficial tagsOfficial tags
[laughs][chuckles][gasps]
[sighs][inhales][breath]
[exhale][coughs][clears throat]
[sniffles][snorts][hisses]
[tsks][groans]

API details​

Request modes​

COMPLETE AUDIOPOST /v1/text-to-speech

Play or save the audio after receiving the complete file.

LUNA-TTS STREAMING AUDIOPOST /v1/text-to-speech/stream

Only Luna-TTS supports streaming. Character requests are rejected.

Request headers​

HeaderRequiredDescription
X-API-KeyYesProject API key
Content-TypeYesapplication/json

JSON body​

FieldTypeRequiredDescription
textstringYesText to synthesize. Maximum 500 weighted characters: each Chinese character counts as 2; every other Unicode character counts as 1.
languagestringNoA language code enabled for the current TTS account.
voice_idstringYesA voice ID enabled for the current project; the request is rejected if omitted.
model_idstringNoluna-tts (default) or luna-tts-character.
speednumberNoLuna-TTS only; use 0.6–1.2.
volumenumberNoLuna-TTS and Character only; use -5 to 5.
scenestringNoCharacter only: normal (default) or whisper.
expressivitystringNoCharacter only: normal (default) or strong.
text_normalizationbooleanNoLuna-TTS: true (default) enables text normalization before synthesis; false preserves the input text. Character accepts but ignores this field.
audio_formatstringNowav (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 headerMeaning
X-Audio-Sample-RateActual sample rate in Hz.
X-Audio-ChannelsActual number of channels.
X-Audio-Bit-DepthBit 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_formatContent-TypeAudio bytes
Omitted or wavaudio/wavWAV audio, including its container header; use a WAV-capable decoder.
pcmaudio/pcmRaw 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 headerMeaning
X-Audio-Sample-RateSample rate in Hz.
X-Audio-ChannelsNumber of channels.
X-Audio-Bit-DepthBits 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.

TREAT THE RESPONSE AS AUDIO

For both endpoints, an HTTP 200 response body contains audio, not JSON. Content-Type is the actual audio MIME type.