Emotion tags on Paxa TTS Flash (v1)
Emotion tags are bracketed words such as [happy] or [sad:0.5] written into the text of a speech request. The voices that read them, the tag list, weights, billing, and errors.
Note
Emotion tags work on Paxa TTS Flash (paxa-tts-flash-v1) only. On Paxa TTS Flash v2, every voice reads directions written in natural language, such as [whispering] or [calm, measured].
An emotion tag is a word in square brackets, written into the text of POST /v1/tts, that changes how a voice delivers what follows it. The delivery changes at the tag and holds until the next tag. Text before the first tag keeps the voice's own delivery. The experimental voices Khanom Chan and Kaprao read tags. Every other v1 voice skips them, and every Paxa TTS Flash v2 voice reads direction tags instead.
curl -X POST https://api.paxalabs.com/v1/tts \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "[happy] ได้ข่าวว่าสอบผ่านแล้ว ดีใจด้วยนะ [sad:0.6] แต่ต่อไปคงไม่ค่อยได้เจอกันแล้ว",
"voice": "khanomchan",
"model": "paxa-tts-flash-v1"
}' \
--output speech.mp3import { writeFile } from "node:fs/promises";
const response = await fetch("https://api.paxalabs.com/v1/tts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "[happy] ได้ข่าวว่าสอบผ่านแล้ว ดีใจด้วยนะ [sad:0.6] แต่ต่อไปคงไม่ค่อยได้เจอกันแล้ว",
voice: "khanomchan",
model: "paxa-tts-flash-v1",
}),
});
if (!response.ok) throw new Error(`TTS failed: ${response.status}`);
await writeFile("speech.mp3", Buffer.from(await response.arrayBuffer()));import os
import requests
response = requests.post(
"https://api.paxalabs.com/v1/tts",
headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"},
json={"text": "[happy] ได้ข่าวว่าสอบผ่านแล้ว ดีใจด้วยนะ [sad:0.6] แต่ต่อไปคงไม่ค่อยได้เจอกันแล้ว", "voice": "khanomchan", "model": "paxa-tts-flash-v1"},
)
response.raise_for_status()
with open("speech.mp3", "wb") as f:
f.write(response.content)Voices that read tags#
| Voice | id | Gender |
|---|---|---|
| Khanom Chan | khanomchan | Female |
| Kaprao | kaprao | Male |
GET /v1/voices lists the tags each voice reads in emotion_tags, empty for a voice that skips them, and marks these voices experimental: true. An experimental voice keeps its id, and its delivery can change while we tune it.
Tag list#
The voices above read these words, each written before the text it should color: [neutral], [calm], [normal], [plain], [happy], [excited], [cheerful], [joyful], [amazed], [laughing], [sad], [crying], [sorrowful], [gloomy], [disappointed], [angry], [shouting], [furious], [annoyed], [frustrated], [afraid], [scared], [fearful], [nervous], [terrified], [anxious].
How tags are read#
- Case is ignored, and
_and-read as spaces:[Sad]and[sad_]are both[sad]. - A bracketed word outside the tag list, such as
[laughs], answers 400 tag_invalid. - Brackets that do not hold a word, such as
[1]or[ไทย], are text and are spoken. A word outside the list followed by a colon, such as[Fig:2]or[note: see below], is text too. - Text that holds only tags answers 422 unspeakable_text before anything is charged.
Set a weight#
A weight after a colon sets how strongly the tag applies, from 0 to 1, as in [sad:0.5]. Write it as a number from 0 to 1 or as a percentage, such as [sad:.25], [sad:0,5], or [sad:50%]. The API reads it to the thousandth, and a value past either end reads as that end: [angry:1.5] is [angry]. A tag at weight 0 does not apply. A weight that is not a number, such as [sad:half], answers tag_invalid.
Billing#
Tags are text. Every character of a tag counts toward the request's length and its charge, at the model's per-character rate. The 81-character example above, tags included, costs 0.81 credits. Credits and billing covers rounding and the minimum.
Other voices#
A v1 voice that reads no tags skips every tag on the list, whatever its weight, and reads the rest of the text. It refuses nothing. On those voices a bracketed word outside the list, such as [laughs], is text and is spoken. The characters of a skipped tag still count toward the charge.
Tags on a live connection#
On a live connection a text frame whose tag the voice cannot read answers an error event with tag_invalid, and the buffer keeps the text it held before the frame. Auto-flush holds back a tag still open at the end of the buffer, and a tag split across frames is read whole. One still open past the length threshold that flushes untagged text is read as text and is not checked as a tag. Tags that no words follow yet wait in the buffer for them. Each synthesis leg opens in the delivery the previous leg ended in. Those opening tags carry no charge. They count toward max_chars and may fill at most half of it. A frame that would leave longer opening tags, or take the tags waiting in the buffer and the opening tags past max_chars, answers text_too_long. cancel returns the connection to the voice's own delivery.
Note
Timing spans cover the spoken text, and a tag takes no span. The OpenAI-compatible endpoint reads tags in input under the same rules.