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

> Emotion tags work on Paxa TTS Flash (`paxa-tts-flash-v1`) only. On Paxa TTS Flash v2, every voice reads [directions](https://paxalabs.com/docs/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](https://paxalabs.com/docs/api/text-to-speech), 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](https://paxalabs.com/docs/directions) instead.

curl:

```bash
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.mp3
```

TypeScript:

```typescript
import { 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()));
```

Python:

```python
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](https://paxalabs.com/docs/api/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](https://paxalabs.com/docs/errors#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](https://paxalabs.com/docs/errors#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](https://paxalabs.com/docs/credits) 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](https://paxalabs.com/docs/live-streaming) 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.

> Timing spans cover the spoken text, and a tag takes no span. The [OpenAI-compatible endpoint](https://paxalabs.com/docs/api/speech) reads tags in `input` under the same rules.
