# Paxa Labs API Text to speech for Thai, English, and Mandarin Chinese, Thai-first multilingual transcription, OCR over Thai documents and other languages, and translation into Thai from any language. Document Extraction pulls typed fields from documents against a schema you write. Authenticate with an API key from the dashboard. Send it as 'Authorization: Bearer pxa_...' or in the x-api-key header. Authorization wins when both are present. Costs are charged in credits before inference and refunded automatically when inference fails. Every error is an RFC 9457 problem+json body whose title field is a stable machine-readable code. Report a wrong output or an API problem to POST /v1/feedback with the request's x-request-id. Base URL: https://api.paxalabs.com. Documentation: https://paxalabs.com/docs. Contact: hello@paxalabs.com. # Paxa Labs API The Paxa API is one HTTP API for Thai, English, and Mandarin Chinese text to speech, Thai-first speech to text, document OCR, and translation into Thai. Create a key, send one POST request, and get audio back. The Paxa API converts text into speech with [Paxa TTS Flash v2](https://paxalabs.com/text-to-speech), the flagship Thai, English, and Mandarin Chinese TTS model. [Transcription](https://paxalabs.com/docs/speech-to-text) turns recordings of Thai speech, and of other languages, into verbatim text with word timestamps. [OCR](https://paxalabs.com/docs/ocr) reads PDFs and images into Markdown or structured blocks, and [Document Extraction](https://paxalabs.com/docs/extraction) fills a schema of typed fields from them. [Translation](https://paxalabs.com/docs/translation) renders any language into Thai with register control and glossaries. Requests are priced in credits. Audio arrives as a complete file or [streamed](https://paxalabs.com/docs/streaming), with [word-level timing](https://paxalabs.com/docs/timestamps) on request. A [live WebSocket](https://paxalabs.com/docs/live-streaming) carries two-way conversations. An [OpenAI-compatible endpoint](https://paxalabs.com/docs/openai-compatibility) accepts existing OpenAI SDK code after a change of base URL. ## Send your first request 1. Create an API key: Create a key in the [dashboard](https://paxalabs.com/app/keys). The full key is shown once, at creation. Store it as `PAXA_API_KEY` in your environment. 2. Send the request: POST your text and a `voice` to `/v1/tts`. The response body is the MP3 audio. 3. Play the audio: The example saves `speech.mp3`. New accounts start with 100 free credits, which covers about 10,000 characters. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2" }' \ --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: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", voice: "khanomkrok", model: "paxa-tts-flash-v2", }), }); 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": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2"}, ) response.raise_for_status() with open("speech.mp3", "wb") as f: f.write(response.content) ``` Coding agents start at [/llms.txt](https://paxalabs.com/llms.txt). See [coding agents](https://paxalabs.com/docs/agents) for the skill and the full corpus, or connect the [MCP server](https://paxalabs.com/docs/mcp) to give an agent the API as tools. ## Base URL Every endpoint sits under `https://api.paxalabs.com`. The API is served over HTTPS only. Every failure returns an RFC 9457 problem+json body carrying a stable machine-readable code. See [Errors](https://paxalabs.com/docs/errors) for the full catalog. > The API is in beta. When synthesis capacity is unavailable, requests answer `provider_unavailable` and nothing is charged. ## All pages - [Create and send API keys](https://paxalabs.com/docs/authentication) - [MCP server](https://paxalabs.com/docs/mcp) - [Load docs into a coding agent](https://paxalabs.com/docs/agents) - [Generate speech from text](https://paxalabs.com/docs/text-to-speech) - [Stream audio as it is generated](https://paxalabs.com/docs/streaming) - [Get word-level timestamps](https://paxalabs.com/docs/timestamps) - [Direct the delivery with tags](https://paxalabs.com/docs/directions) - [Add pauses](https://paxalabs.com/docs/pauses) - [Control variation with temperature](https://paxalabs.com/docs/temperature) - [Emotion tags on Paxa TTS Flash (v1)](https://paxalabs.com/docs/emotion-tags) - [Designed voices](https://paxalabs.com/docs/designed-voices) - [Two-way live speech over WebSocket](https://paxalabs.com/docs/live-streaming) - [Optimize latency](https://paxalabs.com/docs/latency) - [Transcribe recordings](https://paxalabs.com/docs/speech-to-text) - [Read documents with OCR](https://paxalabs.com/docs/ocr) - [Extract fields from documents](https://paxalabs.com/docs/extraction) - [Translate text into Thai](https://paxalabs.com/docs/translation) - [Credits, charges, and refunds](https://paxalabs.com/docs/credits) - [Rate limits and concurrency](https://paxalabs.com/docs/rate-limits) - [Make retries safe with idempotency keys](https://paxalabs.com/docs/idempotency) - [Error codes and responses](https://paxalabs.com/docs/errors) - [Use the OpenAI SDK with Paxa](https://paxalabs.com/docs/openai-compatibility) - [Switch from OpenAI text to speech and Whisper](https://paxalabs.com/docs/migrate-from-openai) - [Use Paxa in apps and frameworks](https://paxalabs.com/docs/integrations) - [Use Paxa with Open WebUI](https://paxalabs.com/docs/integrations/open-webui) - [Use Paxa with LibreChat](https://paxalabs.com/docs/integrations/librechat) - [Use Paxa with AnythingLLM](https://paxalabs.com/docs/integrations/anythingllm) - [Use Paxa with Home Assistant](https://paxalabs.com/docs/integrations/home-assistant) - [Use Paxa with SillyTavern](https://paxalabs.com/docs/integrations/sillytavern) - [Use Paxa with LiveKit Agents](https://paxalabs.com/docs/integrations/livekit) - [Use Paxa with Strands Agents](https://paxalabs.com/docs/integrations/strands) - [Synthesize speech](https://paxalabs.com/docs/api/text-to-speech) - [Synthesize speech (OpenAI-compatible)](https://paxalabs.com/docs/api/speech) - [Live speech](https://paxalabs.com/docs/api/live) - [Design a voice](https://paxalabs.com/docs/api/voice-design) - [List designed voices](https://paxalabs.com/docs/api/designed-voices) - [Save a designed voice](https://paxalabs.com/docs/api/save-voice) - [Rename a designed voice](https://paxalabs.com/docs/api/rename-voice) - [Delete a designed voice](https://paxalabs.com/docs/api/delete-voice) - [Transcribe a recording](https://paxalabs.com/docs/api/stt) - [Realtime transcription](https://paxalabs.com/docs/api/stt-live) - [Transcribe a recording (OpenAI-compatible)](https://paxalabs.com/docs/api/transcriptions) - [Read a document](https://paxalabs.com/docs/api/ocr) - [Extract fields from a document](https://paxalabs.com/docs/api/extract) - [Translate text](https://paxalabs.com/docs/api/translate) - [List models](https://paxalabs.com/docs/api/models) - [List voices](https://paxalabs.com/docs/api/voices) - [List voices for OpenAI-compatible apps](https://paxalabs.com/docs/api/audio-voices) - [List speech models for OpenAI-compatible apps](https://paxalabs.com/docs/api/audio-models) - [Get key, balance, and plan](https://paxalabs.com/docs/api/me) - [Send feedback](https://paxalabs.com/docs/api/feedback) - [List feedback](https://paxalabs.com/docs/api/feedback-list) - [Health check](https://paxalabs.com/docs/api/health) - [API changelog](https://paxalabs.com/docs/changelog) # Create and send API keys Every billed Paxa API request authenticates with an API key sent as a request header. Create keys in the dashboard, send them on each request, and cap what each key can spend. Every billed request to the Paxa API authenticates with an API key sent as a request header. Keys start with `pxa_` and belong to your account. A request without a valid key answers [401 unauthorized](https://paxalabs.com/docs/errors#unauthorized). The catalog endpoints ([models](https://paxalabs.com/docs/api/models), [voices](https://paxalabs.com/docs/api/voices)) and [health](https://paxalabs.com/docs/api/health) are public and need no key. ## Create a key Create keys on the [API keys page](https://paxalabs.com/app/keys) of the dashboard. The full key is shown exactly once, at creation. Only a hash is stored, and the key cannot be shown again. An account holds up to 20 keys, counting disabled ones. ## Send a key Send the key in the `Authorization` header with the `Bearer` scheme, or in the `x-api-key` header. When both are present, `Authorization` wins. A malformed `Authorization` header is rejected, and the server does not fall through to `x-api-key`. Authorization header: ```bash curl https://api.paxalabs.com/v1/me \ -H "Authorization: Bearer $PAXA_API_KEY" ``` x-api-key header: ```bash curl https://api.paxalabs.com/v1/me \ -H "x-api-key: $PAXA_API_KEY" ``` ## Set a spending cap A key can carry an optional credit limit. Set it at creation or edit it later in the dashboard. Once the key has spent that many credits, its requests answer [403 key_limit](https://paxalabs.com/docs/errors#key_limit) and nothing more is charged. Give each environment and each teammate its own capped key. ## Disable or delete a key Disabling a key is reversible. Requests signed with it answer 401 until you enable it again. Deleting a key is permanent and takes effect immediately. Usage already recorded stays in your history. > Keys are secrets. Call the API from your server and keep keys in environment variables. A key shipped in client-side code is public. If a key leaks, create a replacement and delete the leaked one. # MCP server @paxalabs/mcp gives any MCP client the Paxa API as tools. They speak through the machine's speakers, save speech to files with subtitles, listen through the microphone, transcribe recordings and video, read and extract documents, and translate into Thai. They also design new voices from a description. Install it for Claude Code, Claude Desktop, Cursor, or VS Code, and turn Claude Code into a voice mode with the paxa say hook. `@paxalabs/mcp` is the official Model Context Protocol server for the Paxa API. An agent connected to it can speak through the machine's speakers and read long content aloud as a managed playback queue. It also designs new voices from a written description, saves speech to audio files with matching subtitles, and listens for a spoken reply. The other tools transcribe a recording or a video, read PDFs and images with OCR, extract typed fields from documents, and translate any language into Thai. Each tool bills the same credits as the API endpoint behind it. The source is on [GitHub](https://github.com/paxalabs/mcp). > The server is in beta. The tool set is complete and tested end to end, and tool names and behavior may still change before 1.0. Report problems at [https://github.com/paxalabs/mcp/issues](https://github.com/paxalabs/mcp/issues). ## Install You need an [API key](https://paxalabs.com/app/keys?new=mcp). New accounts include 100 free credits. Claude Code: ```bash claude mcp add paxa -e PAXA_API_KEY=pxa_your_key_here -- npx -y @paxalabs/mcp ``` Claude Desktop, Cursor, VS Code: ```json { "mcpServers": { "paxa": { "command": "npx", "args": [ "-y", "@paxalabs/mcp" ], "env": { "PAXA_API_KEY": "pxa_your_key_here" } } } } ``` The JSON entry works for Claude Desktop (`claude_desktop_config.json`), Cursor (`~/.cursor/mcp.json`), VS Code, and any other client that launches stdio servers. Replace the placeholder with your key. Claude Desktop can also install the extension bundle from the [releases page](https://github.com/paxalabs/mcp/releases). Download the `.mcpb` file, open it with Claude Desktop, and enter your key in the extension settings. The bundle carries the server and its dependencies and needs no Node.js on the machine. ## Tools | Tool | What it does | Credits | | --- | --- | --- | | speak | Synthesize a short line and play it through the speakers, blocking until playback ends | 10 per 1,000 characters, 1.25 times that in a designed voice | | queue_speech | Read long content aloud: splits the text, synthesizes ahead while playing, and returns at once | 10 per 1,000 characters, 1.25 times that in a designed voice | | control_playback | Control the shared audio queue: `status`, `pause`, `resume`, `skip`, `clear` | Free | | play_audio | Play a local audio file through the speakers | Free | | text_to_speech | Synthesize speech to an audio file (mp3, opus, wav) without playing it. A long script is split and joined into one file, and `subtitles` writes a matching SRT or VTT file | 10 per 1,000 characters, times 1.25 with subtitles, 1.25 in a designed voice, or 1.5 with both | | design_voice | Make a new voice from a written description and play the line it speaks | 5 per call | | refine_voice | Change a draft voice in words, such as "a little lower and slower", and play its new line | 5 per call | | save_voice | Save a draft as one of your voices, with an id the speech tools accept | Free | | rename_voice | Rename one of your designed voices; its id and sound stay | Free | | delete_voice | Delete one of your designed voices and free its slot | Free | | listen | Record one spoken turn from the microphone and return its transcript. The API detects when the speaker stops talking | 12.5 per minute listened, silence included | | transcribe_audio | Transcribe a local recording or video, with speaker labels, word timings, SRT or VTT subtitles, the spoken or written convention, a vocabulary, and a list of pauses to cut | 8.33 per minute of audio | | ocr_document | OCR a local PDF, PNG, JPEG, or WebP into Markdown or structured blocks | 6.5 per page | | extract_fields | Fill a schema of typed fields (Thai IDs, dates, amounts, banks, line items) from a local PDF or image. Every value is printed in the document, or null with the reason | 13 per page, 19.5 for a schema over 50 fields | | translate_to_thai | Translate any language into Thai, with formality, glossary, and context controls | 25 per 1,000 characters | | send_feedback | Rate an output or report a problem to the Paxa team, tied to the request ids the tools print | Free | | list_voices | Your designed voices and slots, then the voice roster with character notes | Free | | list_models | Available models, limits, and pricing | Free | | get_account | Credit balance, plan, and rate limits | Free | All audio flows through one ordered queue and sounds never overlap. A `speak` line plays ahead of queued long-form segments, and `queue_speech` keeps a book or an article flowing by synthesizing the next segment while the current one plays. With a streaming-capable player installed, speech starts on the first bytes from the API; without one, it plays after the download completes. ## Design your own voice When no catalog voice fits, describe the one you want. `design_voice` turns one or two sentences (who is speaking, how they sound, where they are heard, and the language they speak) into a voice and plays the line it speaks. `refine_voice` takes one change at a time, such as "a little lower and slower", and keeps the rest of the voice. `save_voice` keeps the draft you like under an id (`voice_...`) that `speak`, `queue_speech`, and `text_to_speech` accept on Paxa TTS Flash v2. Set it as `PAXA_DEFAULT_VOICE` to make it the default. Each design or change costs 5 credits, saving is free, and speech in a designed voice costs 1.25 times the standard rate. Your plan sets how many voices you keep; [Designed voices](https://paxalabs.com/docs/designed-voices) covers slots and limits. ## Voice mode for Claude Code Three pieces let you walk away from Claude Code. It talks when it has news and calls you when it needs you. Install the server as above, tell Claude when to talk, and wire a hook for the moments the model is idle. ### Tell Claude when to talk Add this to `~/.claude/CLAUDE.md`, or to one project's CLAUDE.md: CLAUDE.md: ```md ## Voice I have the Paxa MCP server (tools: speak, queue_speech, control_playback). I am often away from the screen, so use voice like this: - At the end of a turn where you did real work, call speak with a one or two sentence summary before writing the final message: what you did, what is next, and anything you need from me. - When you need a decision from me, speak the question too. - Keep it short and conversational. Never read code, file paths, logs, or long lists aloud. Those stay in text. - Do not speak for quick back-and-forth or trivial answers. - If I ask to hear something long, use queue_speech. - Speak in the language I write in. ``` ### Get told when Claude needs you When Claude Code waits for a permission or an answer, the model is idle and cannot call `speak`. Claude Code fires a hook at those moments, and `paxa say` turns the hook into a spoken phrase such as "Permission needed." Put the command on your PATH with `npm install -g @paxalabs/mcp`. Then add the hook to `~/.claude/settings.json`: settings.json: ```json { "hooks": { "Notification": [ { "matcher": "permission_prompt|idle_prompt|agent_needs_input", "hooks": [ { "type": "command", "command": "paxa say" } ] } ] } } ``` `paxa say` reads the key from `PAXA_API_KEY`, or from the paxa entry in `~/.claude.json` when that is unset. The built-in phrases are synthesized once per voice and kept in the user cache directory. Every later notification plays from disk with no network round trip and no credits. A `Stop` hook configured the same way speaks "Done." at the end of every turn. `paxa say "Build finished"` also works on its own, and `--voice` picks the voice. The [README](https://github.com/paxalabs/mcp#voice-mode-for-claude-code) covers custom phrases, the `--cache` flag, and faster players on macOS. ## Content creation `transcribe_audio` takes a video as well as a recording: ffmpeg extracts the sound track on the machine and the API transcribes it. With `pauses_over` set, the result lists every silence longer than that many seconds with its start and end, the dead air to cut, and `save: ["srt"]` returns the subtitles too. `text_to_speech` with `subtitles: "srt"` or `"vtt"` writes a subtitle file next to the audio, timed from the synthesized words. A cue breaks at sentence punctuation, at a pause, or at the line length and keeps every word whole, and a Thai line ends where the writer put a space. A script past 5,000 characters is split at paragraphs and sentences, synthesized part by part, and joined into one mp3, opus, or wav file, with the subtitles offset to match. Video input and joining parts need ffmpeg (`brew install ffmpeg` on macOS). Every other tool works without it. ## Listening `listen` turns the microphone into an answer. Ask a question with `speak`, call `listen`, and the reply comes back as text as soon as the speaker stops talking, because realtime transcription detects the end of the turn. It needs a command-line recorder, which stock macOS and Windows do not ship. On macOS, install ffmpeg or sox (`brew install ffmpeg` or `brew install sox`). On Windows, install sox, or ffmpeg with `PAXA_MIC` set to the DirectShow device name. On Linux, ffmpeg, sox, `parecord`, or `arecord` works. It also needs Node 22 or newer, and the app that launched the server (Terminal, VS Code, Claude Desktop) needs microphone permission. ## Environment variables | Variable | Required | Default | Purpose | | --- | --- | --- | --- | | PAXA_API_KEY | Yes | | Your API key. The server starts without it, and every tool that calls the API then fails with setup instructions the agent can relay. | | PAXA_OUTPUT_DIR | No | Working directory | Where `text_to_speech` saves files. | | PAXA_TTS_MODEL | No | `paxa-tts-flash-v2` | Speech model for every call that does not pick one. Set `paxa-tts-flash-v1` only for regional Thai accents, the fixed emotion tags of two voices, or a voice Paxa TTS Flash v2 does not carry. | | PAXA_DEFAULT_VOICE | No | `foithong` | Voice used when a tool call does not pick one: a catalog voice, or one of your designed voices (`voice_...`). Every Paxa TTS Flash v2 voice speaks English, Thai, and Mandarin. | | PAXA_DEFAULT_SPEED | No | `1` | Speaking rate used when a call does not pick one, 0.5 to 1.5. Pitch is preserved and the price is the same, and 1.2 to 1.3 suits long reads. | | PAXA_MIC | No | System default | The microphone `listen` records from, named as an AVFoundation device on macOS, a PulseAudio or ALSA device on Linux, or a DirectShow device on Windows. | | PAXA_VOCABULARY | No | | Comma-separated names and terms the transcript should spell as written (product names, people, jargon), pinned on every `transcribe_audio` and `listen` call. | | PAXA_VOCABULARY_FILE | No | | A text file with one term per line (`#` starts a comment), pinned the same way. The server reads it on every call, and edits apply without a restart. | | PAXA_BASE_URL | No | `https://api.paxalabs.com` | API origin override. | To pin a different list per project, set the vocabulary variables in a project-scope server entry (Claude Code's project scope, `.cursor/mcp.json`, `.vscode/mcp.json`). Each call takes its own terms first, then the configured ones, deduplicated and cut at the API's limit of 50, and reports how many it pinned. ## Playback support | Platform | File player | Streaming player | Pause and resume | | --- | --- | --- | --- | | macOS | `afplay` (built in) | `mpg123`, `ffplay`, or `mpv` if installed | Yes | | Linux | `ffplay`, `mpv`, `mpg123`, `paplay`, or `aplay` | `mpg123`, `ffplay`, or `mpv` | Yes | | Windows | `ffplay` if installed, else PowerShell (wav) | `mpg123`, `ffplay`, or `mpv` if installed | No | Streaming needs a player that reads from stdin. On macOS, `brew install mpg123` (the quickest to start) or `brew install ffmpeg` enables it; without one, speech plays through `afplay` after the download completes. If no player is found at all, the speech tools report it and `text_to_speech` still works. - [Load docs into a coding agent](https://paxalabs.com/docs/agents): Load the docs into a coding agent that writes against the API directly. - [Credits, charges, and refunds](https://paxalabs.com/docs/credits): How credits are charged, refunded, and topped up. - [List voices](https://paxalabs.com/docs/api/voices): The full voice roster the `speak` tools draw from. # Load docs into a coding agent The Paxa API publishes machine-readable docs for coding agents. They are two llms.txt indexes, a markdown twin of every docs page, and an installable skill, all generated from the modules that render these docs. These surfaces teach a coding agent to write against the API. To give an agent the API as tools it calls itself, with playback through the speakers, see the [MCP server](https://paxalabs.com/docs/mcp). The Paxa API publishes three machine-readable surfaces for coding agents. They are two llms.txt indexes, a markdown twin of every page of these docs, and an installable skill. Start with the llms.txt index. Fetch a page twin when the task is narrow. Install the skill when the agent has a skills directory. All three are generated from the same content modules and API contract that render these pages. All three cover every product: text to speech, OCR, and translation into Thai. ## llms.txt [https://paxalabs.com/llms.txt](https://paxalabs.com/llms.txt) is a compact index in the llms.txt convention: one line per page with its markdown URL and a one-sentence description, small enough to sit in a system prompt. [https://paxalabs.com/llms-full.txt](https://paxalabs.com/llms-full.txt) is the entire documentation serialized into one markdown document, for retrieval pipelines and long-context models. ## Fetch a page as Markdown Every docs page is also served as plain markdown at the same path with a `.md` suffix: `/docs/text-to-speech.md`, `/docs/api/translate.md`, and so on, with the overview at `/docs.md`. Fetch one page when the task is narrow; each twin carries the full reference tables and examples of its HTML counterpart. ## Install the skill The skill is one `SKILL.md` carrying the whole integration contract: the speech, transcription, OCR, extraction, translation, and feedback endpoints, voices, pricing, retry semantics, and every error code, with runnable examples. The [skills CLI](https://github.com/vercel-labs/skills) discovers it from this domain and installs it for every agent it supports, Claude Code and Cursor among them: Terminal: ```bash npx skills add https://paxalabs.com ``` The CLI verifies the file against the published sha256 digest and installs it into the agent's skills directory (`.claude/skills/` for Claude Code). The file itself is served at [https://paxalabs.com/skills/paxa/SKILL.md](https://paxalabs.com/skills/paxa/SKILL.md), readable in a browser before any install. > If you installed this skill as `paxa-tts`, run `npx skills add https://paxalabs.com` again. The install command itself is unchanged. > Every one of these files is generated from the same content modules and API contract that render these pages. They cannot drift from what the API serves. ## Report feedback from an agent An agent that gets a wrong output or an unexpected error can report it to [POST /v1/feedback](https://paxalabs.com/docs/api/feedback) with the key it already calls the API with. It names the request with its `x-request-id`, sets `kind` to `model` for an output or `api` for an API problem, and adds an example when an output was wrong. The skill carries the same instructions. Reports take no Idempotency-Key. Before sending a report again, an agent checks [GET /v1/feedback](https://paxalabs.com/docs/api/feedback-list) with `request_id`. The same list returns each report's `status` and the team's `resolution` note. ## OpenAPI The same contract is published as an OpenAPI 3.1 document at [https://paxalabs.com/openapi.json](https://paxalabs.com/openapi.json), for API clients, client generators, and API directories. The two WebSocket endpoints are described in these docs alone. # Generate speech from text Text to speech on the Paxa API is one POST /v1/tts request: Thai, English, or Mandarin Chinese text in, MP3, Opus, WAV, or raw PCM audio out, billed per character in credits. Voices, models, options, and limits. [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech) takes text and a voice, and answers with audio (MP3 by default, Opus, WAV, or raw PCM). Thai, English, Mandarin Chinese, and mixed text all work in the same request. Every request costs 10 credits per 1,000 characters, billed per character and rounded up to the next hundredth of a credit (minimum 0.1 credit), charged [before synthesis](https://paxalabs.com/docs/credits). ## Choose a voice The `voice` field takes a voice id from the roster of the model you send, and both rosters below are live from the catalog. The field accepts the id alone. Case is ignored. `"bualoi"` and `"BuaLoi"` select the same voice. Display names are for people. A name can change; an id never does. [GET /v1/voices](https://paxalabs.com/docs/api/voices) serves both rosters as JSON, listing a voice once for each model that serves it. The [text to speech page](https://paxalabs.com/text-to-speech) plays every voice in longer showcase scripts. ### Paxa TTS Flash v2 voices (paxa-tts-flash-v2) Send `"model": "paxa-tts-flash-v2"` for Paxa TTS Flash v2. Each of its 50 voices speaks Thai, English, and Mandarin Chinese, and every one reads [direction tags](https://paxalabs.com/docs/directions) written into `text`. 26 share their ids with Paxa TTS Flash. | Voice | id | Gender | Language | Description | | --- | --- | --- | --- | --- | | Khanom Krok | khanomkrok | Male | Thai, English, and Mandarin Chinese | Calm, unhurried elder voice and the roster's male lead: documentary, heritage storytelling, and narration. | | Nom Yen | nomyen | Female | Thai, English, and Mandarin Chinese | Bright, energetic voice and the roster's female lead: promos, social clips, and everyday product speech. | | Foi Thong | foithong | Female | Thai, English, and Mandarin Chinese | Captivating narrator for long reads and explainers. | | Massaman | massaman | Male | Thai, English, and Mandarin Chinese | Deep, steady leading-man voice for trailers, drama, and announcements. | | Thong Ek | thongek | Male | Thai, English, and Mandarin Chinese | Crisp, authoritative read for news bulletins and corporate updates. | | Panang | panang | Male | Thai, English, and Mandarin Chinese | Polished, theatrical delivery with a stage actor's gravitas. | | Oliang | oliang | Male | Thai, English, and Mandarin Chinese | Relaxed low drawl for chill content and late-night radio. | | Sanae Chan | sanaechan | Female | Thai, English, and Mandarin Chinese | Warm, magnetic voice with a late-night glow. | | Tub Tim Krob | tubtimkrob | Female | Thai, English, and Mandarin Chinese | Low, intimate delivery for late-night reads. | | Moo Ping | mooping | Male | Thai, English, and Mandarin Chinese | Confident, streetwise voice for ads and shorts. | | Bua Loi | bualoi | Female | Thai, English, and Mandarin Chinese | Sweet, youthful voice for characters and light stories. | | Luk Chup | lukchup | Female | Thai, English, and Mandarin Chinese | Light, childlike voice for characters and playful spots. | | Lod Chong | lodchong | Female | Thai, English, and Mandarin Chinese | Slow, close-mic delivery for mindfulness sessions and sleep audio. | | Woon | woon | Female | Thai, English, and Mandarin Chinese | Hushed, breathy read for wind-downs and bedtime. | | Sangkaya | sangkaya | Male | Thai, English, and Mandarin Chinese | Quiet male voice for sleep tracks and slow narration. | | Khao Niao | khaoniao | Male | Thai, English, and Mandarin Chinese | Low, unhurried male voice for guided breathing, wellbeing narration, and slow documentary. | | Tom Yum | tomyum | Female | Thai, English, and Mandarin Chinese | Fast, hot, chatty best-friend voice for drama recaps, entertainment news, and social clips. | | Khanom Chan | khanomchan | Female | Thai, English, and Mandarin Chinese | Sunny, lively chat voice for social clips and companion apps. | | Kaprao | kaprao | Male | Thai, English, and Mandarin Chinese | Relaxed male voice for dialogue, voice assistants, and everyday explainers. | | Cookie | cookie | Female | Thai, English, and Mandarin Chinese | Kind, attentive English voice for support lines and onboarding. | | Latte | latte | Male | Thai, English, and Mandarin Chinese | Polished English voice for brand films. | | Espresso | espresso | Male | Thai, English, and Mandarin Chinese | Deep, dark, unhurried English reader for long fiction, documentaries, and trailers. | | Mocha | mocha | Female | Thai, English, and Mandarin Chinese | Close, gentle, low-volume English voice for bedtime, wellness, and companion speech. | | Tao Huay | taohuay | Female | Thai, English, and Mandarin Chinese | Tender, melancholy Mandarin voice for drama recaps, audio novels, and companion speech. | | Oolong | oolong | Male | Thai, English, and Mandarin Chinese | Low, rich Mandarin narrator for audio novels, brand reads, and late-night stories. | | Saku | saku | Female | Thai, English, and Mandarin Chinese | Crisp, composed voice for AI assistants, smart devices, and in-app guidance. | | Satay | satay | Male | Thai, English, and Mandarin Chinese | Bold, youthful lead for anime dubs, games, and TV series. | | Thong Yod | thongyod | Female | Thai, English, and Mandarin Chinese | Bright, spirited lead for anime dubs, games, and TV series. | | Bai Toey | baitoey | Female | Thai, English, and Mandarin Chinese | Patient, reassuring voice for coaching, wellbeing lessons, and step-by-step explanations. | | Kluay Tod | kluaytod | Male | Thai, English, and Mandarin Chinese | Goofy, good-natured sidekick for cartoons, kids' content, and playful ads. | | Matum | matum | Female | Thai, English, and Mandarin Chinese | Unhurried, precise voice-over for documentaries, travel films, and museum guides. | | Hor Mok | hormok | Male | Thai, English, and Mandarin Chinese | Noble, resonant voice for fantasy heroes, epic audio drama, and games. | | Nam Manao | nammanao | Female | Thai, English, and Mandarin Chinese | Motivating, high-tempo voice for workouts, fitness apps, and training videos. | | Khao Tom | khaotom | Male | Thai, English, and Mandarin Chinese | Good-humored fatherly voice for family brands, how-to clips, and everyday stories. | | Salim | salim | Female | Thai, English, and Mandarin Chinese | Easy, chatty everyday voice for vlogs, reviews, and lifestyle clips. | | Krayasat | krayasat | Male | Thai, English, and Mandarin Chinese | Unhurried, kindly grandfather voice for bedtime stories, folk tales, and life lessons. | | Miang Kham | miangkham | Female | Thai, English, and Mandarin Chinese | Stern, commanding elder voice for grandmother roles, folk tales, and ghost stories. | | Tod Mun | todmun | Female | Thai, English, and Mandarin Chinese | Room-lifting host voice for events, shows, and live announcements. | | Nam Prik | namprik | Female | Thai, English, and Mandarin Chinese | Husky, low alto for brand stories, drama, and late-night talk. | | Khai Jiao | khaijiao | Male | Thai, English, and Mandarin Chinese | High-energy commentator for esports, match highlights, and launch trailers. | | Chu Chi | chuchi | Male | Thai, English, and Mandarin Chinese | Clear, knowledgeable lecturer for classes, e-learning, and explainers. | | Krong Kraeng | krongkraeng | Male | Thai, English, and Mandarin Chinese | Machine-like voice for sci-fi characters, games, and system announcements. | | Chor Muang | chormuang | Female | Thai, English, and Mandarin Chinese | Hesitant, heartfelt voice for love stories, visual novels, and tender scenes. | | Khao Lam | khaolam | Male | Thai, English, and Mandarin Chinese | Low, gentle male voice for love stories, audio drama, and sleep tracks. | | Pla Thu | plathu | Male | Thai, English, and Mandarin Chinese | Laid-back student voice for campus content, study vlogs, and everyday chat. | | Kaeng Phet | kaengphet | Male | Thai, English, and Mandarin Chinese | Big, dramatic voice for movie trailers, game reveals, and launch films. | | Kaeng Pa | kaengpa | Male | Thai, English, and Mandarin Chinese | Rough, menacing villain voice for games, audio drama, and animation. | | Makham | makham | Female | Thai, English, and Mandarin Chinese | Cold, cutting voice for villainesses, sorceresses, and dark fantasy. | | Pui Fai | puifai | Female | Thai, English, and Mandarin Chinese | Sweet, cheerful idol voice for VTubers, virtual hosts, and fan content. | | Tako | tako | Female | Thai, English, and Mandarin Chinese | Easygoing reader for audiobooks, recaps, and explainers. | ### Paxa TTS Flash voices (paxa-tts-flash-v1) Send `"model": "paxa-tts-flash-v1"` for Paxa TTS Flash. Every Paxa TTS Flash voice is designed around one language, Thai, English, or Mandarin Chinese, and reads the other two with a foreign accent. Of its Thai voices, 4 carry a regional accent (Isan, Northern, or Southern). | Voice | id | Gender | Language | Description | | --- | --- | --- | --- | --- | | Khanom Krok | khanomkrok | Male | Thai | Calm, unhurried elder voice and the roster's male lead: documentary, heritage storytelling, and narration. | | Nom Yen | nomyen | Female | Thai | Bright, energetic voice and the roster's female lead: promos, social clips, and everyday product speech. | | Foi Thong | foithong | Female | Thai | Captivating narrator for long reads and explainers. | | Massaman | massaman | Male | Thai | Deep, steady leading-man voice for trailers, drama, and announcements. | | Thong Ek | thongek | Male | Thai | Crisp, authoritative read for news bulletins and corporate updates. | | Panang | panang | Male | Thai | Polished, theatrical delivery with a stage actor's gravitas. | | Oliang | oliang | Male | Thai | Relaxed low drawl for chill content and late-night radio. | | Sanae Chan | sanaechan | Female | Thai | Warm, magnetic voice with a late-night glow. | | Tub Tim Krob | tubtimkrob | Female | Thai | Low, intimate delivery for late-night reads. | | Moo Ping | mooping | Male | Thai | Confident, streetwise voice for ads and shorts. | | Bua Loi | bualoi | Female | Thai | Sweet, youthful voice for characters and light stories. | | Luk Chup | lukchup | Female | Thai | Light, childlike voice for characters and playful spots. | | Lod Chong | lodchong | Female | Thai | Slow, close-mic delivery for mindfulness sessions and sleep audio. | | Woon | woon | Female | Thai | Hushed, breathy read for wind-downs and bedtime. | | Sangkaya | sangkaya | Male | Thai | Quiet male voice for sleep tracks and slow narration. | | Pad Thai | padthai | Female | Thai | Thai-English code-switching MC for events and lifestyle content. | | Khao Niao | khaoniao | Male | Thai | Low, unhurried male voice for guided breathing, wellbeing narration, and slow documentary. | | Tom Yum | tomyum | Female | Thai | Fast, hot, chatty best-friend voice for drama recaps, entertainment news, and social clips. | | Som Tam | somtam | Female | Isan Thai | Isan-accented voice with local warmth. | | Larb | larb | Male | Isan Thai | Isan-accented male voice for regional content. | | Khao Soi | khaosoi | Female | Northern Thai | Northern-accented voice with a soft Lanna cadence. | | Roti | roti | Male | Southern Thai | Southern-accented voice with a quick coastal cadence. | | Donut | donut | Female | English | Cheerful English voice for promos and demos. | | Cookie | cookie | Female | English | Kind, attentive English voice for support lines and onboarding. | | Toast | toast | Male | English | Plain-spoken everyday English voice for tutorials. | | Latte | latte | Male | English | Polished English voice for brand films. | | Espresso | espresso | Male | English | Deep, dark, unhurried English reader for long fiction, documentaries, and trailers. | | Mocha | mocha | Female | English | Close, gentle, low-volume English voice for bedtime, wellness, and companion speech. | | Tao Huay | taohuay | Female | Mandarin Chinese | Tender, melancholy Mandarin voice for drama recaps, audio novels, and companion speech. | | Oolong | oolong | Male | Mandarin Chinese | Low, rich Mandarin narrator for audio novels, brand reads, and late-night stories. | | Tako | tako | Female | Thai | Easygoing reader for audiobooks, recaps, and explainers. | Experimental: **Khanom Chan** (`khanomchan`, Female, Thai). Natural conversational voice that changes mood with emotion tags written into the text: happy, sad, angry, or afraid. Experimental: **Kaprao** (`kaprao`, Male, Thai). Easygoing male voice that changes mood with emotion tags written into the text: happy, sad, angry, or afraid. Special: **Yoyo** (`yoyo`, Male, Thai). One of Paxa Labs' co-founders: a conversational podcast host with an easy live-mic feel. The experimental voices in the Paxa TTS Flash roster read [emotion tags](https://paxalabs.com/docs/emotion-tags), bracketed words such as `[happy]` or `[sad:0.5]` written into `text`, and change their delivery where each tag stands. Every other Paxa TTS Flash voice skips the tags. ## Choose a model The required `model` field names the serving model, `paxa-tts-flash-v2` or `paxa-tts-flash-v1`. [GET /v1/models](https://paxalabs.com/docs/api/models) lists every model with its voice ids, per-request character ceiling, and price, and [GET /health](https://paxalabs.com/docs/api/health) reports which are served right now. curl: ```bash curl https://api.paxalabs.com/v1/models ``` Response: ```json { "models": [ { "id": "paxa-tts-flash-v1", "name": "Paxa TTS Flash", "product": "tts", "max_chars": 5000, "max_request_chars": null, "credits_per_1k_chars": 10, "reference_credits_per_1k_chars": null, "credits_per_page": null, "large_schema_credits_per_page": null, "large_schema_min_leaves": null, "max_leaves": null, "credits_per_hour": null, "max_pages": null, "max_duration_seconds": null, "diarization_limit_seconds": null, "max_session_seconds": null, "encodings": null, "max_bytes": null, "formats": null, "min_request_credits": 0.1, "voices": [ "khanomkrok", "nomyen", "foithong", "massaman", "thongek", "panang", "oliang", "sanaechan", "tubtimkrob", "mooping", "bualoi", "lukchup", "lodchong", "woon", "sangkaya", "padthai", "khaoniao", "tomyum", "khanomchan", "kaprao", "yoyo", "somtam", "larb", "khaosoi", "roti", "donut", "cookie", "toast", "latte", "espresso", "mocha", "taohuay", "oolong", "tako" ], "sources": null, "target": null }, { "id": "paxa-tts-flash-v2", "name": "Paxa TTS Flash v2", "product": "tts", "max_chars": 5000, "max_request_chars": null, "credits_per_1k_chars": 10, "reference_credits_per_1k_chars": null, "credits_per_page": null, "large_schema_credits_per_page": null, "large_schema_min_leaves": null, "max_leaves": null, "credits_per_hour": null, "max_pages": null, "max_duration_seconds": null, "diarization_limit_seconds": null, "max_session_seconds": null, "encodings": null, "max_bytes": null, "formats": null, "min_request_credits": 0.1, "voices": [ "khanomkrok", "nomyen", "foithong", "massaman", "thongek", "panang", "oliang", "sanaechan", "tubtimkrob", "mooping", "bualoi", "lukchup", "lodchong", "woon", "sangkaya", "khaoniao", "tomyum", "khanomchan", "kaprao", "cookie", "latte", "espresso", "mocha", "taohuay", "oolong", "saku", "satay", "thongyod", "baitoey", "kluaytod", "matum", "hormok", "nammanao", "khaotom", "salim", "krayasat", "miangkham", "todmun", "namprik", "khaijiao", "chuchi", "krongkraeng", "chormuang", "khaolam", "plathu", "kaengphet", "kaengpa", "makham", "puifai", "tako" ], "sources": null, "target": null }, { "id": "paxa-stt-lite-v1-preview", "name": "Paxa STT Lite", "product": "stt", "max_chars": null, "max_request_chars": null, "credits_per_1k_chars": null, "reference_credits_per_1k_chars": null, "credits_per_page": null, "large_schema_credits_per_page": null, "large_schema_min_leaves": null, "max_leaves": null, "credits_per_hour": 500, "max_pages": null, "max_duration_seconds": 3600, "diarization_limit_seconds": 540, "max_session_seconds": null, "encodings": null, "max_bytes": 26214400, "formats": [ "mp3", "wav", "flac", "ogg", "m4a", "aac", "webm" ], "min_request_credits": 0.1, "voices": [], "sources": null, "target": null }, { "id": "paxa-stt-lite-realtime-v1-preview", "name": "Paxa STT Lite Realtime", "product": "stt", "max_chars": null, "max_request_chars": null, "credits_per_1k_chars": null, "reference_credits_per_1k_chars": null, "credits_per_page": null, "large_schema_credits_per_page": null, "large_schema_min_leaves": null, "max_leaves": null, "credits_per_hour": 750, "max_pages": null, "max_duration_seconds": null, "diarization_limit_seconds": null, "max_session_seconds": 14400, "encodings": [ { "encoding": "pcm_s16le", "sample_rates": [ 16000, 48000 ] }, { "encoding": "mulaw", "sample_rates": [ 8000 ] } ], "max_bytes": null, "formats": null, "min_request_credits": 0.1, "voices": [], "sources": null, "target": null }, { "id": "paxa-ocr-lite-v1", "name": "Paxa OCR Lite", "product": "ocr", "max_chars": null, "max_request_chars": null, "credits_per_1k_chars": null, "reference_credits_per_1k_chars": null, "credits_per_page": 6.5, "large_schema_credits_per_page": null, "large_schema_min_leaves": null, "max_leaves": null, "credits_per_hour": null, "max_pages": 50, "max_duration_seconds": null, "diarization_limit_seconds": null, "max_session_seconds": null, "encodings": null, "max_bytes": 10485760, "formats": [ "pdf", "png", "jpeg", "webp" ], "min_request_credits": 6.5, "voices": [], "sources": null, "target": null }, { "id": "paxa-doc-extract-v1", "name": "Paxa Document Extraction", "product": "extraction", "max_chars": null, "max_request_chars": null, "credits_per_1k_chars": null, "reference_credits_per_1k_chars": null, "credits_per_page": 13, "large_schema_credits_per_page": 19.5, "large_schema_min_leaves": 51, "max_leaves": 100, "credits_per_hour": null, "max_pages": 20, "max_duration_seconds": null, "diarization_limit_seconds": null, "max_session_seconds": null, "encodings": null, "max_bytes": 10485760, "formats": [ "pdf", "png", "jpeg", "webp" ], "min_request_credits": 13, "voices": [], "sources": null, "target": null }, { "id": "paxa-translation-lite-v1", "name": "Paxa Translate Lite", "product": "translation", "max_chars": 20000, "max_request_chars": 200000, "credits_per_1k_chars": 25, "reference_credits_per_1k_chars": 8, "credits_per_page": null, "large_schema_credits_per_page": null, "large_schema_min_leaves": null, "max_leaves": null, "credits_per_hour": null, "max_pages": null, "max_duration_seconds": null, "diarization_limit_seconds": null, "max_session_seconds": null, "encodings": null, "max_bytes": null, "formats": null, "min_request_credits": 2, "voices": [], "sources": [ "en", "zh", "ja", "ko", "ar", "fr", "de", "hi", "id", "ms", "pt", "ru", "es", "vi" ], "target": "th" } ], "object": "list", "data": [ { "id": "paxa-tts-flash-v1", "object": "model", "created": 1785715200, "owned_by": "paxalabs" }, { "id": "paxa-tts-flash-v2", "object": "model", "created": 1792108800, "owned_by": "paxalabs" }, { "id": "paxa-stt-lite-v1-preview", "object": "model", "created": 1788912000, "owned_by": "paxalabs" }, { "id": "paxa-stt-lite-realtime-v1-preview", "object": "model", "created": 1790121600, "owned_by": "paxalabs" }, { "id": "paxa-ocr-lite-v1", "object": "model", "created": 1787529600, "owned_by": "paxalabs" }, { "id": "paxa-doc-extract-v1", "object": "model", "created": 1789171200, "owned_by": "paxalabs" }, { "id": "paxa-translation-lite-v1", "object": "model", "created": 1786320000, "owned_by": "paxalabs" } ] } ``` ## Request options A request carries `text`, `voice`, and `model`. The other fields are optional: - `format` chooses the container. The default `mp3` is compressed. `opus` is Ogg Opus, 48 kbps on Paxa TTS Flash and 64 on Paxa TTS Flash v2, the smallest, for streaming and web playback. `wav` carries uncompressed 16-bit PCM, the largest, for audio that feeds further processing. `pcm` carries those samples with no header, 16-bit little-endian mono, for voice agents that take raw samples. Both run at 24 kHz unless `sample_rate` names another rate, and Paxa TTS Flash v2 serves 8000, 16000, 22050, 24000, 32000, 44100, 48000 Hz. `mulaw` and `alaw` carry G.711 mu-law and A-law at 8 kHz with no header, for phone lines, on Paxa TTS Flash v2. Every format and rate costs the same. - `stream` switches delivery from a complete body to chunks. - [`timestamps`](https://paxalabs.com/docs/timestamps) requests word, sentence, or utterance timing. It changes both the response shape and the rate. - `speed` sets the speaking rate, from 0.5 to 1.5 times the voice's design speed. The charge is the same at every rate. - [`temperature`](https://paxalabs.com/docs/temperature) sets how much the reading varies from one request to the next, from 0 to 2, on Paxa TTS Flash v2. The charge is the same at every value. - `language` sets how codes, digits, and symbols are read. Words keep the language of their script, and a number inside Thai or Chinese text keeps that text's language. Elsewhere, `th` reads them in Thai and `en` in English. Under `th`, `AB2039` gets Thai letter names and digits. `zh` reads codes in English, a number next to Chinese characters in Mandarin, and other digits and symbols in Thai. The default, `auto`, follows the language most of the text is written in, counting English only in words of three or more letters, and takes Thai on a tie. Under `auto`, a bare number, `AB2039`, or `250 ml` reads as Thai, and `ISO9001` or `100 USD` as English. Text with no Thai, Chinese, digits, or counted English word reads as English when a two-letter word has a lowercase letter, such as `Hi` or `I'm OK`. Pin `th` or `en` to choose how a short number or code reads. The charge is the same in every language. The [API reference](https://paxalabs.com/docs/api/text-to-speech) documents every field. The example below requests Paxa TTS Flash v2. For Paxa TTS Flash, send `"model": "paxa-tts-flash-v1"` with one of its voice ids. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "format": "mp3", "stream": false }' \ --output speech.mp3 ``` Buffered delivery (the default) answers with a complete body and a `content-length`. Set `stream: true` to receive audio while it is synthesized; [Streaming](https://paxalabs.com/docs/streaming) covers the trade-offs. ## Limits A request accepts at most 5,000 characters on Paxa TTS Flash v2 and 5,000 on Paxa TTS Flash. Longer text answers [400 text_too_long](https://paxalabs.com/docs/errors#text_too_long) and nothing is charged. Split long documents into paragraph-sized requests. Requests per minute depend on the plan. See [Rate limits](https://paxalabs.com/docs/rate-limits). # Stream audio as it is generated Streaming delivers audio in chunks while synthesis is still running, cutting time to first sound. Set stream: true, or use the OpenAI-compatible endpoint where streaming is the default. Streaming delivers audio in chunks while synthesis is still running. Set `stream: true` on [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech) and the first bytes arrive while the rest is being synthesized, which cuts time to first sound for real-time uses like voice agents and live narration. On the [OpenAI-compatible endpoint](https://paxalabs.com/docs/api/speech), streaming is the default. Streaming here is one-way delivery of one request; for a two-way connection that accepts text incrementally, see [Live streaming](https://paxalabs.com/docs/live-streaming). curl: ```bash curl -N -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "stream": true}' \ --output speech.mp3 ``` TypeScript: ```typescript 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: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", voice: "khanomkrok", model: "paxa-tts-flash-v2", stream: true, }), }); if (!response.ok || !response.body) { throw new Error(`TTS failed: ${response.status}`); } // Chunks arrive while synthesis is still running. for await (const chunk of response.body) { player.feed(chunk); } ``` 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": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "stream": True}, stream=True, ) response.raise_for_status() with open("speech.mp3", "wb") as f: for chunk in response.iter_content(chunk_size=None): f.write(chunk) ``` ## Buffered and streamed delivery | | Buffered (`stream: false`) | Streamed (`stream: true`) | | --- | --- | --- | | Delivery | Complete body with a `content-length` | Chunked as synthesis proceeds | | First byte | After synthesis finishes | As soon as the first chunk exists | | Failure after start | Clean 502 with a problem body | The stream truncates; no trailing error body | ## Consume the stream Feed chunks to the player or socket as they arrive. Buffer the whole body only when writing a file. The charge happens before synthesis and `x-credits-charged` is a response header. The exact cost is known from the first byte. With [`timestamps`](https://paxalabs.com/docs/timestamps) set, a streamed response switches from raw MP3 chunks to NDJSON events that carry audio and timing together; the event shapes are in the [reference](https://paxalabs.com/docs/api/text-to-speech). ## Handle failures The response commits with the first chunk. Any failure before the first byte answers a clean [502 provider_error](https://paxalabs.com/docs/errors#provider_error) and the charge is refunded. If synthesis fails mid-stream, the connection ends early: treat an unexpectedly closed stream as a failed request. The refund still runs automatically. Cancelling the request from your side keeps the charge: the audio was being delivered as paid for. Prefer buffered delivery when you need all-or-nothing semantics, for example when writing files for later use. > Pair streaming with an [Idempotency-Key](https://paxalabs.com/docs/idempotency). If the connection drops, a retry under the same key serves the request again without a second charge. If synthesis itself failed and was refunded, the retry answers [409 idempotency_refunded](https://paxalabs.com/docs/errors#idempotency_refunded) and needs a fresh key. # Get word-level timestamps Timestamps are word, sentence, or utterance timing spans returned together with the audio, for captions and read-along UIs. One request option changes the response shape and the rate. Timestamps are timing spans, word, sentence, or utterance, returned together with the audio. Set `timestamps` on [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech) to receive them. The response then carries the audio together with timing spans, each holding the span's text, its start, and its end in seconds. Timed requests bill at 1.25 times the model's per-character rate, rounded up to the next hundredth of a credit, minimum 0.1 credit per request. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "timestamps": "word" }' \ -o response.json jq -r .audio response.json | base64 -d > speech.mp3 jq .timing.spans response.json ``` 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: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", voice: "khanomkrok", model: "paxa-tts-flash-v2", timestamps: "word", }), }); if (!response.ok) throw new Error(`TTS failed: ${response.status}`); const { audio, timing } = await response.json(); await writeFile("speech.mp3", Buffer.from(audio, "base64")); for (const span of timing.spans) { console.log(`${span.start.toFixed(2)}s ${span.text}`); } ``` Python: ```python import base64 import os import requests response = requests.post( "https://api.paxalabs.com/v1/tts", headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, json={ "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "timestamps": "word", }, ) response.raise_for_status() data = response.json() with open("speech.mp3", "wb") as f: f.write(base64.b64decode(data["audio"])) for span in data["timing"]["spans"]: print(f"{span['start']:.2f}s {span['text']}") ``` ## Choose a granularity The `timestamps` value picks how the audio is segmented; every span carries `text`, `start`, and `end` in seconds. Spans cover the spoken text, and an [emotion tag](https://paxalabs.com/docs/emotion-tags) takes no span. | Value | Spans | | --- | --- | | word | One span per spoken word. The model segments Thai itself, and the text needs no spaces. | | sentence | One span per sentence. | | utterance | One span per continuous run of speech between pauses. An utterance can cover several sentences read in one breath, or part of a long one. | ## Response format With buffered delivery the response is `application/json`, and the body is no longer binary audio. The `audio` field carries the complete file as base64. The `timing.spans` field lists every span at the requested granularity. Decoding `audio` yields exactly the bytes an untimed request returns, and the usual headers, including `x-credits-charged`, are unchanged. Response: ```json { "audio": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYy...", "timing": { "granularity": "word", "spans": [ { "text": "สวัสดี", "start": 0, "end": 0.51 }, { "text": "ครับ", "start": 0.51, "end": 0.85 }, { "text": "ยินดี", "start": 0.85, "end": 1.27 }, { "text": "ต้อนรับ", "start": 1.27, "end": 1.87 }, { "text": "สู่", "start": 1.87, "end": 2.12 }, { "text": "Paxa", "start": 2.12, "end": 2.46 }, { "text": "Labs", "start": 2.46, "end": 2.8 } ] } } ``` ## Stream timed audio With `stream: true` the response is `application/x-ndjson`. Each line is one JSON event carrying a base64 audio chunk and the spans that chunk completed. A final `done` event carries the total span count. Compare it with the spans you collected. The spans themselves travel exactly once, on the chunk lines. If the stream ends without a `done` event, treat the request as failed. The charge is refunded automatically, under the same rule as [audio streaming](https://paxalabs.com/docs/streaming#failure-semantics). NDJSON stream: ```json {"type":"chunk","audio":"SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYy...","spans":[{"text":"สวัสดี","start":0,"end":0.51},{"text":"ครับ","start":0.51,"end":0.85},{"text":"ยินดี","start":0.85,"end":1.27}]} {"type":"chunk","audio":"SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYy...","spans":[{"text":"ต้อนรับ","start":1.27,"end":1.87},{"text":"สู่","start":1.87,"end":2.12},{"text":"Paxa","start":2.12,"end":2.46},{"text":"Labs","start":2.46,"end":2.8}]} {"type":"done","granularity":"word","span_count":7} ``` ## Pricing The surcharge applies only when `timestamps` is set. The same request then bills 1.25 times its untimed cost. The 36-character example above costs 0.45 credits timed and 0.36 credits untimed. [GET /v1/models](https://paxalabs.com/docs/api/models) lists each model's per-character rate. Rounding and the 0.1 credit minimum work as described in [Credits and billing](https://paxalabs.com/docs/credits). Try it on your own text in the [playground](https://paxalabs.com/app/speech). The model behind timed speech has its own page, [text to speech](https://paxalabs.com/text-to-speech). > The [OpenAI-compatible endpoint](https://paxalabs.com/docs/api/speech) ignores `timestamps`, since the OpenAI request shape has no equivalent field. Timing is also available on every [live connection](https://paxalabs.com/docs/live-streaming), priced the same way. # Direct the delivery with tags Direction tags are bracketed directions such as [whispering], sounds such as [laughs], hesitations, and and marks, written into the text of a Paxa TTS Flash v2 request. How each is read, weights, blends, billing, and every tag the API refuses. On Paxa TTS Flash v2, every voice reads tags written into the `text` of [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech). A direction in square brackets, such as `[whispering]`, sets how the text after it is delivered. A sound such as `[laughs]` plays where it is written, a hesitation such as `[um]` is spoken as a word, and `` and `` mark a span. The API reads every tag before any charge, and a tag the model refuses answers [400 tag_invalid](https://paxalabs.com/docs/errors#tag_invalid). On Paxa TTS Flash (`paxa-tts-flash-v1`), only its experimental voices read [emotion tags](https://paxalabs.com/docs/emotion-tags), and every other voice skips them. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "[warm, unhurried] ยินดีต้อนรับกลับมาครับ [chuckles] วันนี้มีข่าวดี [เอ่อ] แต่ขอเก็บเป็นความลับไว้ก่อนนะครับ", "voice": "khanomkrok", "model": "paxa-tts-flash-v2" }' \ --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: "[warm, unhurried] ยินดีต้อนรับกลับมาครับ [chuckles] วันนี้มีข่าวดี [เอ่อ] แต่ขอเก็บเป็นความลับไว้ก่อนนะครับ", voice: "khanomkrok", model: "paxa-tts-flash-v2", }), }); 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": "[warm, unhurried] ยินดีต้อนรับกลับมาครับ [chuckles] วันนี้มีข่าวดี [เอ่อ] แต่ขอเก็บเป็นความลับไว้ก่อนนะครับ", "voice": "khanomkrok", "model": "paxa-tts-flash-v2"}, ) response.raise_for_status() with open("speech.mp3", "wb") as f: f.write(response.content) ``` ## Directions Directions are natural language. Write how the words after the tag should sound, in English, Thai, or Chinese, and the voice delivers them that way until the next direction. Any words in square brackets that are not a sound, a hesitation, a pause, or a refused setting read as a direction. Some to start from: - `[whispering]`, `[calm, measured]`, `[building excitement]`, `[barely awake, at three in the morning]` - `[อบอุ่น พูดช้า ๆ]`, `[เขินอาย พูดเบา ๆ]`, `[ค่อย ๆ ตื่นเต้นขึ้น]` - `[害羞地,小声]`, `[冷冷地,带着笑意]`, `[像是等这一刻等了十年]` Text before the first direction keeps the voice's own delivery, and `[neutral]` returns to it. A sound or a hesitation leaves the direction in force. - Nothing in square brackets is spoken except a hesitation. A tag runs from `[` to the next `]` with no other `[` and no line break between them, and full-width brackets `[ ]` work the same way. - Tags are read without regard to case, full-width letters and digits read as their ASCII forms, and runs of spaces read as one: `[Sad]`, `[SAD]`, and `[ sad ]` are all `[sad]`. - A bracket without letters, such as `[1]` or `[...]`, answers `tag_invalid`. ## Weights and blends A weight follows a colon, as in `[sad:0.5]` or `[laughs:0.3]`. Write it as a number such as `0.5`, `.5`, or `0,5`, or as a percentage such as `50%`. Thai and full-width digits read too. A weight below 0 reads as 0. There is no upper limit: `[excited:2]` is valid. On `[neutral]`, on an emotion word from the [emotion tag list](https://paxalabs.com/docs/emotion-tags#tags) such as `[sad]`, and on a sound, a weight that is not a number, such as `[sad:half]`, answers `tag_invalid`. On any other direction the colon is part of its words: `[note: see below]` is one direction. `+` joins directions into a blend, each part with its own weight: `[happy+sad]`, `[happy:0.7+sad:0.3]`. A blend takes directions only: `[happy+laughs]` and `[sad+um]` answer `tag_invalid`. ## Sounds A sound plays where it is written. Write any sound below by its name or one of its spellings, with a weight if you like, as in `[soft laugh:0.8]`. A sound at weight 0 is left out. | Sound | Also written as | | --- | --- | | [sighs] | sigh, a sigh, deep sigh, heavy sigh, long sigh, sighs deeply, sighs heavily, soft sigh, sighs softly, ถอนหายใจ, ถอนใจ, ถอนหายใจยาว, 叹气, 叹息, 唉 | | [laughs] | laugh, laughter, a laugh, short laugh, big laugh, soft laugh, laughs softly, quiet laugh, เสียงหัวเราะ, 笑声, 大笑 | | [chuckles] | chuckle, a chuckle, soft chuckle, small chuckle, light chuckle, chuckles softly, หัวเราะหึๆ, หัวเราะหึ ๆ, หัวเราะในลำคอ, 轻笑, 窃笑 | | [exhales sharply] | exhale sharply, sharp exhale, huff, huffs, พ่นลมหายใจแรงๆ, พ่นลมหายใจแรง ๆ, 猛地呼气 | | [gasps] | gasp, a gasp, sharp gasp, soft gasp, หายใจเฮือก, 倒吸一口气, 倒抽一口冷气 | | [inhales deeply] | inhale deeply, deep inhale, deep breath, a deep breath, takes a deep breath, breathes in deeply, หายใจเข้าลึกๆ, หายใจเข้าลึก ๆ, สูดหายใจลึกๆ, สูดหายใจลึก ๆ, 深吸一口气, 深呼吸 | | [giggles] | giggle, a giggle, soft giggle, หัวเราะคิกคัก, คิกคัก, 咯咯笑, 嘻嘻 | | [sobs] | sob, a sob, soft sob, sobs quietly, เสียงสะอื้น, 抽泣, 啜泣, 呜咽 | | [yawns] | yawn, a yawn, หาว, 打哈欠, 哈欠 | | [clears throat] | clear throat, clears his throat, clears her throat, clears their throat, throat clear, ahem, กระแอม, กระแอมไอ, 清嗓子, 清了清嗓子 | | [inhales] | inhale, breathes in, quick inhale, หายใจเข้า, 吸气 | | [breath] | a breath, takes a breath, quick breath, หายใจ, 呼吸 | | [exhales] | exhale, breathes out, หายใจออก, 呼气 | | [whimpers] | whimper, a whimper, soft whimper, เสียงหงิงๆ, เสียงหงิง ๆ, เสียงครางหงิงๆ, เสียงครางหงิง ๆ, 嘤嘤, 嘤咛, 呜呜 | | [scoffs] | scoff, a scoff, pfft, pff, psh, pshh, soft scoff, แค่นเสียง, หึ, เสียงหึ, 嗤, 嗤了一声, 冷哼, 冷哼一声, 哼 | These sounds answer `tag_invalid` in any spelling. | Sound | Also written as | | --- | --- | | [sniffs] | sniff, sniffle, sniffles, สูดจมูก, 吸鼻子, 抽鼻子 | | [gulps] | gulp, a gulp, กลืนน้ำลาย, 咽口水, 吞口水 | | [coughs] | cough, a cough, dry cough, coughing fit, soft cough, small cough, coughs softly, ไอ, เสียงไอ, ไอแค่กๆ, ไอแค่ก ๆ, ไอค่อกแค่ก, 咳嗽, 咳, 咳了一声, 干咳 | | [sneezes] | sneeze, a sneeze, achoo, ah-choo, atchoo, small sneeze, soft sneeze, จาม, เสียงจาม, ฮัดชิ่ว, ฮัดเช้ย, 打喷嚏, 喷嚏, 阿嚏 | | [groans] | groan, a groan, soft groan, groans softly, เสียงคราง, เสียงครางเบาๆ, เสียงครางเบา ๆ, 呻吟声, 一声呻吟 | | [grunts] | grunt, a grunt, soft grunt, ฮึ่ม, เสียงฮึ่ม, อึ๊บ, เสียงอึ๊บ, 闷哼, 闷哼一声, 低哼 | | [snorts] | snort, a snort, soft snort, พ่นลมจมูก, พ่นลมทางจมูก, เสียงพ่นลมจมูก, 喷鼻息, 鼻哼, 鼻子哼了一声 | | [pants] | pant, pants heavily, pants softly, เสียงหอบ, เสียงหอบหายใจ, 喘气声, 喘息声, 大口喘气 | | [shushes] | shush, a shush, shh, shhh, จุ๊ๆ, จุ๊ ๆ, ชู่ว, ชู่ว์, เสียงชู่ว, 嘘, 嘘了一声 | | [tsks] | tsk, tsk tsk, tut, tuts, tut tut, tut-tut, tongue click, clicks tongue, clicks her tongue, clicks his tongue, clicks their tongue, จิ๊, จิ๊ปาก, จุ๊ปาก, เดาะลิ้น, 啧, 啧啧, 咂嘴 | | [hums] | hum, a hum, soft hum, hums softly, ฮัม, เสียงฮัม, ฮัมเพลง, 哼歌, 哼小曲, 哼曲子 | | [screams] | scream, a scream, shriek, shrieks, a shriek, small shriek, กรี๊ด, เสียงกรี๊ด, เสียงกรีดร้อง, 尖叫声, 一声尖叫, 惊叫声 | ## Hesitations `[um]` and `[hmm]` are hesitations. Each is spoken as the word written inside the brackets. A text with a hesitation always has something to say. A hesitation takes no weight: `[um:0.5]` answers `tag_invalid`. | Hesitation | Also written as | | --- | --- | | [hmm] | hm, hmmm, mmm, อืม, 嗯 | | [um] | umm, uh, uhh, er, เอ่อ, เอ่อๆ, เอ่อ ๆ, อ่า, 呃, 呃呃, 额 | The spellings `[uhm]`, `[erm]`, `[อืมม]` answer `tag_invalid`. ## Pauses `[short pause]`, `[pause]`, `[long pause]`, and an exact length such as `[pause 300ms]` add silence where they are written. [Pauses](https://paxalabs.com/docs/pauses) covers their lengths and the 500 ms limit. ## Emphasis and whisper `` and `` mark a span of text. `...` stresses the words inside, and `...` whispers them. - A weight goes after a colon, as in a bracket: ``, ``. A mark whose weight is not a number, such as ``, is ignored along with its close. - The close restores the direction in force before the mark, or the voice's own delivery when there was none. A direction written inside the span replaces what the close restores. - A close ends the innermost open mark of its kind. A close with no open mark is ignored, and a mark left open holds like a direction. - Case and spaces inside a mark do not matter: `< EMPH >` and `` both read. - Emphasis can also be written as a direction, `[เน้นเสียง]` or `[เน้น]`, `[เน้นคำ]`, `[เน้นคำนี้]`, `[เน้นย้ำ]`, `[ลงน้ำหนัก]`, `[emphasize]`, `[emphasise]`, `[emphasized]`, `[emphasised]`, `[emphasis]`, `[with emphasis]`, `[强调]`, `[強調]`, `[重读]`, `[重讀]`, `[加重语气]`, `[加重語氣]`. ## Tags the API refuses Each of these answers [400 tag_invalid](https://paxalabs.com/docs/errors#tag_invalid) before any charge, even in a text that holds nothing else. | Tag | Examples | | --- | --- | | A bracket without letters | `[1]`, `[...]` | | A sound outside the list of sounds | `[sniffs]`, `[gulps]`, `[coughs]`, `[sneezes]`, `[groans]`, `[grunts]`, `[snorts]`, `[pants]`, `[shushes]`, `[tsks]`, `[hums]`, `[screams]` | | A hesitation spelling the model does not read | `[uhm]`, `[erm]`, `[อืมม]` | | A weight on a hesitation | `[um:0.5]` | | A weight that is not a number on `[neutral]`, an emotion word, or a sound | `[sad:half]`, `[laughs:lots]` | | A blend part that is not a direction | `[happy+laughs]`, `[sad+um]` | | A request setting written as a tag | `[voice]`, `[language]` | | An accent | `[british accent]`, `[สำเนียงอีสาน]`, `[北京口音]` | | A different voice | `[robotic]`, `[old man voice]`, `[เสียงเด็ก]`, `[机器人]` | | Singing | `[singing]`, `[ร้องเพลง]`, `[唱歌]`; `[sing-song]` is a direction | | A sound effect | `[door slams]`, `[applause]`, `[sound effect]`, `[เสียงประกอบ]` | | A language | `[in thai]`, `[english]`, `[ภาษาไทย]`, `[中文]` | ## Billing Tags are text. Every character of a tag, brackets and marks included, counts toward the request's length and its charge at the model's per-character rate. The 126-character example above costs 1.26 credits. A text of tags alone, hesitations aside, answers [422 unspeakable_text](https://paxalabs.com/docs/errors#unspeakable_text) before anything is charged. [Credits and billing](https://paxalabs.com/docs/credits) covers rounding and the minimum. ## Tags on a live connection On a [live connection](https://paxalabs.com/docs/live-streaming) a `text` frame whose tag the model refuses answers an `error` event with `tag_invalid`, and the buffer keeps the text it held before the frame. Auto-flush holds back a tag or mark still open at the end of the buffer, and one 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 directions and marks 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. > The [OpenAI-compatible endpoint](https://paxalabs.com/docs/api/speech) reads tags in `input` under the same rules. # Add pauses Pause tags add silence where they are written in a Paxa TTS Flash v2 request: three named lengths per language, or an exact length up to 500 ms. Where a pause falls, speed, billing, and live connections. On Paxa TTS Flash v2, a pause tag in the `text` of [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech) adds silence after the word before it. Write a named length, `[short pause]`, `[pause]`, or `[long pause]`, or an exact one such as `[pause 300ms]`. Every voice reads them, and the API checks each one before any charge. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "ยืนยันคำสั่งซื้อเรียบร้อยแล้วครับ [pause 300ms] พัสดุจะถึงภายในวันศุกร์ [long pause] มีคำถามโทรหาเราได้ตลอดครับ", "voice": "khanomkrok", "model": "paxa-tts-flash-v2" }' \ --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: "ยืนยันคำสั่งซื้อเรียบร้อยแล้วครับ [pause 300ms] พัสดุจะถึงภายในวันศุกร์ [long pause] มีคำถามโทรหาเราได้ตลอดครับ", voice: "khanomkrok", model: "paxa-tts-flash-v2", }), }); 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": "ยืนยันคำสั่งซื้อเรียบร้อยแล้วครับ [pause 300ms] พัสดุจะถึงภายในวันศุกร์ [long pause] มีคำถามโทรหาเราได้ตลอดครับ", "voice": "khanomkrok", "model": "paxa-tts-flash-v2"}, ) response.raise_for_status() with open("speech.mp3", "wb") as f: f.write(response.content) ``` ## Named lengths A named pause takes its length from the language of the word before the tag. | Language | `[short pause]` | `[pause]` | `[long pause]` | | --- | --- | --- | --- | | Thai | 130 ms | 180 ms | 460 ms | | Chinese | 100 ms | 140 ms | 360 ms | | English and every other language | 120 ms | 170 ms | 460 ms | ## Exact lengths - `[pause 300ms]`, `[pause 0.3s]`, and `[pause:300]` are the same pause. A number with no unit is milliseconds, and `s`, `sec`, `second`, `seconds` read it as seconds. - Thai and full-width digits read too: `[pause ๓๐๐ms]` is `[pause 300ms]`. - A length rounds to the nearest 10 ms. - The longest exact pause is 500 ms. A longer one, such as `[pause 1s]` or `[pause:600]`, plays at 500 ms. ## Where a pause falls A pause adds its length to the gap the reading already leaves after the word before the tag. That gap is longer at the end of a sentence than between two words. A `[pause]` after a full stop makes a longer silence than one inside a sentence. ## Pauses and speed A pause tag keeps its length at every `speed`. Speed changes the gaps the reading leaves on its own, and the tag adds the same length to them. ## Billing Pause tags are text. Every character of a tag, brackets included, counts toward the request's length and its charge. The 111-character example above costs 1.11 credits. A text of pause tags alone answers [422 unspeakable_text](https://paxalabs.com/docs/errors#unspeakable_text) before anything is charged. ## Pauses on a live connection On a [live connection](https://paxalabs.com/docs/live-streaming) a pause tag reads as it does in a single request. A tag split across frames is read whole. ## Paxa TTS Flash Paxa TTS Flash (`paxa-tts-flash-v1`) reads no pause tags. Its experimental voices answer `tag_invalid`, since `[pause]` is not on the [emotion tag list](https://paxalabs.com/docs/emotion-tags#tags), and on every other v1 voice the tag is text and is spoken. > The [OpenAI-compatible endpoint](https://paxalabs.com/docs/api/speech) reads pause tags in `input` under the same rules. # Control variation with temperature temperature sets how much a Paxa TTS Flash v2 reading varies from one request to the next, from 0 to 2. What it changes, designed voices, where it is read, and billing. `temperature` is a field of [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech) that sets how much a reading varies from one request to the next: its pacing, pauses, breaths, and melody. It runs from 0 to 2 and defaults to 0.5. Paxa TTS Flash v2 reads it. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "temperature": 0 }' \ --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: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", voice: "khanomkrok", model: "paxa-tts-flash-v2", temperature: 0, }), }); 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": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "temperature": 0}, ) response.raise_for_status() with open("speech.mp3", "wb") as f: f.write(response.content) ``` ## Values - `0` varies least. The same text in the same voice comes back closest in timing and melody from one request to the next. Two requests are not guaranteed to match. - `0.5` is the default. An omitted field and `0.5` are one request. - `2` varies the reading most. - Values round to the nearest 0.1. A value outside 0 to 2 answers [400 validation](https://paxalabs.com/docs/errors#validation). ## Designed voices On a [designed voice](https://paxalabs.com/docs/designed-voices), as on any other, lower values hold the voice steadier and higher values vary it more. ## Models Paxa TTS Flash v2 (`paxa-tts-flash-v2`) reads `temperature`. On Paxa TTS Flash (`paxa-tts-flash-v1`), a value that rounds to `0.5` is accepted, and any other answers [400 validation](https://paxalabs.com/docs/errors#validation) with `path` `body.temperature` and `reason` `not_allowed`, before any charge. ## Where it is read - [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech). - [POST /v1/audio/speech](https://paxalabs.com/docs/api/speech), as a Paxa extension to OpenAI's body. The OpenAI-compatible transcription endpoint ignores its own `temperature`. - The `start` frame of [WS /v1/tts/live](https://paxalabs.com/docs/live-streaming), fixed for the connection. The `started` event echoes it when it is not `0.5`. A start frame whose model refuses it answers an `error` event with `validation`, and the connection stays open for a corrected start frame. ## Billing and idempotency A request costs the same at every temperature. Under an `Idempotency-Key`, `temperature` is part of the request: a retry with a different value answers [422 idempotency_mismatch](https://paxalabs.com/docs/errors#idempotency_mismatch), and an omitted field matches `0.5`. # 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. # Designed voices A designed voice is a voice you describe in words and save to your account, through the API or the console. It speaks on Paxa TTS Flash v2 under its own id, at its own rate, and only for your account. A designed voice is a voice you describe in words and save to your account, through the API or the console's [Voice design](https://paxalabs.com/app/voice-design) tool. A saved voice has an id that starts with `voice_`. Send that id as `voice` with `model` set to `paxa-tts-flash-v2` on [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech), the [OpenAI-compatible endpoint](https://paxalabs.com/docs/api/speech), or a [live connection](https://paxalabs.com/docs/live-streaming). ## Design a voice 1. Describe it: Send who the voice is and how it should sound as `description` to [POST /v1/voices/design](https://paxalabs.com/docs/api/voice-design). The answer is one voice with a name, a role, a short description, a line spoken in it as MP3, and a `draft`. 2. Refine it: Send the `draft` back with an `instruction` that asks for a change in words. Each answer carries the changed voice and a new draft. 3. Save it: Send the draft you want to [POST /v1/voices/designed](https://paxalabs.com/docs/api/save-voice), with a `name` if you want a different one. Saving gives the voice its id. curl: ```bash curl -X POST https://api.paxalabs.com/v1/voices/design \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai" }' \ -o design.json jq -r .audio design.json | base64 --decode > line.mp3 ``` TypeScript: ```typescript import { writeFile } from "node:fs/promises"; const headers = { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }; // Design, then refine the draft with an instruction. let response = await fetch("https://api.paxalabs.com/v1/voices/design", { method: "POST", headers, body: JSON.stringify({ description: "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai", }), }); if (!response.ok) throw new Error(`Design failed: ${response.status}`); let turn = await response.json(); response = await fetch("https://api.paxalabs.com/v1/voices/design", { method: "POST", headers, body: JSON.stringify({ draft: turn.draft, instruction: "A little slower, with a softer ending", }), }); if (!response.ok) throw new Error(`Refine failed: ${response.status}`); turn = await response.json(); await writeFile("line.mp3", Buffer.from(turn.audio, "base64")); ``` Python: ```python import base64 import os import requests URL = "https://api.paxalabs.com/v1/voices/design" HEADERS = {"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"} # Design, then refine the draft with an instruction. turn = requests.post( URL, headers=HEADERS, json={"description": "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai"}, ) turn.raise_for_status() turn = requests.post( URL, headers=HEADERS, json={ "draft": turn.json()["draft"], "instruction": "A little slower, with a softer ending", }, ) turn.raise_for_status() with open("line.mp3", "wb") as f: f.write(base64.b64decode(turn.json()["audio"])) ``` A design or refine call costs 5 credits, its line included. A call that fails without a voice is refunded, and a call you abandon keeps its charge. The console's Voice design tool runs the same steps without code. It can also read a line of your own of up to 120 characters, billed at the designed voice rate below. ## Use the id The id works only for the account that saved it, from any of its API keys, and only with `paxa-tts-flash-v2`. Case is ignored. [GET /v1/voices/designed](https://paxalabs.com/docs/api/designed-voices) lists the account's designed voices with their ids, and [GET /v1/voices](https://paxalabs.com/docs/api/voices) lists the catalog only. Another account, another model, or a deleted voice answers [400 unknown_voice](https://paxalabs.com/docs/errors#unknown_voice). A designed voice reads the same [direction tags](https://paxalabs.com/docs/directions) as every Paxa TTS Flash v2 voice. > Designed voices speak through your own API keys and the console only. Model marketplaces that resell Paxa Labs speech, and the speech demo on this site, serve catalog voices. ## Pricing Speech in a designed voice bills at 1.25 times the model's per-character rate. The premium adds to the [timestamps](https://paxalabs.com/docs/timestamps) premium. A timed request in a designed voice bills at 1.5 times. A 36-character request costs 0.36 credits in a catalog voice, 0.45 in a designed voice, and 0.54 in a designed voice with timestamps. A live connection bills its cumulative text at the same rates. Rounding and the request minimum work as described in [Credits and billing](https://paxalabs.com/docs/credits). ## Voices per plan The plan sets how many designed voices an account keeps. At the limit, saving another voice is refused until one is deleted. After a downgrade below the voices already saved, every saved voice keeps speaking and new saves are refused. Today the limits are: | Plan | Designed voices | | --- | --- | | Free | 1 | | Starter | 3 | | Standard | 10 | | Pro | 50 | ## Rename and delete A saved voice cannot be changed. [Rename](https://paxalabs.com/docs/api/rename-voice) or [delete](https://paxalabs.com/docs/api/delete-voice) it through the API or in the Voice design tool, and design a new voice for a different sound. Renaming keeps the id. Deleting a voice cannot be undone and frees its place in the plan's limit. Every later request naming its id answers 400 unknown_voice, including a retry under the original [Idempotency-Key](https://paxalabs.com/docs/idempotency), and the original request's charge stands. A live connection that opened with the voice keeps speaking in it until the connection closes. # Two-way live speech over WebSocket Live streaming is a two-way WebSocket for a whole conversation: send text as it becomes available and audio frames stream back, with billing receipts on the same connection. [The live endpoint](https://paxalabs.com/docs/api/live) holds one WebSocket open for a whole conversation: send text whenever it becomes available and receive audio frames as soon as they are ready, in both directions at once. Use it when text arrives incrementally, for example from an LLM token stream or a dialogue turn under composition. When each request's text is already complete, [HTTP streaming](https://paxalabs.com/docs/streaming) delivers the same low time to first byte with no connection to manage. | | HTTP streaming | Live connection | | --- | --- | --- | | Transport | One `POST /v1/tts` per request | One WebSocket for many syntheses | | Text | Complete per request | Incremental fragments | | Billing | Per request, 0.1 credit minimum each | Cumulative per connection, one minimum | | Timing | `timestamps` per request | Set once in `start`, timing events per leg | ## Open a session 1. Connect: Open the WebSocket to the live endpoint with the API key in the `Authorization` header, exactly as on HTTP requests. A refused upgrade answers ordinary problem+json. 2. start: The first frame configures the connection: `model` and `voice`, plus optional `format`, `sample_rate`, `timestamps`, `speed`, `language`, and [`temperature`](https://paxalabs.com/docs/temperature). One configuration per connection. Each leg is its own synthesis. Under the default `language`, `auto`, a leg that is only a number reads as Thai. Pin `th` or `en` to read every leg's numbers and codes the same way. 3. text: Send text fragments as they become available. The server accumulates them and synthesizes at a natural boundary: a newline, sentence-ending punctuation, or a length threshold. Thai text carries no terminal punctuation. A Thai client reaches the length threshold, or sends a flush frame at the end of a turn. On a voice that reads [emotion tags](https://paxalabs.com/docs/emotion-tags#live) or [direction tags](https://paxalabs.com/docs/directions#live), a tag still open at a boundary waits until it closes. One still open past the length threshold is read as text and is not checked as a tag. Each leg opens in the delivery the last one ended in. 4. flush: Force synthesis of everything buffered so far, for example at the end of a dialogue turn. Flushing an empty buffer is a safe no-op. 5. cancel: Stop the leg being spoken and discard the buffered text, for example when the speaker talks over the reply. The connection stays open for the next turn. 6. end: Signal that no more text is coming. The server finishes synthesizing, sends `done`, and closes the connection. TypeScript: ```typescript // Runs on Bun as-is; on Node, `import WebSocket from "ws"` accepts // the same constructor options. Browsers cannot send this header, and // API keys never ship to browsers anyway. const socket = new WebSocket("wss://api.paxalabs.com/v1/tts/live", { headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}` }, }); socket.binaryType = "arraybuffer"; socket.onopen = () => { socket.send(JSON.stringify({ type: "start", model: "paxa-tts-flash-v2", voice: "khanomkrok", timestamps: "word", })); // Send text whenever it becomes available, token by token if you like. socket.send(JSON.stringify({ type: "text", text: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs" })); socket.send(JSON.stringify({ type: "flush" })); socket.send(JSON.stringify({ type: "end" })); }; socket.onmessage = (event) => { if (typeof event.data === "string") { const message = JSON.parse(event.data); if (message.type === "done") socket.close(); return; } player.feed(new Uint8Array(event.data)); // binary frames are audio }; ``` Python: ```python import json import os from websockets.sync.client import connect # pip install websockets with connect( "wss://api.paxalabs.com/v1/tts/live", additional_headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, ) as socket: socket.send(json.dumps({"type": "start", "model": "paxa-tts-flash-v2", "voice": "khanomkrok"})) socket.send(json.dumps({"type": "text", "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs"})) socket.send(json.dumps({"type": "flush"})) socket.send(json.dumps({"type": "end"})) with open("speech.mp3", "wb") as f: for frame in socket: if isinstance(frame, bytes): f.write(frame) # binary frames are audio continue event = json.loads(frame) if event["type"] == "done": break ``` ## Server events Binary frames carry audio in the format chosen at `start`. Every other frame is a JSON event. A `charged` event is a billing receipt. A `timing` event carries spans when [timestamps](https://paxalabs.com/docs/timestamps) are on. An `error` event carries the same stable codes as [Errors](https://paxalabs.com/docs/errors). A `flushed` event acknowledges a drained buffer, and a `cancelled` event acknowledges an interruption. A `done` event closes the exchange. The [API reference](https://paxalabs.com/docs/api/live) documents every message shape, and the [text to speech page](https://paxalabs.com/text-to-speech) traces a live session frame by frame. Server events: ```json {"type":"started","connection":"7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a","model":"paxa-tts-flash-v2","voice":"khanomkrok","format":"mp3","sample_rate":44100,"timestamps":"word"} {"type":"charged","seq":1,"chars":36,"credits":0.45,"total_credits":0.45} {"type":"timing","seq":1,"spans":[{"text":"สวัสดี","start":0,"end":0.51},{"text":"ครับ","start":0.51,"end":0.85}]} {"type":"flushed","seq":1} {"type":"done","total_chars":36,"total_credits":0.45} ``` ## Interrupt the speaker A `cancel` frame stops the leg being spoken and discards whatever text is still buffered. Audio for that leg ends, a `cancelled` event names the leg that stopped, and the connection stays open. The next `text` frame starts a new leg on the same session. This is how an assistant handles a speaker talking over it. Closing the connection would work too, but it gives up the concurrency slot and costs a full upgrade to get back. An interrupted leg keeps the charge its `charged` receipt reported, because that text had already been synthesized when the interruption arrived. The buffered text discarded alongside it was never charged at all. Sending one sentence per `text` frame therefore bounds what an interruption costs to the sentence being spoken, which is the same shape that keeps time to first audio low. Barge-in: ```typescript // Stop the reply the moment the speaker starts. The sentence being // spoken ends where it is, whatever was queued behind it is dropped, // and the same socket carries the next turn. speech.onSpeechStart = () => { socket.send(JSON.stringify({ type: "cancel" })); player.discard(); // drop the audio already buffered on the client }; socket.onmessage = (event) => { if (typeof event.data !== "string") { player.feed(new Uint8Array(event.data)); return; } const message = JSON.parse(event.data); if (message.type === "cancelled") { // No more audio arrives for message.seq. That leg keeps the charge // its receipt reported; the discarded text was never charged. return; } if (message.type === "done") socket.close(); }; ``` ## Billing A connection is billed cumulatively at the same rate as every other request. Speech costs 10 credits per 1,000 characters, or 1.25 times that when timestamps are on, billed per character and rounded up to the next hundredth of a credit. The 0.1 credit minimum applies once per connection, however many messages it carries. Short fragments sent on one connection therefore pay the minimum once: | Delivery | Billed characters | Credits | | --- | --- | --- | | One live connection | 15 | 0.15 | | 3 separate HTTP requests | 4, 5, 6 | 0.3 | Each `charged` event is a receipt for one increment. The connection's total is the sum of its receipts, and [GET /v1/me](https://paxalabs.com/docs/api/me) reflects it live. A WebSocket carries no `x-credits-charged` header, and the receipts report the charge in its place. ## Handle errors and close Failures before the connection upgrades are ordinary HTTP `application/problem+json` responses. After the upgrade, failures arrive as `error` events whose `code` matches the [error catalog](https://paxalabs.com/docs/errors). Recoverable failures leave the connection open for a retry: [rate_limited](https://paxalabs.com/docs/errors#rate_limited), a failed leg reporting [provider_error](https://paxalabs.com/docs/errors#provider_error), which is refunded automatically, and [tag_invalid](https://paxalabs.com/docs/errors#tag_invalid) for a tag the voice cannot read. After `tag_invalid` the buffer keeps what it held before the `text` frame. Credit exhaustion and protocol violations such as [live_protocol](https://paxalabs.com/docs/errors#live_protocol) close the connection with the code listed in the [reference](https://paxalabs.com/docs/api/live). A connection that closes without a `done` event did not finish delivering. Treat the tail as missing. > Each synthesis leg counts as one request against the plan's [rate limit](https://paxalabs.com/docs/rate-limits), exactly like a `POST /v1/tts` call. A `rate_limited` error keeps the buffer and the connection and pauses auto-flush. Send `flush` after `retry_after` seconds, or send `cancel` to drop the buffered text and start a new turn, which auto-flushes again. The connection itself holds one of the plan's [concurrent speech slots](https://paxalabs.com/docs/rate-limits#concurrency) for as long as it stays open. An upgrade past the limit is refused with `429 concurrency_limited`. Close connections you no longer need. # Optimize latency What the API does between a speech request and its first audio, which of those steps a client controls, and what each choice costs. Between a speech request and its first audio, the API verifies the key, charges the request, prepares the text, and synthesizes the opening of it. curl: ```bash curl -N -s -o /dev/null -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "stream": true}' \ -w "connect %{time_connect}s, tls %{time_appconnect}s, first byte %{time_starttransfer}s\n" ``` TypeScript: ```typescript async function firstByteMs(text: string) { const started = performance.now(); 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, voice: "khanomkrok", model: "paxa-tts-flash-v2", stream: true, }), }); if (!response.ok || !response.body) { throw new Error(`TTS failed: ${response.status}`); } const reader = response.body.getReader(); await reader.read(); const elapsed = performance.now() - started; // Read to the end. The client closes a connection with an unread body. while (!(await reader.read()).done); return elapsed; } // The first call includes the TCP and TLS handshakes. // The second call reuses the connection. console.log(await firstByteMs("สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs")); console.log(await firstByteMs("สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs")); ``` Python: ```python import os import time import requests # One session keeps the connection open between requests. session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['PAXA_API_KEY']}" def first_byte_ms(text): started = time.perf_counter() with session.post( "https://api.paxalabs.com/v1/tts", json={"text": text, "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "stream": True}, stream=True, ) as response: response.raise_for_status() chunks = response.iter_content(chunk_size=None) next(chunks) elapsed = (time.perf_counter() - started) * 1000 # Read to the end. The client closes a connection with an unread body. for _ in chunks: pass return elapsed # The first call includes the TCP and TLS handshakes. # The second call reuses the connection. print(first_byte_ms("สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs")) print(first_byte_ms("สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs")) ``` ## What runs before synthesis The API caches a verified key for 5 minutes, and a request that arrives after the entry expires adds a database lookup. It also caches the account balance for 60 minutes after each charged request. When the cached balance is at least 4 times the cost of the request, synthesis starts at the same time as the charge. The charge still completes before the API sends the first byte, and a request that the balance cannot cover answers [402 insufficient_credits](https://paxalabs.com/docs/errors#insufficient_credits) with no audio. In four cases the API completes the charge first and then starts synthesis, which adds the time of the charge to the wait: - The cached balance is under 4 times the cost of the request. - The account has had no charged request in the last 60 minutes, and no balance is cached. - The key has a [credit limit](https://paxalabs.com/docs/authentication#spending-caps). Use a key without one for requests where first audio matters, and keep the limits on batch keys. - The request carries an [Idempotency-Key](https://paxalabs.com/docs/idempotency). Send the key where a failed request is retried, and leave it off a conversation turn that is spoken again without a retry. ## Warm the caches before the first turn [GET /v1/me](https://paxalabs.com/docs/api/me) caches the key and opens the connection, but it charges nothing and leaves the balance uncached. Only a charged request caches the balance. To have both caches ready before a call's first reply, synthesize one word when the call starts. A first request that skips this step waits for a key lookup and for a charge that finishes before synthesis starts. ## Split long text A streamed response starts from the opening of the text while the rest is still being synthesized, which keeps first audio nearly independent of length. Text preparation is the exception. It covers the whole text before synthesis begins, and its time grows in proportion to the length. When first audio matters on a long text, send the opening paragraph as its own request and queue the remainder behind it. ## Read the stream at network speed A streamed request holds its [concurrency slot](https://paxalabs.com/docs/rate-limits#concurrency) until the response finishes, and synthesis runs many times faster than playback. A client that reads at playback speed holds the slot for the length of the audio. Read the body as fast as the network delivers it and buffer on your side, which frees the slot for the next request. ## Choose between HTTP and a live connection A [live connection](https://paxalabs.com/docs/live-streaming) authenticates once and then sends only `text` frames, but it completes the charge for each synthesis before that synthesis starts. For one complete sentence, a streamed HTTP request on an open connection does less work on the server. The live connection is faster when text arrives in pieces from a language model, and when the alternative is a new connection for every turn. Open it when the conversation starts. When the user interrupts, send `cancel` and keep it open, because a closed connection costs a full upgrade to replace. ## Formats and timestamps `wav` and `pcm` skip the encoder, `mp3` adds a short encode, and `opus` adds the longest one. Each is small beside synthesis. A streamed `wav` response is 16-bit little-endian mono PCM at 24 kHz behind a 44-byte header whose two size fields are `0xFFFFFFFF`. `pcm` delivers the same samples with no header. A pipeline that takes raw PCM passes them on with no decoder and nothing to strip. [`timestamps`](https://paxalabs.com/docs/timestamps) do not delay first audio. They change a streamed response to NDJSON events that carry the audio as base64, about one third larger and sent uncompressed. A buffered response with timestamps is gzipped when the client accepts gzip, which removes most of that overhead. ## Transport The API host serves HTTP/2 and HTTP/3. One HTTP/2 connection carries concurrent requests up to your plan's concurrency limit, which removes the need for a connection pool. A response abandoned before its end closes an HTTP/1.1 connection and resets only its own stream on HTTP/2. Preflight responses carry an `access-control-max-age` of 24 hours. ## Retries The API does not queue requests. A request over the per-minute window or the concurrency limit answers 429 at once. A refused request still counts against the window, which means a client that retries through a 429 extends its own lockout. Wait for `retry-after` on [rate_limited](https://paxalabs.com/docs/errors#rate_limited). On [concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited), wait until one of your own requests finishes. # Transcribe recordings The transcription endpoint turns a recording of Thai speech, or of another language, into a verbatim transcript, with word timestamps on request. Billing is by recording length, charged before transcription and refunded automatically on failure. [POST /v1/stt](https://paxalabs.com/docs/api/stt) reads a recording and returns what was said. Send the file as base64 in `audio` with the model id `paxa-stt-lite-v1-preview`. The transcript comes back verbatim, in the language spoken: a Thai sentence that switches into English mid-way is written as spoken. Nothing is translated. Like every endpoint, the request is charged before inference and refunded automatically when inference fails ([Credits](https://paxalabs.com/docs/credits)). > paxa-stt-lite-v1-preview is a research preview. Requests bill at the published rate. The transcript quality and the option set may change between releases, and the changelog on [the product page](https://paxalabs.com/speech-to-text#changelog) records each change. curl: ```bash # Encode without line wrapping: wrapped base64 breaks the JSON string. AUDIO=$(base64 < meeting.m4a | tr -d '\n') # --max-time covers an hour-long recording; curl defaults to no limit. curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\"}" ``` TypeScript: ```typescript import { readFile } from "node:fs/promises"; const audio = (await readFile("meeting.m4a")).toString("base64"); const response = await fetch("https://api.paxalabs.com/v1/stt", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ audio, model: "paxa-stt-lite-v1-preview" }), // A long recording can run for minutes; give it room. signal: AbortSignal.timeout(300_000), }); if (!response.ok) throw new Error(`Transcription failed: ${response.status}`); const { text, usage } = await response.json(); console.log(text, `${usage.credits} credits`); ``` Python: ```python import base64 import os import requests with open("meeting.m4a", "rb") as file: audio = base64.b64encode(file.read()).decode() response = requests.post( "https://api.paxalabs.com/v1/stt", headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, json={"audio": audio, "model": "paxa-stt-lite-v1-preview"}, # A long recording can run for minutes; give it room. timeout=300, ) response.raise_for_status() body = response.json() print(body["text"], body["usage"]["credits"], "credits") ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "usage": { "seconds": 11.952, "credits": 1.66 } } ``` ## Billing A recording costs 8.33 credits per minute of audio, 500 credits per hour, billed by its exact length. The length is read from the file itself before the charge. The cost is known before any listening starts. Every request costs at least 0.1 credits. Word timestamps, speaker turns, the style, the language hint, the convention, and the vocabulary cost nothing extra. The charge is reported only in the body's `usage.credits`, beside `usage.seconds`, the length that was billed. This endpoint sends no `x-credits-charged` header. A fast response and a slow one carry the same wire surface. When transcription fails after the charge, the refund is automatic and the response is [502 provider_error](https://paxalabs.com/docs/errors#provider_error). A recording with no speech is a delivered empty transcript and is billed for its length. ## Recordings and formats `audio` carries the raw file bytes as base64. Seven formats are read: MP3, WAV (PCM), FLAC, Ogg (Opus or Vorbis), M4A, AAC (ADTS), and WebM. The format and the length are read from the bytes. No declared field is consulted. A file that cannot be read as one of the seven answers [400 audio_invalid](https://paxalabs.com/docs/errors#audio_invalid). A file whose length cannot be read from its container, such as a FLAC written without a sample count, answers the same code. None of this charges. Base64 grows a payload by about a third, and a compressed file travels faster: Opus at 24 kbps or MP3 at 64 kbps carries an hour of speech under the size limit. A recording captured in the browser with MediaRecorder is a WebM file. Its length is read from its blocks, and it needs no re-encoding. ## Length and size limits One request transcribes at most 60 minutes, and a longer recording answers [400 audio_too_long](https://paxalabs.com/docs/errors#audio_too_long). The decoded file may be at most 25 MiB, and a larger one answers [413 audio_too_large](https://paxalabs.com/docs/errors#audio_too_large). Neither refusal charges anything. Split longer recordings at a pause and send the parts as separate requests. [Speakers](#speakers) have a shorter ceiling of their own: a recording of 9 minutes or longer with `diarization` set to `true` answers [400 diarization_audio_too_long](https://paxalabs.com/docs/errors#diarization_audio_too_long), uncharged. The same recording transcribes normally with the field off. ## Language `language` tells the model what to expect, as a BCP 47 tag such as `"th"` or `"en"`. The hint filters nothing. Speech in any other language is still transcribed as spoken. A Thai recording with English words and phrases inside it needs no setting. Omitted, the model detects the language itself. Set the tag when detection has picked the wrong language for a recording. ## Word timestamps Set `timestamps` to `"word"` and the response carries a `words` array: one span per recognized word with its text and its start and end in seconds on the recording's own clock. Thai words are segmented for you, and Latin words and numerals arrive whole. A punctuation mark joins the word before it, or the word after it when it opens. It is the same span shape the [speech API](https://paxalabs.com/docs/timestamps) returns. A caption file built from one round-trips through the other. Thai writes no spaces between words. The spans are what say where a word ends. For a subtitle file, [subtitles](#subtitles) cuts one from them for you. A recording with no speech carries an empty array. curl: ```bash AUDIO=$(base64 < meeting.m4a | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"timestamps\": \"word\"}" ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "words": [ { "text": "สวัสดี", "start": 0.12, "end": 0.44 }, { "text": "ค่ะ", "start": 0.52, "end": 0.66 }, { "text": "คุณ", "start": 1.08, "end": 1.24 }, { "text": "สมชาย", "start": 1.32, "end": 1.88 }, { "text": "ยอด", "start": 2.36, "end": 2.56 }, { "text": "ชำระ", "start": 2.6, "end": 2.879 }, { "text": "ของ", "start": 3, "end": 3.14 }, { "text": "ท่าน", "start": 3.2, "end": 3.359 }, { "text": "จำนวน", "start": 3.48, "end": 3.879 }, { "text": "พันสองร้อยห้าสิบ", "start": 4.24, "end": 5.4 }, { "text": "บาท", "start": 5.56, "end": 5.759 }, { "text": "ครบ", "start": 6.32, "end": 6.52 }, { "text": "กำหนด", "start": 6.56, "end": 6.84 }, { "text": "วัน", "start": 6.92, "end": 7.039 }, { "text": "ที่", "start": 7.04, "end": 7.119 }, { "text": "สิบห้า", "start": 7.24, "end": 7.48 }, { "text": "สิงหาคม", "start": 7.68, "end": 8.26 }, { "text": "นี้", "start": 8.36, "end": 8.5 }, { "text": "ชำระ", "start": 9.08, "end": 9.36 }, { "text": "ผ่าน", "start": 9.48, "end": 9.68 }, { "text": "แอป", "start": 9.76, "end": 9.92 }, { "text": "ได้", "start": 10, "end": 10.14 }, { "text": "ตลอด", "start": 10.2, "end": 10.46 }, { "text": "ยี่สิบสี่", "start": 10.56, "end": 10.96 }, { "text": "ชั่วโมง", "start": 11.08, "end": 11.4 }, { "text": "ค่ะ", "start": 11.52, "end": 11.66 } ], "usage": { "seconds": 11.952, "credits": 1.66 } } ``` ## Subtitles Set `subtitles` to `"srt"` or `"vtt"` and the response carries a `subtitles` string: the whole file, ready to write to disk. SubRip numbers its cues and writes a comma before the milliseconds; WebVTT opens with its signature and writes a period. Word timing is read for you. `timestamps` need not be set. Setting it as well returns the `words` array beside the file. Neither costs anything extra. Cues break at a speaker change, at sentence punctuation, and at a pause, then at the line budget. A cue holds at most two lines of 60 columns, or 32 (16 Chinese characters) when most of the transcript is Chinese, Japanese, or Korean. It stays on screen for at least a second, and stops short of the cue after it. Every line break falls on a word boundary. On Thai that is the whole difference. Thai writes no spaces. A subtitle wrapped at a character count splits words down the middle. The word spans say where a word ends, and the breaks land there. Thai vowels and tone marks sit above and below their consonant and take no width of their own. They are not counted either. A Chinese, Japanese, or Korean character takes two columns. A Thai line and an English line of 60 columns fill the same box. Set `subtitle_line_chars` to your own house figure, between 10 and 120. With `diarization` on, a cue never runs across a speaker change, and no speaker is named in the file. A recording with no speech renders an empty SubRip file, or a WebVTT file carrying its signature alone. curl: ```bash AUDIO=$(base64 < meeting.m4a | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"subtitles\": \"srt\"}" \ | jq -r .subtitles > meeting.srt ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "subtitles": "1\n00:00:00,120 --> 00:00:01,000\nสวัสดีค่ะ\n\n2\n00:00:01,080 --> 00:00:02,080\nคุณสมชาย\n\n3\n00:00:02,360 --> 00:00:05,759\nยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาท\n\n4\n00:00:06,320 --> 00:00:08,500\nครบกำหนดวันที่ สิบห้า สิงหาคมนี้\n\n5\n00:00:09,080 --> 00:00:11,660\nชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ\n", "usage": { "seconds": 11.952, "credits": 1.66 } } ``` ## Spoken or written `convention` selects how the transcript writes what was said, and defaults to `"spoken"`. The words are the same under both. What changes is numbers, units, dates, and repeated words. The recording behind the samples on this page says an amount, a date, and a duration: | What was said | spoken (default) | written | | --- | --- | --- | | 1,250 baht, said in Thai | พันสองร้อยห้าสิบบาท | 1,250บาท | | the 15th of August | สิบห้าสิงหาคม | 15สิงหาคม | | 24 hours | ยี่สิบสี่ชั่วโมง | 24ชั่วโมง | The whole transcript under `"spoken"` reads `สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ`. Under `"written"` it reads `สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน 1,250 บาทครบกำหนดวันที่ 15 สิงหาคมนี้ชำระผ่านแอปได้ตลอด 24 ชั่วโมงค่ะ`. `"spoken"` writes numbers as they were pronounced and a word said twice as two words. Use it for a verbatim record, a subtitle that must match the audio, or text a language model will read, since the spoken form carries exactly what the speaker said. `"written"` keeps display conventions: digits and units, the repetition mark ๆ. Use it for dictated text that will be read on a page, such as a memo or a message. Word timestamps are identical under both. curl: ```bash AUDIO=$(base64 < meeting.m4a | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"convention\": \"written\"}" ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน 1,250 บาทครบกำหนดวันที่ 15 สิงหาคมนี้ชำระผ่านแอปได้ตลอด 24 ชั่วโมงค่ะ", "usage": { "seconds": 11.952, "credits": 1.66 } } ``` ## Verbatim or clean `style` selects how closely the transcript follows the recording, and defaults to `"verbatim"`: fillers, false starts, and self-corrections are written as they were said. `"clean"` drops the fillers and merges broken phrases into one. It is a readability choice. A clean transcript departs further from what was said, and our own measurement found it less exact. Pick it for a transcript that readers skim. A record of what was said keeps the default. Any other value answers 400 `validation`. The convention applies on top of either style. ## Speakers Set `diarization` to `true` and the response carries a `segments` array: one entry per speaker turn with its `speaker` number, its `text`, and its start and end in seconds. Speakers are numbered from 0 in order of first appearance in the recording. Nobody is identified. A turn's text is a substring of the transcript. The two never disagree. With `timestamps` set to `"word"` as well, every word span names its `speaker`. A diarized recording is read in one pass. Speaker numbers hold across the whole file. That pass is also what limits it. A diarized recording must be under 9 minutes, and one that long or longer answers [400 diarization_audio_too_long](https://paxalabs.com/docs/errors#diarization_audio_too_long) before any charge. It also takes longer to return than an undiarized one. Split a longer recording at a pause and read the speaker numbers per part. Each part numbers its speakers from 0. The recording behind the samples on this page has one voice. It comes back as one turn: curl: ```bash AUDIO=$(base64 < meeting.m4a | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"diarization\": true}" ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "segments": [ { "speaker": 0, "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "start": 0.12, "end": 11.66 } ], "usage": { "seconds": 11.952, "credits": 1.66 } } ``` ## Vocabulary `vocabulary` lists terms the recording is likely to contain, spelled the way they should appear in the transcript. Product names, people, places, and jargon qualify, in Thai or any other language. The list holds up to 50 terms of 50 characters each, and a longer list or term answers 400 `validation`. The terms bias recognition and are never inserted. A term that was not said does not appear. It is the lever for domain content: a meeting about your own product transcribes the product's name the way you spell it. A vocabulary combines with every other option. curl: ```bash AUDIO=$(base64 < meeting.m4a | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"language\": \"th\", \"vocabulary\": [\"Paxa Labs\", \"quarterly review\"]}" ``` ## Reliability and retries The response is one buffered JSON body. An hour-long recording can take a minute or two. The recording is read in parallel pieces cut at pauses, and the wait grows slowly with the length. Set the client timeout generously. The samples above allow 300 seconds. A request that runs long enough writes its response headers before the outcome is known. Intermediate proxies abandon a connection that has been silent too long, and the early headers prevent that. Such a response always has status 200. It reports a failure in the body, as the same problem document a failed request returns: a `title` and a `status` where `text` would be. Long recordings are the requests that reach it. Treat a body carrying `title` as the error it describes. Shorter recordings always carry their status on the status line. Retries are safe with an [Idempotency-Key](https://paxalabs.com/docs/idempotency). A replay of a delivered request serves the same transcript again without a second charge. The key covers the recording and every option. A replay with a different convention, style, or vocabulary is refused. When transcription fails, the charge is refunded and a fresh request needs a new key. ## Realtime, with turn detection [WS /v1/stt/live](https://paxalabs.com/docs/api/stt-live) transcribes speech as it is spoken, over one WebSocket that stays open for the conversation. The model is `paxa-stt-lite-realtime-v1-preview`, tuned for voice agents. It listens to the microphone and notices when the user starts and stops talking. It delivers one finished transcript per turn. The agent never decides where a turn ends and never splits audio into requests. Send `start` with the model id and the audio format. Then stream raw audio as binary frames as it is captured. The audio is 16-bit PCM at 16 or 48 kHz, or mu-law at 8 kHz as telephony delivers it, mono either way. Send `end` when the conversation is over and wait for `done`. TypeScript: ```typescript import { open } from "node:fs/promises"; // Runs on Bun as-is; on Node, `import WebSocket from "ws"` accepts // the same constructor options. Browsers cannot send this header, and // API keys never ship to browsers anyway: relay the audio through your // server. const socket = new WebSocket("wss://api.paxalabs.com/v1/stt/live", { headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}` }, }); socket.onopen = async () => { socket.send(JSON.stringify({ type: "start", model: "paxa-stt-lite-realtime-v1-preview", audio: { encoding: "pcm_s16le", sample_rate: 16000 }, language: "th", })); // 16 kHz signed 16-bit mono is 32,000 bytes a second. A microphone // delivers frames at this pace on its own; a file has to be paced. const file = await open("caller.pcm"); const frame = new Uint8Array(640); // 20 ms for (;;) { const { bytesRead } = await file.read(frame, 0, frame.length); if (bytesRead === 0) break; socket.send(frame.subarray(0, bytesRead)); await new Promise((resolve) => setTimeout(resolve, 20)); } socket.send(JSON.stringify({ type: "end" })); }; socket.onmessage = (event) => { const message = JSON.parse(String(event.data)); switch (message.type) { case "speech_started": // The user is talking: stop the agent's audio, cancel its reply. break; case "transcript": if (message.is_final) console.log(`turn ${message.turn}: ${message.text}`); break; case "done": socket.close(); } }; ``` Python: ```python import json import os import time from websockets.sync.client import connect # pip install websockets with connect( "wss://api.paxalabs.com/v1/stt/live", additional_headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, ) as socket: socket.send(json.dumps({ "type": "start", "model": "paxa-stt-lite-realtime-v1-preview", "audio": {"encoding": "pcm_s16le", "sample_rate": 16000}, "language": "th", })) # 16 kHz signed 16-bit mono is 32,000 bytes a second; a file has to # be paced the way a microphone paces itself. with open("caller.pcm", "rb") as audio: while frame := audio.read(640): # 20 ms socket.send(frame) time.sleep(0.02) socket.send(json.dumps({"type": "end"})) for message in socket: event = json.loads(message) if event["type"] == "transcript" and event["is_final"]: print(event["turn"], event["text"]) elif event["type"] == "done": break ``` Each turn produces three events in order. `speech_started` arrives as soon as the user begins talking. An agent that is speaking stops its playback and cancels the reply it was generating. `speech_ended` arrives when the user has stopped. `transcript` with `is_final` true follows with the turn's text. The agent sends that text to the language model. Between the first and the last, a `transcript` with `is_final` false may arrive at a natural pause, carrying the turn so far. Use it to warm up the reply and replace it with the final. Every event names its `turn`, numbered from 1. A reply built for turn 3 is discarded the moment turn 4 starts. Times are seconds on the connection's clock, which starts at the first audio frame. Server events: ```json {"type":"started","connection":"7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a","model":"paxa-stt-lite-realtime-v1-preview"} {"type":"speech_started","turn":1,"time":0.3} {"type":"speech_ended","turn":1,"time":12.252} {"type":"charged","seq":1,"seconds":12.252,"credits":2.56,"total_credits":2.56} {"type":"transcript","turn":1,"is_final":true,"text":"สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ","start":0.3,"end":12.252} {"type":"done","total_seconds":12.252,"turns":1,"total_credits":2.56} ``` The session decides where a turn ends; there is no setting to tune. When the agent plays its own audio into the same room, send `playback` with `speaking` true while it plays and false when it stops. The turn detector then expects the user to cut in and does not mistake the agent's echo for a turn. The `language`, `style`, `convention`, `vocabulary`, and `timestamps` options work as they do on a recording and are fixed for the connection. Realtime transcription costs 12.5 credits per minute of audio received, 750 credits per hour. Silence counts, because the model listens to all of it. The charge is taken in periods. A period closes at the end of each turn, after every minute of audio without a turn, and at close. A `charged` event reports each period with the seconds it covered and the running total. `done` carries the connection's totals. A turn's final transcript arrives only after its period is charged. A turn the model could not transcribe costs nothing and is reported as an `error` naming the turn. A connection that sent any audio costs at least 0.1 credits. An open connection holds one of the plan's [concurrent transcription slots](https://paxalabs.com/docs/rate-limits#concurrency), and a connection accepts up to 4 hours of audio before it closes with `done`. Close codes and every frame field are in the [endpoint reference](https://paxalabs.com/docs/api/stt-live). - [Transcribe a recording](https://paxalabs.com/docs/api/stt): Full request and response reference - [Realtime transcription](https://paxalabs.com/docs/api/stt-live): Realtime transcription reference - [Get word-level timestamps](https://paxalabs.com/docs/timestamps): The same spans on synthesized speech - [Credits, charges, and refunds](https://paxalabs.com/docs/credits): How credits and refunds work - [Make retries safe with idempotency keys](https://paxalabs.com/docs/idempotency): Retry safely with an Idempotency-Key - [Error codes and responses](https://paxalabs.com/docs/errors): Every error code # Read documents with OCR The OCR endpoint reads PDFs and images and returns their text as Markdown or structured layout blocks. Billing is per page, charged before reading and refunded automatically on failure. [POST /v1/ocr](https://paxalabs.com/docs/api/ocr) reads a PDF, PNG, JPEG, or WebP file and returns its text. Send the file as base64 in `document` with the model id `paxa-ocr-lite-v1`, and the response carries one entry per page. Like every endpoint, the request is charged before inference and refunded automatically when inference fails ([Credits](https://paxalabs.com/docs/credits)). curl: ```bash # Encode without line wrapping: wrapped base64 breaks the JSON string. DOC=$(base64 < invoice.pdf | tr -d '\n') # --max-time covers a multi-page document; curl defaults to no limit. curl -X POST https://api.paxalabs.com/v1/ocr \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"document\": \"$DOC\", \"model\": \"paxa-ocr-lite-v1\"}" ``` TypeScript: ```typescript import { readFile } from "node:fs/promises"; const document = (await readFile("invoice.pdf")).toString("base64"); const response = await fetch("https://api.paxalabs.com/v1/ocr", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ document, model: "paxa-ocr-lite-v1" }), // A multi-page document can run for minutes; give it room. signal: AbortSignal.timeout(300_000), }); if (!response.ok) throw new Error(`OCR failed: ${response.status}`); const { pages, usage } = await response.json(); console.log(pages[0].markdown, `${usage.credits} credits`); ``` Python: ```python import base64 import os import requests with open("invoice.pdf", "rb") as file: document = base64.b64encode(file.read()).decode() response = requests.post( "https://api.paxalabs.com/v1/ocr", headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, json={"document": document, "model": "paxa-ocr-lite-v1"}, # A multi-page document can run for minutes; give it room. timeout=300, ) response.raise_for_status() body = response.json() print(body["pages"][0]["markdown"], body["usage"]["credits"], "credits") ``` Response: ```json { "pages": [ { "page": 1, "markdown": "# ใบเสร็จรับเงิน\n\nร้านข้าวแกงบ้านสวน สาขาสีลม\n\n| รายการ | จำนวน | ราคา |\n| --- | --- | --- |\n| ข้าวแกงเขียวหวานไก่ | 1 | 60 |\n| น้ำเปล่า | 1 | 10 |\n\nรวมทั้งสิ้น 70 บาท" } ], "usage": { "pages": 1, "credits": 6.5 } } ``` ## Billing A page costs 6.5 credits, whatever is on it. A page is one PDF page or one image file. The page count is read from the document itself before the charge. The cost is known before any reading starts. The charge is reported only in the body's `usage.credits`. This endpoint sends no `x-credits-charged` header. A fast response and a slow one carry the same wire surface. When reading fails after the charge, the refund is automatic and the response is [502 provider_error](https://paxalabs.com/docs/errors#provider_error). ## Documents and formats `document` carries the raw file bytes as base64. Four formats are read: PDF, PNG, JPEG, and WebP. The format is detected from the bytes, and an image always counts as one page. A file that cannot be read as one of the four answers [400 document_invalid](https://paxalabs.com/docs/errors#document_invalid), and a PDF that needs a password to open answers [400 document_password_required](https://paxalabs.com/docs/errors#document_password_required). A PDF that carries permissions-only encryption, the kind that opens without being asked for a password, is read normally. None of this charges. Base64 grows a payload by about a third. Mind your client's body limits when sending large scans, and remove PDF passwords before encoding. ## Page and size limits One request reads at most 50 pages, and a longer document answers [400 too_many_pages](https://paxalabs.com/docs/errors#too_many_pages). The decoded file may be at most 10 MiB, and a larger one answers [413 document_too_large](https://paxalabs.com/docs/errors#document_too_large). Neither refusal charges anything. Split larger documents and send the parts as separate requests. ## Choose an output `output` selects the result shape and defaults to `"markdown"`. Both shapes carry the same reading. The difference is how much structure travels with it. ### Markdown The default returns each page as GitHub-flavored Markdown in reading order. Headings, lists, and tables arrive as Markdown structures. The output pastes into wikis, prompts, and search indexes directly. Join pages with a blank line to rebuild the document. ### Structured blocks Set `output` to `"structured"` and each page arrives as typed blocks in reading order. Each block carries its kind and its content in typed fields. A pipeline can route headings, tables, and prose without parsing Markdown. | Block | Carries | Meaning | | --- | --- | --- | | heading | text, level | A section title. level starts at 1 for the most prominent. | | paragraph | text | A run of body text. | | list | items | The list entries in order. | | table | rows | The table cells as rows of column values, first row first. | | figure | text | An image or chart. text is its caption or nearby label. | curl: ```bash DOC=$(base64 < invoice.pdf | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/ocr \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"document\": \"$DOC\", \"model\": \"paxa-ocr-lite-v1\", \"output\": \"structured\"}" ``` Response: ```json { "pages": [ { "page": 1, "blocks": [ { "type": "heading", "level": 1, "text": "ใบเสร็จรับเงิน" }, { "type": "paragraph", "text": "ร้านข้าวแกงบ้านสวน สาขาสีลม" }, { "type": "table", "rows": [ [ "รายการ", "จำนวน", "ราคา" ], [ "ข้าวแกงเขียวหวานไก่", "1", "60" ], [ "น้ำเปล่า", "1", "10" ] ] }, { "type": "paragraph", "text": "รวมทั้งสิ้น 70 บาท" } ] } ], "usage": { "pages": 1, "credits": 6.5 } } ``` ## Reliability and retries The response is one buffered JSON body. A multi-page document can take minutes. Set the client timeout generously. The samples above allow 300 seconds. A request that runs long enough writes its response headers before the outcome is known. Intermediate proxies abandon a connection that has been silent too long, and the early headers prevent that. Such a response always has status 200. It reports a failure in the body, as the same problem document a failed request returns: a `title` and a `status` where `pages` would be. Long documents are the requests that reach it. Treat a body carrying `title` as the error it describes. Shorter documents always carry their status on the status line. Retries are safe with an [Idempotency-Key](https://paxalabs.com/docs/idempotency). A replay of a delivered request serves the same reading again without a second charge. When reading fails, the charge is refunded and a fresh request needs a new key. - [Read a document](https://paxalabs.com/docs/api/ocr): Full request and response reference - [Credits, charges, and refunds](https://paxalabs.com/docs/credits): How credits and refunds work - [Make retries safe with idempotency keys](https://paxalabs.com/docs/idempotency): Retry safely with an Idempotency-Key - [Error codes and responses](https://paxalabs.com/docs/errors): Every error code # Extract fields from documents Document Extraction reads a PDF or image and fills in a schema of typed fields you define, each value the span printed in the document or null with the reason. Billing is per page at a rate the schema's size selects, charged before reading and refunded automatically on failure. [POST /v1/extract](https://paxalabs.com/docs/api/extract) reads a PDF, PNG, JPEG, or WebP file and fills in the schema you send. Send the file as base64 in `document`, the model id `paxa-doc-extract-v1`, and a `schema` of typed fields. Every value delivered is text printed in the document, read as the field's type, or null with the reason. Nothing is inferred or looked up. Like every endpoint, the request is charged before inference and refunded automatically when inference fails ([Credits](https://paxalabs.com/docs/credits)). curl: ```bash # Encode without line wrapping: wrapped base64 breaks the JSON string. DOC=$(base64 < invoice.pdf | tr -d '\n') # --max-time covers a multi-page document; curl defaults to no limit. curl -X POST https://api.paxalabs.com/v1/extract \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d @- < There is no boolean type. A checkbox's label is on the page and its tick is not. Model a checkbox as an enum over its printed labels. ## Page, size, and schema limits One request reads at most 20 pages, and a longer document answers [400 too_many_pages](https://paxalabs.com/docs/errors#too_many_pages). The decoded file may be at most 10 MiB, and a larger one answers [413 document_too_large](https://paxalabs.com/docs/errors#document_too_large). A schema counts at most 100 leaves and is at most 128 KB as JSON. The document rules of the [OCR guide](https://paxalabs.com/docs/ocr#documents) apply unchanged: the format is detected from the bytes, an image is one page, and an unreadable or password-protected file is refused before the charge. One document per request. A file holding several documents gets one set of fields. ## Reliability and retries The response is one buffered JSON body. A multi-page document can take minutes. Set the client timeout generously. The samples above allow 300 seconds. A request that runs long enough writes its response headers before the outcome is known. Intermediate proxies abandon a connection that has been silent too long, and the early headers prevent that. Such a response always has status 200. It reports a failure in the body, as the same problem document a failed request returns: a `title` and a `status` where `fields` would be. Treat a body carrying `title` as the error it describes. Retries are safe with an [Idempotency-Key](https://paxalabs.com/docs/idempotency). A replay of a delivered request serves the same fields again without a second charge. When extraction fails, the charge is refunded and a fresh request needs a new key. The model id is the contract version. The dialect, the response shape, and the leaf-counting rule are fixed under `paxa-doc-extract-v1`, and a change ships as a new id served beside it. - [Extract fields from a document](https://paxalabs.com/docs/api/extract): Full request and response reference - [Read documents with OCR](https://paxalabs.com/docs/ocr): Read documents with OCR - [Credits, charges, and refunds](https://paxalabs.com/docs/credits): How credits and refunds work - [Error codes and responses](https://paxalabs.com/docs/errors): Every error code # Translate text into Thai The translation endpoint turns text in any language into natural Thai. Control register, loanwords, and terminology with glossaries, document context, and adaptive style examples. [POST /v1/translate](https://paxalabs.com/docs/api/translate) translates text into Thai from any source language. Send one string or an array of segments with the model id `paxa-translation-lite-v1`; the response carries one Thai translation per segment with the detected source language. Like every endpoint, the request is charged before inference and refunded automatically when inference fails ([Credits](https://paxalabs.com/docs/credits)). curl: ```bash # --max-time covers a large request; curl defaults to no limit, # but most clients and SDKs time out well before a big translation ends. curl -X POST https://api.paxalabs.com/v1/translate \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Welcome to our store. Free shipping on orders over 500 baht.", "model": "paxa-translation-lite-v1" }' ``` TypeScript: ```typescript const response = await fetch("https://api.paxalabs.com/v1/translate", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ text: "Welcome to our store. Free shipping on orders over 500 baht.", model: "paxa-translation-lite-v1", formality: "formal", }), // A large request can run for minutes; give it room. signal: AbortSignal.timeout(300_000), }); if (!response.ok) throw new Error(`Translation failed: ${response.status}`); const { translations, usage } = await response.json(); console.log(translations[0].text, `${usage.credits} credits`); ``` Python: ```python import os import requests response = requests.post( "https://api.paxalabs.com/v1/translate", headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, json={ # Send the whole glossary. Terms that do not appear in the text # are dropped before the request runs, and never billed. "glossary": [ {"source": "free shipping", "target": "ส่งฟรี"}, {"source": "cash on delivery", "target": "เก็บเงินปลายทาง"}, ], "text": "Welcome to our store. Free shipping on orders over 500 baht.", "model": "paxa-translation-lite-v1", }, # A large request can run for minutes; requests defaults to waiting # forever, but most deployments put a shorter limit in front of it. timeout=300, ) response.raise_for_status() body = response.json() print(body["translations"][0]["text"]) print(body["usage"]["glossary_terms_sent"], "of", body["usage"]["glossary_terms_available"]) ``` Response: ```json { "translations": [ { "text": "ยินดีต้อนรับสู่ร้านของเรา ส่งฟรีเมื่อสั่งซื้อครบ 500 บาท", "detected_source": "en" } ], "usage": { "text_chars": 60, "reference_chars": 0, "billable_chars": 60, "credits": 2, "glossary_terms_sent": 0, "glossary_terms_available": 0 } } ``` ## Billing Translation bills at two rates. Characters you send in `text` are translated, and cost 25 credits per 1,000. Characters in `instructions`, `context`, `examples`, `do_not_translate`, and `glossary` are read but never translated, and cost 8 credits per 1,000, counting both sides of every pair. The two are summed and rounded up once to the next hundredth of a credit, and every request costs at least 2 credits. Reference material costs less to serve. It becomes input the model reads. Translated characters drive everything the model writes as well. Send the context a passage needs. Every response reports the exact split in `usage`. A charge is always reconcilable against the body you sent. Two ceilings apply. The `text` field accepts at most 20,000 characters, because it is the only field that produces output. That allowance is counted once per requested rendering, since each rendering produces that much Thai. Longer text answers [400 text_too_long](https://paxalabs.com/docs/errors#text_too_long). The request as a whole accepts at most 200,000 billable characters across every field, and beyond that answers [400 request_too_large](https://paxalabs.com/docs/errors#request_too_large). Neither charges anything. Both ceilings are measured after unused glossary terms are dropped. Attaching a large glossary never pushes a request over one. ## Batch segments An array of up to 200 segments translates in one request. The segments share document context, which keeps pronouns, register, and terminology consistent across them. The per-request minimum applies once for the whole array. Segment boundaries come back exactly as sent. Batch UI strings, subtitles, and product feeds. Batching also keeps `context` and `glossary` cheap. Both are sent once for the whole request. The same context costs the same on one segment as on 200. Spread across a full batch it is a fraction of a percent of the bill. Batching: ```typescript // One request, one charge: segments share document context and the // per-request minimum applies once. const strings = ["Sign in", "Create account", "Forgot password?"]; const response = await fetch("https://api.paxalabs.com/v1/translate", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ text: strings, model: "paxa-translation-lite-v1" }), }); const { translations } = await response.json(); for (const [index, item] of translations.entries()) { console.log(`${strings[index]} -> ${item.text}`); } ``` ## Keyed segments A segment can be an object as well as a string. Give it an `id` and the result carries that id back. Your code then matches results to inputs by key, with no dependence on position. Ids are never interpreted and need not be unique. Order is preserved either way, and one array may mix objects and plain strings. A segment object also takes `max_length`, the longest acceptable translation in characters. The model compresses the translation to fit and does not truncate it. A value below what the meaning needs therefore costs fidelity. A result that still exceeds the limit is delivered and flagged. Use it for subtitles and UI strings, where the space is fixed. TypeScript: ```typescript // Key the segments and you match results by id, not by position. // max_length compresses to fit; a result that still exceeds it comes // back delivered and flagged rather than refused. const response = await fetch("https://api.paxalabs.com/v1/translate", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "paxa-translation-lite-v1", text: [ { id: "cta.save", text: "Save changes", max_length: 24 }, { id: "cta.cancel", text: "Cancel", max_length: 12 }, "A plain string works alongside them.", ], do_not_translate: ["Paxa TTS Flash"], }), signal: AbortSignal.timeout(300_000), }); const { translations } = await response.json(); for (const segment of translations) { if (segment.review) { console.warn(segment.id, segment.review.reason, segment.review.detail); } console.log(segment.id ?? "(unkeyed)", segment.text); } ``` ## Markdown and HTML Set `format` to `"markdown"` or `"html"` and the model translates only the human-readable prose. Link targets, code, tags, and attributes come back untouched. Thai is written without spaces between words. A translator that shifts an inline tag by one character therefore moves where a word breaks for a Thai reader. That is the reason these modes exist. ## Review flags Every delivered segment is checked against what the request asked for: nothing empty, nothing left untranslated, the terminology and preserved strings present, `max_length` respected, the markup structurally intact. A segment that fails a check carries a `review` object naming the reason. A flag never refuses a request. The translation is delivered and charged either way, and a flag means the segment is worth a look. Checks that could reject a delivery would be a new way to lose a request you already paid for. A response whose segments carry no `review` member passed every check. ## Request options | Field | Values | Effect | | --- | --- | --- | | source | auto (default), en, zh, ja, ko, ar, fr, de, hi, id, ms, pt, ru, es, vi | Pin the source language, or let the model detect it per segment. Auto-detection accepts any language, including mixed-language text. | | formality | auto, formal, casual | Polite written Thai with polite particles where natural, conversational Thai, or Thai matching the source's tone. | | borrowed_words | auto, transliterate, preserve | The established Thai form where one exists, Thai script throughout, or the original script for technical terms and names. | | format | text, markdown, html | What the segments are written in. The markup modes preserve every construct exactly and translate only prose. | | alternatives | 1 to 5 | How many renderings to return per segment, best first. Each one is a full pass over the text and bills as one. | | do_not_translate | up to 100 strings | Strings that must appear in the translation exactly as written. Billed at the reference rate. | | instructions | up to 4,000 chars | Free-form direction that the fixed options cannot express, such as house rules, an audience, or a term to avoid. Applied to every segment. Billed at the reference rate. | | context | up to 100,000 chars | Background the model reads but never translates: the surrounding document, domain, or audience. Billed at the reference rate. | | examples | up to 100 pairs | Source and target pairs whose tone and phrasing the translation imitates. Billed at the reference rate. | | glossary | up to 5,000 pairs | Terminology to enforce wherever a source term appears. Terms that cannot fire on this text are dropped before the request runs. Dropped terms are neither sent nor billed. Billed at the reference rate. | | glossary_mode | matched, all | Whether to drop glossary terms that do not appear in the text. "matched" is the default; "all" sends and bills every term you attach. | ### Instructions `formality` and `borrowed_words` cover the common cases. `instructions` covers the rest. Write plain direction, in English or Thai. It applies to every segment of the request. Say who the reader is, which house rules apply, which words to avoid, or how a recurring phrase should land. Instructions govern style. They cannot change the shape of the response. They cannot make the model act on the text it is translating either. A segment that reads like an order is still translated as text. Use `context` for background the model should know. Use `instructions` for what it should do. curl: ```bash curl -X POST https://api.paxalabs.com/v1/translate \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Your table is ready. Please follow me.", "model": "paxa-translation-lite-v1", "instructions": "Hotel front desk speaking to a guest. Warm, never stiff. Keep room and restaurant names in English." }' ``` ### Glossary Glossary terms are applied contextually. The model inflects, cases, and places each term grammatically, and matches it even when the source text uses a variant form. Literal substitution is often ungrammatical in Thai, which is why glossaries work this way. If a term must appear verbatim, for example a product name or a legal phrase, state that in `context`. Send your whole glossary. Terms whose source does not appear in the text cannot change the translation. Those terms are dropped before the request runs. They are never sent to the model and never billed. A 500-term style guide attached to a short support reply costs what its two or three firing terms cost. The response reports `glossary_terms_sent` and `glossary_terms_available`. Both figures show how a request landed. Matching ignores case, accents, and typographic apostrophes. A glossary typed as `customer's` matches a document typeset with `customer’s`, and `hotel` matches `hôtel`. Matching also sees through suffixes and German compounds: `invoice` fires on `invoices`, and `Rechnung` fires on `Rechnungsnummer`. It needs no word boundaries. It works the same in Chinese, Japanese, and Thai. Matching cannot see through inflection that changes the stem of a word. An irregular plural and a Russian case ending are both examples. Set `always: true` on such a term to send it regardless. Use the same flag for a term that governs a concept, such as an abbreviation the text spells out. Setting `glossary_mode` to `"all"` sends and bills every term you attach. ### Register Thai grammar encodes politeness. Particle choice, pronouns, and vocabulary all shift with register. `formality: "formal"` produces polite written Thai suited to announcements, support replies, and official copy. `"casual"` produces the conversational Thai of chat and social captions. `"auto"`, the default, mirrors the source tone. Rendered register pairs and full translated specimens are on the [translation page](https://paxalabs.com/translation). ### Borrowed words Thai absorbs foreign terms as established loanwords (computer is คอมพิวเตอร์, not a coined Thai word). `"auto"` uses the established form where one exists and keeps unestablished terms in their original script; `"transliterate"` renders borrowed words in Thai script throughout; `"preserve"` keeps technical terms and proper nouns exactly as written, useful for developer documentation and brand copy. curl: ```bash curl -X POST https://api.paxalabs.com/v1/translate \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Welcome to our store. Free shipping on orders over 500 baht.", "model": "paxa-translation-lite-v1", "source": "en", "formality": "formal", "borrowed_words": "auto", "context": "Homepage banner for an online fashion store.", "glossary": [{"source": "free shipping", "target": "ส่งฟรี"}] }' ``` ## Source language Each response entry reports `detected_source`, the ISO 639-1 code of that segment's input (or "und" when inconclusive). Mixed-language input is handled per segment, and text that is already Thai passes through unchanged apart from register adjustments; it bills normally. ## Latency and retries Translation is a reasoning workload. Expect a response in seconds, and up to tens of seconds near the character ceiling. Set your HTTP client's timeout to at least 300 seconds. Most clients and SDKs default to far less than a large request needs. A client-side timeout aborts a request that was already charged. Send an [Idempotency-Key](https://paxalabs.com/docs/idempotency) to make retries safe. The model is not deterministic. A replayed request can word its translation differently from the original delivery, and the charge is never repeated either way. A request that runs long enough writes its response headers before the outcome is known. Intermediate proxies abandon a connection that has been silent too long, and the early headers prevent that. Such a response always has status 200. It reports a failure in the body, as the same problem document a failed request returns: a `title` and a `status` where `translations` would be. If your client sends requests near the character ceiling, treat a body carrying `title` as the error it describes. Shorter requests always carry their status on the status line. > Content declined by the upstream safety system answers [422 content_blocked](https://paxalabs.com/docs/errors#content_blocked) with the charge refunded. Retrying the same content fails the same way; revise the input. - [Translate text](https://paxalabs.com/docs/api/translate): Full request and response reference - [Credits, charges, and refunds](https://paxalabs.com/docs/credits): How credits and refunds work - [Make retries safe with idempotency keys](https://paxalabs.com/docs/idempotency): Safe retries with Idempotency-Key - [Error codes and responses](https://paxalabs.com/docs/errors): Every error code # Credits, charges, and refunds Credits are the unit every Paxa API request is billed in, at one base rate on every plan. Credits are the unit every request is billed in. Each model charges its own rate, the same on every plan and every refill. A new account starts with 100 free credits, which cover personal use and evaluation. Any paid plan or refill includes commercial use of your output. ## Rates | Product | Credits | Price | Minimum | | --- | --- | --- | --- | | Speech | 10 per 1,000 characters | $10.00 per million characters | 0.1 credit | | Translation (translated text) | 25 per 1,000 characters | $25.00 per million characters | 2 credits | | Translation (reference text) | 8 per 1,000 characters | $8.00 per million characters | | | OCR | 6.5 per page | $6.50 per 1,000 pages | 1 page | | Extraction (up to 50 leaf fields) | 13 per page | $13.00 per 1,000 pages | 1 page | | Extraction (51 to 100 leaf fields) | 19.5 per page | $19.50 per 1,000 pages | 1 page | | Transcription | 500 per hour | $0.50 per hour | 0.1 credit | A character is one UTF-16 code unit, and the characters of an [emotion tag](https://paxalabs.com/docs/emotion-tags) or a [direction tag](https://paxalabs.com/docs/directions) count like any other. A page is one PDF page or one image file, which [OCR](https://paxalabs.com/docs/ocr#billing) and [extraction](https://paxalabs.com/docs/extraction#billing) both count from the document before the charge, and a schema's leaf count picks the extraction band. [Transcription](https://paxalabs.com/docs/speech-to-text#billing) bills the recording's exact length, 8.33 credits per minute, read from the file before the charge. Every translation response reports the [split](https://paxalabs.com/docs/translation#billing) between translated and reference characters. ## How charging works The API charges a request before the model runs. If the model then fails, the API refunds the charge and answers [502 provider_error](https://paxalabs.com/docs/errors#provider_error). Speech text with nothing to voice, such as emoji alone, answers [422 unspeakable_text](https://paxalabs.com/docs/errors#unspeakable_text) and is refunded the same way. A product that is unavailable answers [503 provider_unavailable](https://paxalabs.com/docs/errors#provider_unavailable) before anything is charged. Charges round up to the next hundredth of a credit. Requests that ask for [timestamps](https://paxalabs.com/docs/timestamps) bill at 1.25 times the speech rate, and a 57-character speech request with timestamps costs 0.72 credits. Speech in a [designed voice](https://paxalabs.com/docs/designed-voices) bills at 1.25 times the speech rate, or 1.5 times with timestamps. Each translation [alternative](https://paxalabs.com/docs/translation#options) bills as one more pass over the text. No other speech, transcription, OCR, or extraction option adds to a charge. Audio responses and the OpenAI-compatible transcription route carry the charge in the `x-credits-charged` header. Transcription on `/v1/stt`, OCR, extraction, and translation report it in the body's `usage.credits` object and send no header. An [idempotent replay](https://paxalabs.com/docs/idempotency) reports the original charge, because nothing new is billed. A [live connection](https://paxalabs.com/docs/live-streaming) bills cumulatively, applies the 0.1 credit minimum once per connection, and reports each charge as a receipt on the socket. ## Credit buckets An account holds two balances. Plan credits arrive with a subscription each billing period, reset when the period turns over, and are spent first. Wallet credits come from refills, the free grant, and referral rewards, and they never expire. A request draws from the plan bucket first and takes the difference from the wallet. ## When the balance runs out When the balance cannot cover a request, the API answers [402 insufficient_credits](https://paxalabs.com/docs/errors#insufficient_credits) and charges nothing. There is no overage billing. Buy credits on the dashboard's billing page, where $10 adds 10,000 wallet credits and plans are subscribed, switched, and cancelled. The [pricing page](https://paxalabs.com/pricing) lists both. The same page offers auto top-up, which is off until you turn it on. Your saved card then buys the credit amount you chose whenever the balance falls below your threshold, at most once every 10 minutes. ## Check your balance [GET /v1/me](https://paxalabs.com/docs/api/me) reports the live balance of both buckets, the presented key's spend, and the effective plan. Poll it when reconciling usage. curl: ```bash curl https://api.paxalabs.com/v1/me \ -H "Authorization: Bearer $PAXA_API_KEY" ``` Response: ```json { "key": { "id": "6b9f1a52-1b6e-4c9d-9f34-58a4c2f9d3aa", "name": "production", "key_start": "pxa_a3f2k9", "credit_limit": null, "credits_spent": 1240.39, "created_at": "2026-07-02T09:14:07.412Z", "last_used_at": "2026-08-03T05:41:52.108Z" }, "balance": { "wallet": 8200, "plan": 14759.61, "total": 22959.61 }, "plan": { "key": "standard", "rpm": 200, "concurrency": 15, "monthly_credits": 25200 } } ``` # Rate limits and concurrency Two account-level limits set by plan: requests per minute, shared across every product, and concurrent requests, counted per product. How each limit counts, how to read the x-ratelimit headers, and how to retry a 429. Two limits shape traffic, and the plan sets both. Requests per minute are counted once for the whole account: one window covers every product and every key. Concurrent requests are counted per product: speech, transcription, OCR, and translation each get their own pool of the plan's size, and [Document Extraction](https://paxalabs.com/docs/extraction) draws on the OCR pool. The catalog, [account](https://paxalabs.com/docs/api/me), and [health](https://paxalabs.com/docs/api/health) endpoints are never limited, and [feedback](https://paxalabs.com/docs/api/feedback) has daily caps of its own. Each plan's numbers are on the [pricing page](https://paxalabs.com/pricing). Today they are: | Plan | Requests per minute | Concurrent requests | | --- | --- | --- | | Free | 10 | 1 | | Starter | 60 | 5 | | Standard | 200 | 15 | | Pro | No limit | 60 | Concurrent requests apply per product: the Standard plan can hold 15 speech requests, 15 transcription requests, 15 OCR requests, and 15 translation requests in flight at the same time. [GET /v1/me](https://paxalabs.com/docs/api/me) reports both numbers for the authenticated account. ## One request window, separate concurrency pools The two limits count differently, on purpose: - Requests per minute are shared. One window covers the whole account: every product, every key, and every [live synthesis leg](https://paxalabs.com/docs/live-streaming) together. - Concurrent requests are separate. Speech, transcription, OCR, and translation each get their own pool of the plan's size, and document extraction requests count in the OCR pool. A slow translation batch never blocks speech, document reading, or transcription, and the reverse. - An open [live connection](https://paxalabs.com/docs/api/live) holds one speech slot for as long as it stays connected, whether or not it is synthesizing right now. ## The rate limit window The limit is a sliding 60-second window. The previous minute still counts by its unelapsed fraction. A burst cannot double up at a minute boundary, and capacity frees up continuously. ## Read the headers On plans with a limit, every synthesis response reports the budget: `x-ratelimit-limit`, `x-ratelimit-remaining`, and `x-ratelimit-reset` (Unix time in seconds). Plans without a limit send no rate limit headers. Response headers: ```http HTTP/1.1 200 OK Content-Type: audio/mpeg x-request-id: 7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a x-credits-charged: 0.86 x-ratelimit-limit: 60 x-ratelimit-remaining: 42 x-ratelimit-reset: 1785762360 ``` ## Concurrent requests A slot is held by one in-flight request. An HTTP synthesis holds it from admission until its response finishes, a translation or document reading until its JSON returns, and a live connection from upgrade to close. When the account holds every slot for a product, new requests to that product answer [429 concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited) and nothing is charged. A `concurrency_limited` response carries no `retry-after` header, because the wait ends when one of your own requests finishes. Retry after one completes, or close a live connection you no longer need. Note the `x-ratelimit-remaining` value in the sample. The request window still has room, and the 429 is about in-flight slots. 429 response: ```http HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json x-ratelimit-limit: 60 x-ratelimit-remaining: 41 x-ratelimit-reset: 1785762420 {"title": "concurrency_limited", "status": 429} ``` ## Stay under the limits Both limits count requests, and the size of each request does not matter. Batch work into fewer calls. One [translation request](https://paxalabs.com/docs/api/translate) carrying an array of segments counts once against the window and holds one slot. One TTS request carrying a paragraph counts once, where per-sentence calls count once each. Keys share the account's limits. Adding keys does not add capacity. Pace retries with `retry-after`. A [live WebSocket](https://paxalabs.com/docs/api/live) upgrade can be refused with 429 like any synthesis request, under either limit. ## Handle 429 responses Two codes answer 429, and `title` tells them apart. [rate_limited](https://paxalabs.com/docs/errors#rate_limited) means the minute window is exhausted. Honor the `retry-after` header. [concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited) means every slot for the product is held. Retry once one of your requests finishes. A simple wait-and-retry wrapper covers both for most workloads. 429 response: ```http HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json retry-after: 12 x-ratelimit-limit: 60 x-ratelimit-remaining: 0 x-ratelimit-reset: 1785762420 {"title": "rate_limited", "status": 429} ``` TypeScript: ```typescript async function withRetry(request: () => Promise): Promise { for (;;) { const response = await request(); if (response.status !== 429) return response; // rate_limited sends retry-after. concurrency_limited does not, // and a short wait covers a slot freeing up. const seconds = Number(response.headers.get("retry-after") ?? "1"); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } } ``` # Make retries safe with idempotency keys An Idempotency-Key header makes retries safe: the same key with the identical payload charges once and serves the same result. Replays, conflicts, and how to choose keys. Idempotency makes a retry behave like the first attempt. Send an `Idempotency-Key` header, and the identical request charges once however many times you send it. A key is any string of 1 to 200 printable ASCII characters. Keys are namespaced to your API key and never collide with another account's. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-8146-confirmation" \ -d '{"text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2"}' \ --output speech.mp3 ``` TypeScript: ```typescript // Reuse the same key when retrying the identical request: the charge // happens once and the same audio is served again. const idempotencyKey = `order-${order.id}-confirmation`; const response = await fetch("https://api.paxalabs.com/v1/tts", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify({ text: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", voice: "khanomkrok", model: "paxa-tts-flash-v2" }), }); ``` ## Supported endpoints Seven billed HTTP endpoints accept the header: [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech), the [OpenAI-compatible speech endpoint](https://paxalabs.com/docs/api/speech), [POST /v1/translate](https://paxalabs.com/docs/api/translate), [POST /v1/ocr](https://paxalabs.com/docs/api/ocr), [POST /v1/extract](https://paxalabs.com/docs/api/extract), [POST /v1/stt](https://paxalabs.com/docs/api/stt), and the [OpenAI-compatible transcription endpoint](https://paxalabs.com/docs/api/transcriptions). The [live WebSocket](https://paxalabs.com/docs/api/live) does not accept it. Its billing is cumulative per connection, and reconnecting opens a new billing scope. The main use is [streaming](https://paxalabs.com/docs/streaming). A dropped stream retried under the same key replays without a second charge. ## Replays The first request under a key records the charge. A retry with the same key and the identical payload is verified against that record and served without charging again. Its `x-credits-charged` header, or the body's `usage.credits` for transcription, OCR, extraction, and translation, reports the original charge. One request per key runs at a time, and concurrent duplicates answer [409 idempotency_in_flight](https://paxalabs.com/docs/errors#idempotency_in_flight). ## Conflicts | Response | When | What to do | | --- | --- | --- | | [409 idempotency_in_flight](https://paxalabs.com/docs/errors#idempotency_in_flight) | Another request with this key is running right now. | Wait for it to finish, then retry to replay its outcome. | | [409 idempotency_refunded](https://paxalabs.com/docs/errors#idempotency_refunded) | The original request failed and was refunded. | Send a new Idempotency-Key. | | [422 idempotency_mismatch](https://paxalabs.com/docs/errors#idempotency_mismatch) | The key was already used for a different payload. | Reuse a key only to retry the identical request. | ## Choose a key Derive keys from identifiers you already have, for example `order-8146-confirmation`. One key means one piece of audio: a new attempt at different content needs a new key. > Without the header, every request charges exactly once under its own internal id, tied to the `x-request-id` response header for support. # Error codes and responses Every Paxa API failure is an RFC 9457 problem+json body whose title field is a stable machine-readable code. Every failure is an RFC 9457 problem details response with the `application/problem+json` content type, at the minimum shape. Two fields describe the failure: - `title` carries the stable code to branch on. - `status` mirrors the HTTP status. Two codes add two fields that name the field at fault. A [schema_invalid](https://paxalabs.com/docs/errors#schema_invalid) or a [validation](https://paxalabs.com/docs/errors#validation) refusal of one field carries `path`, the field, and `reason`, a stable code. A validation `path` starts with where the field was sent, as in `body.voice` or `headers.idempotency-key`, and its reasons are these: | reason | Meaning | | --- | --- | | missing | A required field is absent. | | wrong_type | The field holds the wrong JSON type, such as a string where a number goes. | | too_short | A string or list is shorter than the field's minimum. | | too_long | A string or list is longer than the field's maximum. | | too_small | A number is below the field's minimum. | | too_large | A number is above the field's maximum. | | not_allowed | The value is not one of the values the field accepts. | | malformed | A string does not match the pattern or format the field requires. | | unparsable | The body is not valid JSON or form data. No field could be read. | | invalid | The field breaks another rule of the endpoint schema. | Responses carry no prose. This page is the reference for every code's meaning and the recommended handling. Error body: ```json {"title": "insufficient_credits", "status": 402} ``` ## Handle an error Branch on `title`. Two codes can share a status, and the status alone does not identify a failure: both [rate_limited](https://paxalabs.com/docs/errors#rate_limited) and [concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited) answer 429. Retry `rate_limited` after `retry-after`, retry `concurrency_limited` when one of your requests finishes, refill on [402](https://paxalabs.com/docs/errors#insufficient_credits), and treat [502](https://paxalabs.com/docs/errors#provider_error) as refunded. TypeScript: ```typescript const response = await fetch("https://api.paxalabs.com/v1/tts", options); if (!response.ok) { const problem = await response.json(); // Branch on title: it is the stable code the errors reference documents. switch (problem.title) { case "rate_limited": { const seconds = Number(response.headers.get("retry-after") ?? "1"); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); break; } case "insufficient_credits": notifyBilling(); break; default: throw new Error(problem.title); } } ``` Python: ```python import time response = requests.post(f"https://api.paxalabs.com/v1/tts", headers=headers, json=body) if not response.ok: problem = response.json() # Branch on title: it is the stable code the errors reference documents. if problem["title"] == "rate_limited": time.sleep(int(response.headers.get("retry-after", "1"))) elif problem["title"] == "insufficient_credits": notify_billing() else: raise RuntimeError(problem["title"]) ``` > Validation runs before authentication, which means a malformed body sent with a bad key answers `400 validation`. ## Retry policy Four codes are retryable. Retry [429 rate_limited](https://paxalabs.com/docs/errors#rate_limited) after the `retry-after` header. Retry [429 concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited) once one of the account's in-flight requests on that product finishes. That code carries no `retry-after`. Retry [502 provider_error](https://paxalabs.com/docs/errors#provider_error) with a new [Idempotency-Key](https://paxalabs.com/docs/idempotency); the failed charge is already refunded. Retry [503 provider_unavailable](https://paxalabs.com/docs/errors#provider_unavailable) once [GET /health](https://paxalabs.com/docs/api/health) reports the model served again. Every other code is terminal for that request. A 400-class code fails the same way until the request changes, [402 insufficient_credits](https://paxalabs.com/docs/errors#insufficient_credits) fails until the balance changes, and [429 feedback_limit](https://paxalabs.com/docs/errors#feedback_limit) fails until older reports leave the 24-hour window. Every authenticated product request answers with an `x-request-id` header, failures included. Log it alongside the failure. The code and the id together make a complete incident report, which [POST /v1/feedback](https://paxalabs.com/docs/api/feedback) takes with `kind` set to `api`. ## Error reference | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `validation` | The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. | Fix the field at path as the reason says and retry. Compare the request against the endpoint's schema in this reference when the body names no field. | | 400 | `unknown_model` | The model field does not name a served model. | GET /v1/models lists the catalog of served model ids. | | 400 | `unknown_voice` | The voice id names no voice on the model's roster and no designed voice this account holds. | GET /v1/voices lists the id of every catalog voice, and GET /v1/voices/designed lists the account's designed voices. A designed voice id (voice_...) speaks only on Paxa TTS Flash v2 for the account that saved it, and stops once deleted. | | 400 | `draft_invalid` | The draft field holds no draft this account received from POST /v1/voices/design. It is altered or cut short, belongs to another account, or no longer opens after a platform update. Nothing was charged. | Send the draft string exactly as the design response returned it, with a key of the same account. A draft that no longer opens cannot be recovered. Design the voice again. | | 400 | `voice_name_taken` | The name is a catalog voice's name, which no designed voice can take. Nothing was saved. | Choose another name. GET /v1/voices lists the catalog's names. | | 400 | `text_too_long` | The text exceeds the model's per-request character ceiling. | Split the text into shorter requests. GET /v1/models reports each model's ceiling as max_chars. | | 400 | `tag_invalid` | The text holds a tag the model refuses. On Paxa TTS Flash v2, the bracket asks for what the model does not do, such as an accent, singing, or a sound outside the supported list. It can also hold no letters or carry a weight that is not a number. On a Paxa TTS Flash voice that takes emotion tags, the bracketed word is outside the tag list, or its weight is not a number. Nothing was charged. | On Paxa TTS Flash v2, the Direction tags guide lists every tag the model refuses. On Paxa TTS Flash, use a word from the tag list in the Emotion tags guide. Its weight is a number from 0 to 1 or a percentage, as in [sad:0.5], and a value past either end reads as that end. | | 400 | `request_too_large` | The request's billable characters exceed the model's total ceiling. Translation bills every field: instructions, context, examples, do_not_translate strings, and the glossary count toward the ceiling alongside the text. | Shorten the request or move terms out of the glossary. GET /v1/models reports the total ceiling as max_request_chars and the text-only ceiling as max_chars. | | 400 | `document_invalid` | The document field could not be read as a PDF, PNG, JPEG, or WebP file. A damaged or truncated PDF answers this code. Nothing was charged. | Base64-encode the raw file bytes and send them in the document field. GET /v1/models reports the accepted formats. | | 400 | `document_password_required` | The PDF needs a password to open. A PDF that carries permissions-only encryption, the kind that opens without being asked for a password, is read normally. Nothing was charged. | Send a copy that opens without a password. | | 400 | `too_many_pages` | The document has more pages than the model's per-request ceiling. Nothing was charged. | Split the PDF into shorter documents and send each as its own request. GET /v1/models reports the ceiling as max_pages. | | 400 | `schema_invalid` | The extraction schema is outside the dialect. The problem body carries two extra fields: path, the offending field in dot form (empty for the schema as a whole), and reason, one stable code from the schema reasons list. Nothing was charged. | Fix the field at path as the reason says and retry. The Document Extraction reference lists every reason and the dialect it checks against. | | 400 | `audio_invalid` | The audio field could not be read as an MP3, WAV, FLAC, Ogg, M4A, AAC, or WebM recording, or its length could not be read from the container. A damaged or truncated file answers this code. Nothing was charged. | Base64-encode the raw file bytes and send them in the audio field. Re-export a file that fails, since a container without a readable length cannot be billed. GET /v1/models reports the accepted formats. | | 400 | `audio_too_long` | The recording is longer than the model's per-request ceiling. Nothing was charged. | Split the recording at a pause and send each part as its own request. GET /v1/models reports the ceiling as max_duration_seconds. | | 400 | `diarization_audio_too_long` | The recording reaches the length at which speaker attribution stops, which is well below the transcription ceiling. Nothing was charged. | Send the recording without diarization, or split it at a pause and diarize each part. GET /v1/models reports the ceiling as diarization_limit_seconds. | | 401 | `unauthorized` | The request carried no API key, or the key is invalid or disabled. | Send a key from the dashboard as 'Authorization: Bearer pxa_...' or in the x-api-key header. | | 402 | `insufficient_credits` | The account does not have enough credits for this request. Nothing was charged. | Top up or upgrade in the dashboard, then retry. | | 403 | `key_limit` | This API key reached its spending cap. Nothing was charged. | Raise or remove the cap in the dashboard, or send the request with another key. | | 403 | `voice_slots_full` | The account holds as many designed voices as its plan allows. Nothing was saved. | Delete a voice with DELETE /v1/voices/designed/{id}, or upgrade the plan for more slots. GET /v1/voices/designed reports the slots in use and the plan's limit. | | 404 | `not_found` | No endpoint exists at the requested method and path. | Check the path against the API reference at https://paxalabs.com/docs. | | 413 | `document_too_large` | The decoded document exceeds the model's size ceiling. Nothing was charged. | Compress the document or split it, then retry. GET /v1/models reports the ceiling in bytes as max_bytes. | | 413 | `audio_too_large` | The decoded recording exceeds the model's size ceiling. Nothing was charged. | Re-encode at a lower bitrate, as Opus or MP3, or split the recording, then retry. GET /v1/models reports the ceiling in bytes as max_bytes. | | 409 | `idempotency_in_flight` | Another request with this Idempotency-Key is in flight right now. | Wait for it to finish, then retry to replay its outcome. | | 409 | `idempotency_refunded` | The original request under this Idempotency-Key failed and was refunded. | Send the request again with a new Idempotency-Key. | | 422 | `idempotency_mismatch` | This Idempotency-Key was already used for a different request. | Reuse a key only to retry an identical request. New content needs a new key. | | 400 | `unsupported_language` | The source field names a language the model does not accept as an explicit pin. | GET /v1/models lists each translation model's accepted source codes. Send "auto" to translate from any language with automatic detection. | | 400 | `unsupported_format` | The model does not serve this format, or this format at the requested sample rate. | Paxa TTS Flash v2 serves pcm and wav at every listed sample rate, and mulaw and alaw at 8000 Hz. Paxa TTS Flash serves pcm and wav at 24000 Hz only, and no mulaw or alaw. mp3 and opus take only their own rate on both. Omit sample_rate to take the format's own rate. | | 422 | `content_blocked` | The upstream safety system declined to process this content. The charge was refunded. | Revise the input. Retrying the same content fails again. If the block looks wrong, contact support with the x-request-id. | | 422 | `unspeakable_text` | The text contains nothing the model can voice: only tags, emoji, punctuation, or a script it does not speak. A text of tags alone is refused before the charge; any other is charged and refunded. | Send text with Thai, English, or Mandarin Chinese words in it. Retrying the same text fails again. | | 400 | `live_protocol` | A frame on a live WebSocket violated the session protocol. The frame arrived before start, repeated start, or followed end. | Open the session with one start frame, send the session's frames while it is active, and finish with end. The connection closes with code 4400 after this error. | | 429 | `rate_limited` | Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. | Honor the retry-after header, or upgrade the plan for a higher limit. | | 429 | `concurrency_limited` | The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. | Wait for one of the account's in-flight requests on this product to finish, then retry. There is no retry-after header because the wait depends on your own requests. Upgrade the plan for more concurrency. | | 429 | `feedback_limit` | The account reached a daily feedback cap: 50 written reports, or 1,000 reports that carry only a rating, in the last 24 hours. Nothing was stored. | Send the report later, once older reports leave the 24-hour window. Before sending one again, GET /v1/feedback with request_id shows whether it already arrived. | | 500 | `internal` | Request state was inconsistent on the server. | Retry with a new Idempotency-Key. If it persists, POST /v1/feedback with the x-request-id. | | 502 | `provider_error` | Model inference failed after the request was charged. | A fresh request is refunded automatically; a replayed Idempotency-Key keeps its original charge and is never recharged. Retry with a new Idempotency-Key. If it persists, POST /v1/feedback with the x-request-id. | | 503 | `provider_unavailable` | The model behind this endpoint is not available right now. Nothing was charged. | Retry later; GET /health reports per-model availability. | # Use the OpenAI SDK with Paxa POST /v1/audio/speech and POST /v1/audio/transcriptions are drop-in replacements for the OpenAI speech and transcription endpoints: point the SDK's base URL at the Paxa API and existing calls work unchanged. The Paxa API serves the OpenAI audio.speech request shape at [POST /v1/audio/speech](https://paxalabs.com/docs/api/speech). Set the SDK's base URL to `https://api.paxalabs.com/v1` and authenticate with a Paxa API key. Existing `audio.speech.create` calls then work unchanged. Pricing, headers, and error codes match [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech). The [transcription endpoint](#transcription) is served the same way. Python: ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.paxalabs.com/v1", api_key=os.environ["PAXA_API_KEY"], ) with client.audio.speech.with_streaming_response.create( model="paxa-tts-flash-v2", voice="khanomkrok", input="สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", ) as response: response.stream_to_file("speech.mp3") ``` TypeScript: ```typescript import { writeFile } from "node:fs/promises"; import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.paxalabs.com/v1", apiKey: process.env.PAXA_API_KEY, }); const response = await client.audio.speech.create({ model: "paxa-tts-flash-v2", voice: "khanomkrok", input: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", }); await writeFile("speech.mp3", Buffer.from(await response.arrayBuffer())); ``` curl: ```bash curl -X POST https://api.paxalabs.com/v1/audio/speech \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "paxa-tts-flash-v2", "input": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok" }' \ --output speech.mp3 ``` ## Field mapping | Field | Behavior | | --- | --- | | model | Required, same as on /v1/tts. A Paxa model id, for example `paxa-tts-flash-v2`. OpenAI's `tts-1`, `tts-1-hd`, `gpt-4o-mini-tts`, `gpt-4o-mini-tts-2025-03-20`, `gpt-4o-mini-tts-2025-12-15` also work and run on `paxa-tts-flash-v2`. | | input | The text to synthesize. Same limits, pricing, [direction tags](https://paxalabs.com/docs/directions), and [emotion tags](https://paxalabs.com/docs/emotion-tags) on `paxa-tts-flash-v1` as `text` on /v1/tts. | | voice | The voice id from [GET /v1/voices](https://paxalabs.com/docs/api/voices), for example `"khanomkrok"`; case is ignored, as for `voice` on /v1/tts. This endpoint also takes OpenAI's voice names, such as `alloy`, mapped to Paxa voices, and the `x-voice-id` response header names the voice that spoke. | | response_format | `mp3` (the default), `opus`, `wav`, `pcm` (16-bit little-endian mono at 24 kHz, as OpenAI's, unless `sample_rate` names another rate), `mulaw`, or `alaw`, as `format` on /v1/tts. Paxa TTS Flash answers `mulaw` and `alaw` with unsupported_format, and other OpenAI formats answer 400. | | sample_rate | A Paxa extension, read for `pcm` and `wav` as on /v1/tts and ignored with other formats, though any value must be one of Paxa TTS Flash v2's rates. It sets their rate in Hz, where Paxa TTS Flash v2 serves 8000, 16000, 22050, 24000, 32000, 44100, 48000 and Paxa TTS Flash only 24000. Set it when the platform playing the audio expects a fixed rate. | | temperature | A Paxa extension, read as on /v1/tts: how much the reading varies from one request to the next, from 0 to 2 on Paxa TTS Flash v2. [Temperature](https://paxalabs.com/docs/temperature) covers it. | | stream | Defaults to `true`, matching OpenAI delivery. Set `false` for a complete body with a `content-length`. | `speed` is served at this model's range. A value outside that range answers 400. A rate the model cannot reach is never applied silently. Fields the OpenAI API defines but Paxa does not serve, for example `instructions`, are ignored, and existing calls carrying them keep working. The reverse also holds. The [timestamps](https://paxalabs.com/docs/timestamps) extension exists only on `/v1/tts`, and timed speech requires the native endpoint. Apps that fill a voice or model picker from the base URL read [GET /v1/audio/voices](https://paxalabs.com/docs/api/audio-voices) and [GET /v1/audio/models](https://paxalabs.com/docs/api/audio-models). They list Paxa TTS Flash v2 and its voices, and need no key. ## Differences from POST /v1/tts The compatible endpoint covers the plain case: text in, audio out, streamed by default. Everything beyond it lives on the native endpoint: [`timestamps`](https://paxalabs.com/docs/timestamps) and `language` exist only on `/v1/tts` and `/v1/tts/live`, and the native request shape is what the [API reference](https://paxalabs.com/docs/api/text-to-speech) documents field by field. A `timestamps` or `language` sent here is ignored: the response is plain audio, and every request reads as `auto`. Both endpoints accept an [Idempotency-Key](https://paxalabs.com/docs/idempotency), report the exact charge in `x-credits-charged`, and answer failures from the same [error catalog](https://paxalabs.com/docs/errors). Retry and billing code written against one endpoint works against the other. ## Transcription [POST /v1/audio/transcriptions](https://paxalabs.com/docs/api/transcriptions) serves the OpenAI `audio.transcriptions.create` shape, under the same base URL and the same key. The recording rides as a multipart file part. Nothing is base64 encoded. Python: ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.paxalabs.com/v1", api_key=os.environ["PAXA_API_KEY"], ) with open("meeting.m4a", "rb") as file: subtitles = client.audio.transcriptions.create( file=file, model="paxa-stt-lite-v1-preview", response_format="srt", ) with open("meeting.srt", "w") as out: out.write(subtitles) ``` TypeScript: ```typescript import { createReadStream } from "node:fs"; import { writeFile } from "node:fs/promises"; import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.paxalabs.com/v1", apiKey: process.env.PAXA_API_KEY, }); const subtitles = await client.audio.transcriptions.create({ file: createReadStream("meeting.m4a"), model: "paxa-stt-lite-v1-preview", response_format: "srt", }); await writeFile("meeting.srt", subtitles); ``` curl: ```bash # The recording rides as a file part; nothing is base64 encoded. curl -X POST https://api.paxalabs.com/v1/audio/transcriptions \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -F file=@meeting.m4a \ -F model=paxa-stt-lite-v1-preview \ -F response_format=srt \ -o meeting.srt ``` | Field | Behavior | | --- | --- | | file | The recording as a file part, in any of 7 containers; the format and the length come from the bytes. Same ceilings and pricing as `audio` on /v1/stt. | | model | Required. A Paxa model id, for example `paxa-stt-lite-v1-preview`. OpenAI's `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-03-20`, `gpt-4o-mini-transcribe-2025-12-15` also work and run on `paxa-stt-lite-v1-preview`. | | language | A BCP 47 hint, the same field as on /v1/stt. Omitted, the model detects the language itself. | | response_format | `json` (the default), `verbose_json`, `text`, `srt`, or `vtt`. `verbose_json` adds the duration, the segments, and the words when `timestamp_granularities` names `word`. | The charge rides `x-credits-charged`, since OpenAI's body has nowhere to report it. `prompt` and `temperature` are ignored, and `timestamp_granularities` is read with `verbose_json` alone. `stream` set to `true` answers [400 validation](https://paxalabs.com/docs/errors#validation); [WS /v1/stt/live](https://paxalabs.com/docs/api/stt-live) transcribes as the audio arrives. The two transcript conventions, the two styles, speaker turns, the vocabulary, and the subtitle line length are extensions of [POST /v1/stt](https://paxalabs.com/docs/api/stt) and are not read here. A recording long enough to run past a proxy's idle limit belongs on the native endpoint, whose JSON body is kept alive while it works. ## Switch an existing integration - Point the SDK's base URL at `https://api.paxalabs.com/v1`. - Swap the OpenAI key for an API key from the [dashboard](https://paxalabs.com/app/keys). - Keep OpenAI's model names, which run on `paxa-tts-flash-v2`, or set `model` to `paxa-tts-flash-v2` or `paxa-tts-flash-v1`. - Keep OpenAI's voice names, or set `voice` to an id the chosen model serves, from [GET /v1/voices](https://paxalabs.com/docs/api/voices). A voice off that model's roster answers [400 unknown_voice](https://paxalabs.com/docs/errors#unknown_voice). - Set `response_format` to `mp3`, `opus`, `wav`, or `pcm`, or `mulaw` or `alaw` for phone lines; the other OpenAI containers are not served. > Failures follow this API's [error model](https://paxalabs.com/docs/errors). The SDK raises its usual HTTP error, and the response body carries the stable code in `title`. # Accepted voice names The other voice names POST /v1/audio/speech accepts, and the Paxa voice that speaks each one. Besides Paxa voice ids, POST /v1/audio/speech accepts the names below. The Paxa voice beside each name speaks it, and the response names the voice that spoke in its `x-voice-id` header. | Name | Paxa voice | | --- | --- | | alloy | Foi Thong (`foithong`) | | ash | Khanom Krok (`khanomkrok`) | | ballad | Oliang (`oliang`) | | cedar | Khao Tom (`khaotom`) | | coral | Cookie (`cookie`) | | echo | Massaman (`massaman`) | | fable | Espresso (`espresso`) | | marin | Salim (`salim`) | | nova | Nom Yen (`nomyen`) | | onyx | Thong Ek (`thongek`) | | sage | Bai Toey (`baitoey`) | | shimmer | Tako (`tako`) | | verse | Latte (`latte`) | A name resolves on any model that serves its voice, and `paxa-tts-flash-v2` serves every voice here. A Paxa voice id always takes precedence. A name that is neither a Paxa voice id nor in this table answers [400 unknown_voice](https://paxalabs.com/docs/errors#unknown_voice). [GET /v1/voices](https://paxalabs.com/docs/api/voices) lists every Paxa voice. # Switch from OpenAI text to speech and Whisper OpenAI is removing tts-1 and whisper-1 from its API. Point the OpenAI SDK at Paxa, and your model and voice names keep working. Switching from OpenAI? Change the base URL and the key, and your model and voice names keep working. - `tts-1`, `tts-1-hd`, and `gpt-4o-mini-tts` leave OpenAI's API on January 6, 2027. - `whisper-1`, `gpt-4o-transcribe`, and `gpt-4o-mini-transcribe` leave on February 26, 2027, along with `gpt-4o-transcribe-diarize`. Paxa does not take that name; speaker labels are served on [POST /v1/stt](https://paxalabs.com/docs/api/stt). The details are on [OpenAI's deprecations page](https://developers.openai.com/api/docs/deprecations). For speech, OpenAI points to its Realtime API, which works differently from `/v1/audio/speech`. The Paxa API serves [POST /v1/audio/speech](https://paxalabs.com/docs/api/speech) and [POST /v1/audio/transcriptions](https://paxalabs.com/docs/api/transcriptions) in OpenAI's shape and accepts OpenAI's model and voice names. Where your code or tool lets you set a base URL, two settings switch it. | Setting | Value | | --- | --- | | Base URL | `https://api.paxalabs.com/v1` | | API key | A Paxa key from the [dashboard](https://paxalabs.com/app/keys) | Model names, voice names, and request bodies stay as they are. A new account starts with 100 free credits. ## Text to speech Python: ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.paxalabs.com/v1", api_key=os.environ["PAXA_API_KEY"], ) with client.audio.speech.with_streaming_response.create( model="tts-1", voice="alloy", input="สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", ) as response: response.stream_to_file("speech.mp3") ``` TypeScript: ```typescript import { writeFile } from "node:fs/promises"; import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.paxalabs.com/v1", apiKey: process.env.PAXA_API_KEY, }); const response = await client.audio.speech.create({ model: "tts-1", voice: "alloy", input: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", }); await writeFile("speech.mp3", Buffer.from(await response.arrayBuffer())); ``` curl: ```bash curl -X POST https://api.paxalabs.com/v1/audio/speech \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "tts-1", "input": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "alloy" }' \ --output speech.mp3 ``` AI SDK: ```typescript import { readFile, writeFile } from "node:fs/promises"; import { createOpenAI } from "@ai-sdk/openai"; import { generateSpeech, transcribe } from "ai"; const paxa = createOpenAI({ baseURL: "https://api.paxalabs.com/v1", apiKey: process.env.PAXA_API_KEY, }); const { audio } = await generateSpeech({ model: paxa.speech("tts-1"), voice: "alloy", text: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", }); await writeFile("speech.mp3", audio.uint8Array); const { text } = await transcribe({ model: paxa.transcription("whisper-1"), audio: await readFile("meeting.m4a"), }); ``` ## Speech to text Python: ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.paxalabs.com/v1", api_key=os.environ["PAXA_API_KEY"], ) transcript = client.audio.transcriptions.create( model="whisper-1", file=open("meeting.m4a", "rb"), ) print(transcript.text) ``` - `response_format` is `json` (the default), `verbose_json`, `text`, `srt`, or `vtt`. - `language` passes a hint, and without one the model detects the language. - A recording runs up to 60 minutes and 25 MiB. - Paxa STT Lite is a Research preview. ## Which voice speaks When a request names an OpenAI voice, one of our voices speaks it. Every one of them speaks English, Thai, and Mandarin, mixed sentences included. Each response names the voice that spoke in its `x-voice-id` header. | You send | Paxa voice | Role | | --- | --- | --- | | alloy | Foi Thong (`foithong`) | Narrator | | ash | Khanom Krok (`khanomkrok`) | Lead voice | | ballad | Oliang (`oliang`) | Late-night radio | | cedar | Khao Tom (`khaotom`) | Family man | | coral | Cookie (`cookie`) | Support agent | | echo | Massaman (`massaman`) | Leading man | | fable | Espresso (`espresso`) | Long-form reader | | marin | Salim (`salim`) | Girl next door | | nova | Nom Yen (`nomyen`) | Lead voice | | onyx | Thong Ek (`thongek`) | News anchor | | sage | Bai Toey (`baitoey`) | Mentor | | shimmer | Tako (`tako`) | Friendly reader | | verse | Latte (`latte`) | Brand films | To choose a voice yourself, send its id instead, for example `"khanomkrok"`. [GET /v1/voices](https://paxalabs.com/docs/api/voices) lists all 50, and the [voice library](https://paxalabs.com/voices) plays them. Model names map the same way. `tts-1`, `tts-1-hd`, `gpt-4o-mini-tts`, `gpt-4o-mini-tts-2025-03-20`, `gpt-4o-mini-tts-2025-12-15` run on `paxa-tts-flash-v2`, and `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-03-20`, `gpt-4o-mini-transcribe-2025-12-15` run on `paxa-stt-lite-v1-preview`. ## In tools you already use Most tools with an OpenAI speech option let you change the base URL. Menu names vary by version. - In Open WebUI, open Admin Panel, Settings, Audio. Choose the OpenAI engine for text to speech and for speech to text, set the base URL to `https://api.paxalabs.com/v1`, and paste your Paxa key. - In n8n, create an OpenAI credential for audio alone, with Base URL `https://api.paxalabs.com/v1` and your Paxa key, and use it in the OpenAI node's audio actions. Keep your existing credential for chat, because Paxa serves speech and transcription only. - In Home Assistant's OpenAI TTS custom integration, choose the custom option, set the URL to `https://api.paxalabs.com/v1/audio/speech`, and keep your model and voice. - In LibreChat, set the `url` of the speech and transcription settings in `librechat.yaml` to `https://api.paxalabs.com/v1/audio/speech` and `https://api.paxalabs.com/v1/audio/transcriptions`. LibreChat posts to the URL as written. - In SillyTavern, choose the OpenAI Compatible TTS provider and set the endpoint to `https://api.paxalabs.com/v1/audio/speech`. The [integration guides](https://paxalabs.com/docs/integrations) walk through Open WebUI, LibreChat, Home Assistant, and SillyTavern step by step, along with AnythingLLM, LiveKit Agents, and Strands Agents. A tool that reads `OPENAI_BASE_URL` moves all of its OpenAI calls to Paxa, chat included, and Paxa does not serve chat. Set it only for a tool that uses OpenAI for audio alone. ## What differs from OpenAI - The formats are mp3 (the default), opus, wav, pcm, mulaw, and alaw. pcm is 24 kHz 16-bit mono. aac and flac answer 400. - Speed runs from 0.5 to 1.5, and a value outside that range answers 400. - `instructions` is ignored. To steer the delivery, write [direction tags](https://paxalabs.com/docs/directions) into the text, such as `[whispering] see you tomorrow` or `[cheerful] welcome back!`. - Streaming is on by default, and `"stream": false` returns a complete file. - Streaming sends raw audio bytes under an audio content type, also when a request sets `"stream_format": "sse"`. A client that parses events reads the content type first. - A request the schema refuses answers [400 validation](https://paxalabs.com/docs/errors#validation) with `path`, the field at fault, and `reason`, a stable code for what is wrong with it. - An unknown model or voice answers [400 unknown_model](https://paxalabs.com/docs/errors#unknown_model) or [400 unknown_voice](https://paxalabs.com/docs/errors#unknown_voice). - A speech request carries up to 5,000 characters. - Speech costs 10 credits per 1,000 characters, and transcription 8.33 credits per minute. See [pricing](https://paxalabs.com/pricing). ## More than a drop-in - Every voice speaks English, Thai, and Mandarin, mixed sentences included. - [Direction tags](https://paxalabs.com/docs/directions) steer the delivery and add sounds such as `[laughs]` and `[sighs]`, and `` marks a span. - The native API adds [word timestamps](https://paxalabs.com/docs/timestamps) and a [live socket](https://paxalabs.com/docs/live-streaming) that takes text as it is written. ## Questions ### Does alloy sound the same? No. Our voices answer to OpenAI's names, and the table above shows which voice speaks each one. Listen before you switch, or pick any voice by id. ### What if my tool has no base URL setting? Then it needs a native Paxa integration. [Tell us which tool](https://paxalabs.com/contact). ### Can I keep OpenAI for chat? Yes. Point only your audio calls at Paxa. ### What happens with a name you do not map? An unknown voice or model answers 400 `unknown_voice` or `unknown_model`. Nothing switches silently. # Use Paxa in apps and frameworks Apps and frameworks built for OpenAI's speech and transcription endpoints work with Paxa after two settings change. Setup guides for Open WebUI, LibreChat, AnythingLLM, Home Assistant, SillyTavern, LiveKit Agents, and Strands Agents. Give the apps you already run voices that speak English, Thai, and Mandarin, mixed sentences included, and transcription for what your users say. Any app or framework built for OpenAI's speech and transcription endpoints can call Paxa. ## Connect an OpenAI-compatible app Most apps need two settings. | Setting | Value | | --- | --- | | Base URL | `https://api.paxalabs.com/v1` | | API key | A Paxa key from the [dashboard](https://paxalabs.com/app/keys) | An app that asks for the full endpoint takes `https://api.paxalabs.com/v1/audio/speech` for text to speech and `https://api.paxalabs.com/v1/audio/transcriptions` for speech to text. A new account starts with 100 free credits. ## Choose a model and a voice An app's OpenAI defaults keep working. Paxa accepts OpenAI's speech and transcription model names, `gpt-4o-transcribe-diarize` aside, and its 13 voice names, and [maps each voice name](https://paxalabs.com/docs/accepted-voice-names) to a Paxa voice. To choose the voice yourself, set the model to `paxa-tts-flash-v2` and the voice to a Paxa voice id, such as `khanomkrok`. [GET /v1/voices](https://paxalabs.com/docs/api/voices) lists every voice with its model, and the [voice library](https://paxalabs.com/voices) plays them. For transcription, set the model to `paxa-stt-lite-v1-preview`, a Research preview. ## Setup guides - [Use Paxa with Open WebUI](https://paxalabs.com/docs/integrations/open-webui): Read chat replies aloud and dictate messages - [Use Paxa with LibreChat](https://paxalabs.com/docs/integrations/librechat): Speech and transcription from librechat.yaml - [Use Paxa with AnythingLLM](https://paxalabs.com/docs/integrations/anythingllm): Spoken replies, dictated prompts, and transcribed uploads - [Use Paxa with Home Assistant](https://paxalabs.com/docs/integrations/home-assistant): A voice for the assistant and for announcements - [Use Paxa with SillyTavern](https://paxalabs.com/docs/integrations/sillytavern): A voice for each character - [Use Paxa with LiveKit Agents](https://paxalabs.com/docs/integrations/livekit): Speech and transcription for voice agents - [Use Paxa with Strands Agents](https://paxalabs.com/docs/integrations/strands): Paxa tools for Strands agents through MCP ## Check the connection Many apps show a generic error when a request fails. To read the cause, send the same request with curl, using the model and voice your app sends. curl: ```bash curl -X POST https://api.paxalabs.com/v1/audio/speech \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "paxa-tts-flash-v2", "input": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok" }' \ --output speech.mp3 ``` A working setup writes an MP3 file. A refused request writes a short JSON body to the same file, and its `title` field names the cause. | Code | Cause | | --- | --- | | unauthorized | The API key is missing or mistyped. | | insufficient_credits | The account has run out of credits. | | unknown_model | The model is neither a Paxa model id nor an accepted name. | | unknown_voice | The voice is neither a voice of that model nor an accepted name. | | validation | A field is outside what Paxa accepts, such as a speed above 1.5. `path` names the field. | The [error catalog](https://paxalabs.com/docs/errors) lists every code. ## Tag your integration If you publish an integration that other people install, such as a plugin or a client library, send the optional `x-paxa-client` request header with its name and version. It tells Paxa which integration a request comes from. curl: ```bash curl -X POST https://api.paxalabs.com/v1/audio/speech \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -H "x-paxa-client: my-plugin/1.4.0" \ -d '{ "model": "paxa-tts-flash-v2", "input": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok" }' \ --output speech.mp3 ``` - The value is a name, or a name and a version joined by `/`, such as `my-plugin` or `my-plugin/1.4.0`. - A name has 1 to 48 characters from lowercase letters, digits, `.`, `_`, and `-`, and starts with a letter or digit. - A version has 1 to 24 characters from the same set plus `+`, and starts with a letter or digit. - Paxa trims spaces around the value and lowercases it before reading it. - Paxa ignores a value in any other form, and the request proceeds as if the header were absent. The header changes nothing about the request, its response, or its price. The speech, transcription, OCR, Document Extraction, and translation endpoints read it, and voice design does not. On a live socket, send it with the upgrade request. The API's CORS policy allows it, which lets browser clients send it too. # Use Paxa with Open WebUI Read Open WebUI chat replies aloud in English, Thai, and Mandarin, and dictate messages, through Open WebUI's built-in OpenAI audio engines. Give your Open WebUI chats a voice that speaks English, Thai, and Mandarin, mixed sentences included, and dictate messages by voice. Open WebUI's built-in OpenAI engines for text to speech and speech to text connect to Paxa with a base URL and an API key. Both live under Admin Panel > Settings > Audio. Menu and field names can differ between Open WebUI versions. ## Read replies aloud Under Text-to-Speech, set these fields and click Save. | Field | Value | | --- | --- | | Text-to-Speech Engine | `OpenAI` | | API Base URL | `https://api.paxalabs.com/v1` | | API Key | Your Paxa API key | | TTS Model | `paxa-tts-flash-v2` | | TTS Voice | A Paxa voice id, such as `khanomkrok` | Choosing the OpenAI engine fills in `tts-1` and `alloy`. Both work on Paxa, and `alloy` speaks in a Paxa voice ([accepted voice names](https://paxalabs.com/docs/accepted-voice-names)). Set your own model and voice to choose the voice that speaks. The TTS Voice suggestions list the Paxa TTS Flash v2 voices, which Open WebUI reads from [GET /v1/audio/voices](https://paxalabs.com/docs/api/audio-voices), and the TTS Model suggestions list `paxa-tts-flash-v2` from [GET /v1/audio/models](https://paxalabs.com/docs/api/audio-models). A voice set on a model, or in a user's own settings, takes priority over this default. Change those too if a chat keeps an old voice. Leave Additional Parameters empty. Open WebUI sends no `response_format`, and Paxa answers with MP3. To test, play any reply with the speaker icon under it. ## Dictate messages Under Speech-to-Text, set these fields and click Save. | Field | Value | | --- | --- | | Speech-to-Text Engine | `OpenAI` | | API Base URL | `https://api.paxalabs.com/v1` | | API Key | Your Paxa API key | | Request Format | `Multipart Upload` | | STT Model | `paxa-stt-lite-v1-preview` | STT Model starts empty, and a request without a model answers 400. Paxa does not accept the JSON Base64 request format. Paxa STT Lite is a Research preview, and a recording can run up to 60 minutes and 25 MiB. To test, click the microphone in the message box and speak. ## Set it with environment variables To configure a deployment, set these variables on the Open WebUI container. .env: ```bash AUDIO_TTS_ENGINE=openai AUDIO_TTS_OPENAI_API_BASE_URL=https://api.paxalabs.com/v1 AUDIO_TTS_OPENAI_API_KEY=pxa_your_key_here AUDIO_TTS_MODEL=paxa-tts-flash-v2 AUDIO_TTS_VOICE=khanomkrok AUDIO_STT_ENGINE=openai AUDIO_STT_OPENAI_API_BASE_URL=https://api.paxalabs.com/v1 AUDIO_STT_OPENAI_API_KEY=pxa_your_key_here AUDIO_STT_MODEL=paxa-stt-lite-v1-preview ``` Open WebUI reads these variables on its first start. After that, settings saved in the Admin Panel take precedence, unless `ENABLE_PERSISTENT_CONFIG` is set to `false`. ## When speech or dictation fails Open WebUI shows a generic message, such as a server connection error, when Paxa refuses a request. [Send the same request with curl](https://paxalabs.com/docs/integrations#check) to read the cause. Look first for a mistyped model or voice, an old voice set on a model or a user, and anything in Additional Parameters. # Use Paxa with LibreChat Read LibreChat replies aloud in English, Thai, and Mandarin, and transcribe what users say, by pointing LibreChat's OpenAI speech providers at Paxa. Give LibreChat replies a voice that speaks English, Thai, and Mandarin, mixed sentences included, and let your users talk to their chats. LibreChat's OpenAI speech providers connect to Paxa from `librechat.yaml`. ## Add Paxa to librechat.yaml Put your Paxa API key in LibreChat's `.env` file. .env: ```bash TTS_API_KEY=pxa_your_key_here STT_API_KEY=pxa_your_key_here ``` Then add the speech section to `librechat.yaml` and restart LibreChat. librechat.yaml: ```yaml speech: tts: openai: url: 'https://api.paxalabs.com/v1/audio/speech' apiKey: '${TTS_API_KEY}' model: 'paxa-tts-flash-v2' voices: ['khanomkrok', 'nomyen', 'foithong'] stt: openai: url: 'https://api.paxalabs.com/v1/audio/transcriptions' apiKey: '${STT_API_KEY}' model: 'paxa-stt-lite-v1-preview' ``` - Set `url` to the full endpoint path, as in the sample. - List the voices your users can pick under `voices`. Any voice id from [GET /v1/voices](https://paxalabs.com/docs/api/voices) whose model is `paxa-tts-flash-v2` works. - Keep one provider under each of `speech.tts` and `speech.stt`, and remove any other. - OpenAI's model and voice names, such as `tts-1` and `alloy`, also work ([accepted voice names](https://paxalabs.com/docs/accepted-voice-names)). Paxa STT Lite is a Research preview, and a recording can run up to 60 minutes and 25 MiB. ## Turn it on for your users Each user picks the speech engines in the speech tab of their own settings. To make Paxa the default for everyone, set `engineTTS` under `speech.speechTab.textToSpeech` and `engineSTT` under `speech.speechTab.speechToText` to `external`, and set `voice` under `speech.speechTab.textToSpeech` to a voice from your list. Field names can differ between LibreChat versions. LibreChat's [speech settings page](https://www.librechat.ai/docs/configuration/stt_tts) documents the current ones. # Use Paxa with AnythingLLM Read AnythingLLM replies aloud in English, Thai, and Mandarin, dictate prompts, and transcribe uploaded recordings, through AnythingLLM's OpenAI-compatible providers. Give AnythingLLM a voice that speaks English, Thai, and Mandarin, mixed sentences included. Paxa can also transcribe the prompts you dictate and the audio and video files you upload to a workspace. Each feature has its own provider setting, and each takes the same base URL and API key. Menu and field names can differ between AnythingLLM versions. ## Read replies aloud Open Settings > Voice & Speech. Under Text-to-speech Preference, choose OpenAI Compatible, set these fields, and save. | Field | Value | | --- | --- | | Base URL | `https://api.paxalabs.com/v1` | | API Key | Your Paxa API key | | TTS Model | `paxa-tts-flash-v2` | | Voice Model | A Paxa voice id, such as `khanomkrok` | Left empty, the model and voice fall back to `tts-1` and `alloy`, which Paxa also accepts ([accepted voice names](https://paxalabs.com/docs/accepted-voice-names)). Any voice id from [GET /v1/voices](https://paxalabs.com/docs/api/voices) whose model is `paxa-tts-flash-v2` works. ## Dictate prompts On the same page, under Speech-to-text Preference, choose Generic OpenAI and set these fields. | Field | Value | | --- | --- | | Base URL | `https://api.paxalabs.com/v1` | | API Key | Your Paxa API key | | Transcription Model | `paxa-stt-lite-v1-preview` | ## Transcribe uploaded audio and video Open Settings > Transcription, choose OpenAI Compatible, and set the same three fields: Base URL, API Key, and Transcription Model. Set the model here. Left empty, AnythingLLM sends `whisper-small`, which Paxa answers with [400 unknown_model](https://paxalabs.com/docs/errors#unknown_model). Paxa STT Lite is a Research preview, and a recording can run up to 60 minutes and 25 MiB. ## Set it with environment variables To configure a Docker or bare-metal deployment, set these variables in AnythingLLM's `.env` file. .env: ```bash # Text to speech TTS_PROVIDER="generic-openai" TTS_OPEN_AI_COMPATIBLE_ENDPOINT="https://api.paxalabs.com/v1" TTS_OPEN_AI_COMPATIBLE_KEY=pxa_your_key_here TTS_OPEN_AI_COMPATIBLE_MODEL=paxa-tts-flash-v2 TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL=khanomkrok # Voice input in chat STT_PROVIDER="generic-openai" STT_OPEN_AI_COMPATIBLE_ENDPOINT="https://api.paxalabs.com/v1" STT_OPEN_AI_COMPATIBLE_KEY=pxa_your_key_here STT_OPEN_AI_COMPATIBLE_MODEL=paxa-stt-lite-v1-preview # Uploaded audio and video WHISPER_PROVIDER="generic-openai" WHISPER_GENERIC_OPEN_AI_BASE_URL="https://api.paxalabs.com/v1" WHISPER_GENERIC_OPEN_AI_API_KEY=pxa_your_key_here WHISPER_GENERIC_OPEN_AI_MODEL=paxa-stt-lite-v1-preview ``` # Use Paxa with Home Assistant Give Home Assistant's voice assistant and announcements a voice that speaks English, Thai, and Mandarin, through the OpenAI TTS custom integration. Give your Home Assistant voice assistant and announcements a voice that speaks English, Thai, and Mandarin, mixed sentences included. The [OpenAI TTS](https://github.com/sfortis/openai_tts) custom integration, installed from HACS, connects to Paxa through its Custom preset. Menu and field names can differ between versions of the integration. ## Add Paxa as a provider 1. Install the integration: In HACS, search for OpenAI TTS, download it, and restart Home Assistant. 2. Add an entry: Go to Settings > Devices & services > Add integration > OpenAI TTS, and pick the Custom / Self-hosted provider. 3. Connect Paxa: Set API Endpoint to `https://api.paxalabs.com/v1/audio/speech` and API Key to your Paxa API key. Setup checks the entry with an empty text, which Paxa refuses before it reads the key. A mistyped key passes that check and fails at the first announcement. When speech fails, check the key against the dashboard first. ## Add a TTS agent Add a TTS agent to the entry and set these fields. | Field | Value | | --- | --- | | Model | `paxa-tts-flash-v2` | | Voice | A Paxa voice id, such as `khanomkrok` | | Speed | From 0.5 to 1.5 | | Audio format | `mp3` | - Type the voice. The Custom preset has no voice list for Paxa, and any voice id from [GET /v1/voices](https://paxalabs.com/docs/api/voices) whose model is `paxa-tts-flash-v2` works. OpenAI's voice names, such as `alloy`, also work ([accepted voice names](https://paxalabs.com/docs/accepted-voice-names)). - Keep Speed between 0.5 and 1.5. The slider runs from 0.25 to 4.0, and Paxa answers 400 outside its own range. - Keep Audio format on `mp3`, or choose `opus`, `wav`, or `pcm`. `aac` and `flac` answer 400. ## Use it in the voice assistant Open Settings > Voice assistants, pick the assistant, and choose the agent under Text-to-speech. The assistant's own Voice setting takes priority over the agent's voice. Set it to a Paxa voice as well. The agent is a regular text-to-speech entity. Home Assistant's `tts.speak` action and the integration's `openai_tts.say` action both use it for announcements. # Use Paxa with SillyTavern Give SillyTavern characters voices that speak English, Thai, and Mandarin, through SillyTavern's OpenAI Compatible TTS provider. Give each SillyTavern character a voice that speaks English, Thai, and Mandarin, mixed sentences included. SillyTavern's OpenAI Compatible TTS provider connects to Paxa with an endpoint and an API key. ## Before you start - Create a Paxa API key on the [dashboard](https://paxalabs.com/app/keys). A new account starts with 100 free credits. - Pick voices from [GET /v1/voices](https://paxalabs.com/docs/api/voices) whose model is `paxa-tts-flash-v2`, or listen first in the [voice library](https://paxalabs.com/voices). ## Connect 1. Open the TTS settings: Open the Extensions panel and its TTS section. 2. Choose the provider: Select OpenAI Compatible as the TTS provider. 3. Set the endpoint: Set Provider Endpoint to `https://api.paxalabs.com/v1/audio/speech`. The field takes the full URL. 4. Add the key: Click API Key and paste your Paxa API key. 5. Set the model: Set Model to `paxa-tts-flash-v2`. 6. List the voices: Set Available Voices to Paxa voice ids separated by commas, such as `khanomkrok,nomyen,foithong`. 7. Set the speed: Keep Speed between 0.5 and 1.5. 8. Assign voices: Check Enabled, open a chat, choose a voice for each character in the voice map, and click Apply. ## Feature support | Feature | Support | | --- | --- | | Voice list from the API | No. SillyTavern reads the voices from Available Voices. | | Speed | Yes, from 0.5 to 1.5 | | Audio format | MP3, which SillyTavern always requests | | SillyTavern's defaults | Yes. `tts-1` and the default voices map to Paxa voices ([accepted voice names](https://paxalabs.com/docs/accepted-voice-names)). | ## Known limitations - The Speed slider runs from 0.25 to 4, and Paxa answers 400 outside 0.5 to 1.5. - SillyTavern sends each request from its own server, which needs to reach the Paxa API. - Menu and field names can differ between SillyTavern versions. ## Support Paxa supports this setup. For account, billing, or API problems, [contact us](https://paxalabs.com/contact). Checked against SillyTavern 1.19.0 on October 24, 2026. # Use Paxa with LiveKit Agents Give a LiveKit voice agent a voice that speaks English, Thai, and Mandarin, and Paxa transcription, through LiveKit's OpenAI plugin. Give your LiveKit voice agent a voice that speaks English, Thai, and Mandarin, mixed sentences included. LiveKit's OpenAI plugin connects to Paxa with a base URL and an API key, for text to speech and for speech to text. ## Install the plugin Terminal: ```bash pip install "livekit-agents[openai]" export PAXA_API_KEY=pxa_your_key_here ``` ## Give the agent a voice Python: ```python import os from livekit.plugins import openai paxa_tts = openai.TTS( base_url="https://api.paxalabs.com/v1", api_key=os.environ["PAXA_API_KEY"], model="paxa-tts-flash-v2", voice="khanomkrok", response_format="pcm", ) ``` - Set `model` to `paxa-tts-flash-v2`, or keep OpenAI's model names, which run on it. The plugin asks for an event stream on most model names, and Paxa answers with raw audio, which the plugin plays as it arrives. - Set `voice` to any voice id from [GET /v1/voices](https://paxalabs.com/docs/api/voices) whose model is `paxa-tts-flash-v2`. OpenAI's voice names also work ([accepted voice names](https://paxalabs.com/docs/accepted-voice-names)). - Keep `response_format="pcm"`. It returns 16-bit mono samples at 24 kHz, the rate the plugin plays, with no decoding step. - Set `speed` between 0.5 and 1.5. A value outside that range answers 400. The agent speaks a reply one sentence at a time. Thai written without punctuation counts as one sentence, and a long Thai reply then starts speaking only after the LLM finishes it. ## Add transcription In the same file, create the transcription side and pass both to the session. Python: ```python from livekit.agents import AgentSession, inference paxa_stt = openai.STT( base_url="https://api.paxalabs.com/v1", api_key=os.environ["PAXA_API_KEY"], model="paxa-stt-lite-v1-preview", language="th", ) session = AgentSession( stt=paxa_stt, # Any LLM LiveKit Agents supports. llm=inference.LLM("openai/gpt-4.1-mini"), tts=paxa_tts, vad=inference.VAD(), ) ``` - The plugin transcribes each finished turn as one recording. The session needs a VAD to find where each turn ends. - `language` passes a hint, such as `th`. Set `detect_language=True` to send none and let the model detect the language. - Paxa STT Lite is a Research preview. Argument names can differ between LiveKit Agents versions. # Use Paxa with Strands Agents Give a Strands agent Paxa's speech, transcription, OCR, and translation tools through the Paxa MCP server and the MCP client built into Strands. Give a [Strands Agents](https://strandsagents.com) agent the Paxa API as tools. It can speak in English, Thai, and Mandarin, transcribe recordings, read documents with OCR and extract their fields, and translate into Thai. Strands loads the tools from the Paxa MCP server, `@paxalabs/mcp`, through its built-in MCP client. No Strands-specific package is needed. ## Before you start - Install Node.js 20 or later, or 22 for `listen`. The server runs with `npx`. - Create an API key on the [dashboard](https://paxalabs.com/app/keys?new=mcp). A new account starts with 100 free credits. - Choose a model provider for the agent. Strands uses Amazon Bedrock by default, and its [model providers guide](https://strandsagents.com/docs/user-guide/sdk/model-providers/) lists the others. ## Python Terminal: ```bash pip install "strands-agents>=1.62,<2" export PAXA_API_KEY=pxa_your_key_here ``` Python: ```python import os from mcp import StdioServerParameters, stdio_client from strands import Agent from strands.tools.mcp import MCPClient paxa = MCPClient(lambda: stdio_client( StdioServerParameters( command="npx", args=["-y", "@paxalabs/mcp"], env={"PAXA_API_KEY": os.environ["PAXA_API_KEY"]}, ) )) agent = Agent(tools=[paxa]) agent("Save 'สวัสดีครับ, welcome to the show' as welcome.mp3 with the khanomkrok voice.") ``` The agent starts the server on first use and stops it when the agent is done. ## TypeScript Terminal: ```bash npm install @strands-agents/sdk @modelcontextprotocol/sdk export PAXA_API_KEY=pxa_your_key_here ``` TypeScript: ```typescript import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { Agent, McpClient } from "@strands-agents/sdk"; const paxa = new McpClient({ transport: new StdioClientTransport({ command: "npx", args: ["-y", "@paxalabs/mcp"], env: { PAXA_API_KEY: process.env.PAXA_API_KEY ?? "" }, }), }); const agent = new Agent({ tools: [paxa] }); await agent.invoke( "Save 'สวัสดีครับ, welcome to the show' as welcome.mp3 with the khanomkrok voice.", ); ``` ## Choose the tools The playback tools, `design_voice`, `refine_voice`, and `listen` use the speakers and microphone of the machine that runs the server. A server-side agent usually needs only the file tools. In Python, pass `tool_filters` to `MCPClient` to keep those. Python: ```python paxa = MCPClient( lambda: stdio_client( StdioServerParameters( command="npx", args=["-y", "@paxalabs/mcp"], env={"PAXA_API_KEY": os.environ["PAXA_API_KEY"]}, ) ), tool_filters={ "allowed": [ "text_to_speech", "transcribe_audio", "ocr_document", "extract_fields", "translate_to_thai", "list_voices", ] }, ) ``` The [MCP server page](https://paxalabs.com/docs/mcp#tools) lists every tool and its price. Its [environment variables](https://paxalabs.com/docs/mcp#environment), such as the output folder and the default voice, go in `env` next to `PAXA_API_KEY`. Speech runs on Paxa TTS Flash v2, whose 50 voices each speak English, Thai, and Mandarin. [Direction tags](https://paxalabs.com/docs/directions) written into the text, such as `[whispering]` or `[laughs]`, set the delivery. Transcription is a Research preview. Strands documents its MCP client in the [MCP tools guide](https://strandsagents.com/docs/user-guide/sdk/tools/mcp-tools/). # Synthesize speech `POST https://api.paxalabs.com/v1/tts` POST /v1/tts converts text into speech, as MP3, Opus, WAV, or raw PCM, in one request. Charged in credits per character before synthesis, refunded automatically when synthesis fails. Set `stream: true` for chunked delivery ([Streaming](https://paxalabs.com/docs/streaming)) and [`timestamps`](https://paxalabs.com/docs/timestamps) for timing spans. An [Idempotency-Key](https://paxalabs.com/docs/idempotency) makes retries safe, and text past `max_chars` answers [400 text_too_long](https://paxalabs.com/docs/errors#text_too_long) with no charge. On a voice that reads [emotion tags](https://paxalabs.com/docs/emotion-tags) or [direction tags](https://paxalabs.com/docs/directions), a tag the voice cannot read answers [400 tag_invalid](https://paxalabs.com/docs/errors#tag_invalid), also with no charge. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. - `idempotency-key` (string, optional, 1 to 200 characters, pattern ^[!-~]+$): Makes a retry safe: two requests carrying the same key are charged once. One request per key runs at a time. A concurrent duplicate answers 409 idempotency_in_flight. Reuse a key only to retry an identical request. A changed payload is rejected with 422. Accepts up to 200 printable ASCII characters other than space. See authentication at https://paxalabs.com/docs/authentication. ## Body - `text` (string, required, at least 1 character, example "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs"): The text to synthesize, in Thai, English, Mandarin Chinese (Simplified script), or any mix of them. Cost is 10 credits per 1000 characters, billed per character and rounded up to the next hundredth of a credit, minimum 0.1 credit per request. At most 5,000 characters; longer text answers 400 text_too_long, and GET /v1/models reports each model's own ceiling as max_chars. On Paxa TTS Flash v2, every voice reads tags. A bracketed direction such as [whispering] sets the delivery from where it stands. A sound such as [laughs] plays where it is written, [pause] or [pause 300ms] adds silence, at most 500 ms for an exact length, and and mark a span. A tag that model refuses, such as [british accent] or [sniffs], answers 400 tag_invalid. On a Paxa TTS Flash (paxa-tts-flash-v1) voice whose emotion_tags list is not empty, a bracketed tag such as [happy] or [sad:0.5] sets the delivery from where it stands. On those voices a tag outside the list, or a weight that is not a number, answers 400 tag_invalid. Every other v1 voice skips the tags it knows. Tag characters bill like the rest of the text. - `voice` (string, required, 1 to 100 characters, example "khanomkrok"): The voice id, the stable identifier GET /v1/voices lists, for example "khanomkrok". Case is ignored, and only the id is matched. A designed voice has an id starting "voice_", and GET /v1/voices/designed lists the account's. It speaks only on Paxa TTS Flash v2 (paxa-tts-flash-v2) and only for the account that saved it; on another model or account, or once deleted, it answers 400 unknown_voice. Speech in a designed voice bills at 1.25x the model's per-character rate, or 1.5x with timestamps. - `model` (string, required, 1 to 100 characters, example "paxa-tts-flash-v2"): TTS model id, for example paxa-tts-flash-v2. GET /v1/models lists the served catalog. - `format` ("mp3" or "opus" or "wav" or "pcm" or "mulaw" or "alaw", optional, default "mp3"): Audio format. mp3 is compressed and the default. opus is Ogg Opus, 48 kbps on Paxa TTS Flash and 64 on Paxa TTS Flash v2, the smallest, for streaming and web playback. wav is uncompressed 16-bit PCM under a RIFF header, the largest, for audio that feeds further processing. pcm is those samples with no header, 16-bit little-endian mono, for voice agents and phone systems that take raw samples. Both are 24 kHz unless sample_rate says otherwise. mulaw and alaw are G.711 mu-law and A-law with no header, one byte per sample at 8 kHz, for phone lines. Paxa TTS Flash v2 serves them, and Paxa TTS Flash answers unsupported_format. - `sample_rate` (8000 or 16000 or 22050 or 24000 or 32000 or 44100 or 48000, optional, example 16000): Sample rate of pcm and wav, in Hz. Paxa TTS Flash v2 serves 8000, 16000, 22050, 24000, 32000, 44100, 48000, and Paxa TTS Flash serves 24000 only. For any other format the value may only restate that format's own rate, which is 8000 for mulaw and alaw. A rate the model doesn't serve answers unsupported_format. A request costs the same at every rate. - `stream` (boolean, optional, default false): When true the audio arrives as a chunked stream while it is synthesized, lowering time to first byte. When false the response is a complete body with a content-length. - `timestamps` ("word" or "sentence" or "utterance", optional): Adds speech timing to the response: "word" times every word, "sentence" every sentence, "utterance" every continuous run of speech. When set, the response is application/json with base64 audio and timing spans, or application/x-ndjson events when stream is true, and the request bills at 1.25x the model's per-character rate. - `speed` (number, optional, default 1, min 0.5, max 1.5, example 1.1): Speaking rate, as a multiplier on the voice's own design speed. It runs from 0.5 to 1.5, and 1 is the voice as designed. Rate changes how the audio sounds. It does not change how many characters were sent. A request costs the same at every rate. Values round to the nearest 0.05. Timing spans are measured against the audio that was produced. They already carry the rate. - `language` ("auto" or "th" or "en" or "zh", optional, default "auto", example "en"): Sets how codes, digits, and symbols are read. Words keep the language of their script, and a number inside Thai or Chinese text keeps that text's language. Elsewhere, "th" reads them in Thai (AB2039 with Thai letter names and digits) and "en" in English. "zh" reads codes in English, a number next to Chinese characters in Mandarin, and other digits and symbols in Thai. "auto", the default, takes the language most of the text is written in, counting English only in words of three or more letters, and Thai on a tie. Under "auto", a bare number, AB2039, or 250 ml reads as Thai, and ISO9001 or 100 USD as English. Text with no Thai, Chinese, digits, or counted English word reads as English when a two-letter word has a lowercase letter, such as Hi or I'm OK. Pin "th" or "en" to choose how a short number or code reads. A request costs the same in every language. Every value but "auto" is a code GET /v1/voices reports as a voice's language. POST /v1/audio/speech ignores this field and reads every request as "auto". - `temperature` (number, optional, default 0.5, min 0, max 2, example 0.5): Sets how much the pacing, pauses, breaths, and melody of a reading vary from one request to the next. It runs from 0 to 2, and 0.5 is the default. They vary least at 0 and most at 2. Paxa TTS Flash v2 reads it. Any other model accepts only a value that rounds to 0.5, and any other answers validation. A request costs the same at every temperature. Values round to the nearest 0.1. POST /v1/audio/speech and the start frame of WS /v1/tts/live read it too. ## Response audio/mpeg or audio/ogg or audio/wav or audio/pcm;rate=24000;channels=1 or audio/basic or audio/x-alaw-basic. The synthesized speech as binary audio, in the format and sample rate the request chose. | Header | Description | | --- | --- | | x-request-id | Identifies the request for support and log correlation. Sent on every response, errors included, except the cached catalog (GET /v1/models, /v1/voices, /v1/audio/models, and /v1/audio/voices). Name it in request_ids when you send feedback about the request. | | x-credits-charged | Credits charged for this request, exact to a hundredth. An idempotent replay reports the original charge. Sent on audio responses and on the OpenAI-compatible transcription endpoint, whose body follows OpenAI's shape and carries no usage object. POST /v1/translate, /v1/ocr, /v1/extract, /v1/stt, and /v1/voices/design report the charge in the usage object of the body instead. | | x-voice-id | The id of the voice that spoke. When the request named the voice by an accepted alias, such as an OpenAI voice name, it is the Paxa voice the alias maps to. Sent on audio responses. | | x-ratelimit-limit | Requests per minute the plan allows. | | x-ratelimit-remaining | Requests left in the current minute window. | | x-ratelimit-reset | Unix time in seconds when the current window resets. | ### With timestamps application/json. When timestamps is set, the response body is JSON carrying the audio and its timing. The status and the headers are unchanged. - `audio` (string, required): The complete audio file in the requested container, base64-encoded. - `timing` (object, required) - `timing.granularity` ("word" or "sentence" or "utterance", required): The granularity that was requested. - `timing.spans` (array of objects, required): Spans in text order, together covering the spoken input. - `timing.spans[].text` (string, required): The spoken input text this span covers. An emotion tag takes no span. - `timing.spans[].start` (number, required): Where the span begins in the audio, in seconds. - `timing.spans[].end` (number, required): Where the span ends in the audio, in seconds. application/x-ndjson. One JSON event per line, arriving as audio is synthesized. Each chunk carries the spans its own audio completed. The stream ends with a done event. A mid-stream failure ends it with an error event and refunds the charge automatically. **chunk**: One NDJSON line of the streamed timed response. - `type` ("chunk", required) - `audio` (string, required): Audio chunk in the requested container, base64-encoded. Decode each chunk and concatenate the bytes in arrival order; never concatenate the base64 strings themselves. Empty on a line that only carries spans completed after the last audio. - `spans` (array of objects, required): Spans newly completed by the audio delivered up to and including this chunk; empty when this chunk completes none. - `spans[].text` (string, required): The spoken input text this span covers. An emotion tag takes no span. - `spans[].start` (number, required): Where the span begins in the audio, in seconds. - `spans[].end` (number, required): Where the span ends in the audio, in seconds. **done**: The final NDJSON line. Every span already arrived on the chunk lines. This event carries their count and never repeats them. - `type` ("done", required) - `granularity` ("word" or "sentence" or "utterance", required): The granularity that was requested. - `span_count` (integer, required): How many spans the chunk lines delivered in total. Compare it with the spans you collected. **error**: Synthesis failed after delivery began. The charge is refunded automatically; this is the final line of the stream. - `type` ("error", required) - `error` (object, required): RFC 9457 problem details at the minimum shape, served with content type application/problem+json. Responses carry no prose: the errors reference is the documentation for every code. One code, 400 schema_invalid, adds two fields so a caller can fix the schema without guessing: path and reason. - `error.title` (string, required): Stable machine-readable error code, for example "insufficient_credits". Branch on this field; the errors reference documents every code's meaning and the recommended handling. - `error.status` (integer, required): HTTP status code, mirrored from the response. - `error.path` (string, optional): Present on 400 schema_invalid alone. Names the schema field the reason applies to, in dot form ("line_items[].amount"), or an empty string for the schema as a whole. - `error.reason` (string, optional): Present on 400 schema_invalid alone. One stable code from the schema reasons list in the Document Extraction reference, naming what is wrong at path. ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `unknown_model` (400): The model field does not name a served model. - `unsupported_format` (400): The model does not serve this format, or this format at the requested sample rate. - `unknown_voice` (400): The voice id names no voice on the model's roster and no designed voice this account holds. - `text_too_long` (400): The text exceeds the model's per-request character ceiling. - `tag_invalid` (400): The text holds a tag the model refuses. On Paxa TTS Flash v2, the bracket asks for what the model does not do, such as an accent, singing, or a sound outside the supported list. It can also hold no letters or carry a weight that is not a number. On a Paxa TTS Flash voice that takes emotion tags, the bracketed word is outside the tag list, or its weight is not a number. Nothing was charged. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `idempotency_in_flight` (409): Another request with this Idempotency-Key is in flight right now. - `idempotency_refunded` (409): The original request under this Idempotency-Key failed and was refunded. - `idempotency_mismatch` (422): This Idempotency-Key was already used for a different request. - `unspeakable_text` (422): The text contains nothing the model can voice: only tags, emoji, punctuation, or a script it does not speak. A text of tags alone is refused before the charge; any other is charged and refunded. - `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "format": "mp3", "stream": false }' \ --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: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", voice: "khanomkrok", model: "paxa-tts-flash-v2", }), }); 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": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2"}, ) response.raise_for_status() with open("speech.mp3", "wb") as f: f.write(response.content) ``` # Synthesize speech (OpenAI-compatible) `POST https://api.paxalabs.com/v1/audio/speech` POST /v1/audio/speech serves the OpenAI audio.speech request shape with Paxa voices. Same pricing, headers, and error codes as /v1/tts; streaming by default. Point an OpenAI SDK at `https://api.paxalabs.com/v1` and call `audio.speech.create`. The [OpenAI compatibility guide](https://paxalabs.com/docs/openai-compatibility) covers the field mapping; timed speech and a pinned `language` use [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech). ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. - `idempotency-key` (string, optional, 1 to 200 characters, pattern ^[!-~]+$): Makes a retry safe: two requests carrying the same key are charged once. One request per key runs at a time. A concurrent duplicate answers 409 idempotency_in_flight. Reuse a key only to retry an identical request. A changed payload is rejected with 422. Accepts up to 200 printable ASCII characters other than space. See authentication at https://paxalabs.com/docs/authentication. ## Body - `model` (string, required, 1 to 100 characters): TTS model id, for example paxa-tts-flash-v2. OpenAI's tts-1, tts-1-hd, gpt-4o-mini-tts, gpt-4o-mini-tts-2025-03-20, gpt-4o-mini-tts-2025-12-15 are accepted and run on paxa-tts-flash-v2. - `input` (string, required, at least 1 character): The text to synthesize. Same limits, pricing, and tags as POST /v1/tts. - `voice` (string, required, 1 to 100 characters): The voice id from GET /v1/voices, for example "khanomkrok", or a designed voice id starting "voice_", resolved and billed as on POST /v1/tts. Case is ignored. OpenAI's voice names, such as alloy, are accepted and map to Paxa voices, and the x-voice-id response header names the voice that spoke. - `response_format` ("mp3" or "opus" or "wav" or "pcm" or "mulaw" or "alaw", optional, default "mp3"): Audio container, as format on POST /v1/tts. Other OpenAI formats are not served and answer 400. - `sample_rate` (8000 or 16000 or 22050 or 24000 or 32000 or 44100 or 48000, optional, example 16000): Sample rate of pcm and wav, in Hz. Paxa TTS Flash v2 serves 8000, 16000, 22050, 24000, 32000, 44100, 48000, and Paxa TTS Flash serves 24000 only. A Paxa extension to OpenAI's body. On this endpoint, formats other than pcm and wav ignore it, and any value must still be one of the listed rates. A rate the model doesn't serve answers unsupported_format. A request costs the same at every rate. - `stream` (boolean, optional, default true): Streams the audio as it is synthesized, matching OpenAI delivery. Defaults to true. Set false to receive a complete body with a content-length. - `stream_format` ("audio" or "sse", optional): The OpenAI field of the same name. The response is always the raw audio bytes under the audio content type of response_format, "sse" included. Read the content-type header before parsing events. - `speed` (number, optional, default 1, min 0.5, max 1.5, example 1.1): Speaking rate, as a multiplier on the voice's own design speed. It runs from 0.5 to 1.5, and 1 is the voice as designed. Rate changes how the audio sounds. It does not change how many characters were sent. A request costs the same at every rate. Values round to the nearest 0.05. The OpenAI field of the same name. A value outside that range answers 400. - `temperature` (number, optional, default 0.5, min 0, max 2, example 0.5): Sets how much the pacing, pauses, breaths, and melody of a reading vary from one request to the next. It runs from 0 to 2, and 0.5 is the default. They vary least at 0 and most at 2. Paxa TTS Flash v2 reads it. Any other model accepts only a value that rounds to 0.5, and any other answers validation. A request costs the same at every temperature. Values round to the nearest 0.1. A Paxa extension to OpenAI's body, read as on POST /v1/tts. ## Response audio/mpeg or audio/ogg or audio/wav or audio/pcm;rate=24000;channels=1 or audio/basic or audio/x-alaw-basic. The synthesized speech as binary audio, in the format and sample rate the request chose. | Header | Description | | --- | --- | | x-request-id | Identifies the request for support and log correlation. Sent on every response, errors included, except the cached catalog (GET /v1/models, /v1/voices, /v1/audio/models, and /v1/audio/voices). Name it in request_ids when you send feedback about the request. | | x-credits-charged | Credits charged for this request, exact to a hundredth. An idempotent replay reports the original charge. Sent on audio responses and on the OpenAI-compatible transcription endpoint, whose body follows OpenAI's shape and carries no usage object. POST /v1/translate, /v1/ocr, /v1/extract, /v1/stt, and /v1/voices/design report the charge in the usage object of the body instead. | | x-voice-id | The id of the voice that spoke. When the request named the voice by an accepted alias, such as an OpenAI voice name, it is the Paxa voice the alias maps to. Sent on audio responses. | | x-ratelimit-limit | Requests per minute the plan allows. | | x-ratelimit-remaining | Requests left in the current minute window. | | x-ratelimit-reset | Unix time in seconds when the current window resets. | ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `unknown_model` (400): The model field does not name a served model. - `unsupported_format` (400): The model does not serve this format, or this format at the requested sample rate. - `unknown_voice` (400): The voice id names no voice on the model's roster and no designed voice this account holds. - `text_too_long` (400): The text exceeds the model's per-request character ceiling. - `tag_invalid` (400): The text holds a tag the model refuses. On Paxa TTS Flash v2, the bracket asks for what the model does not do, such as an accent, singing, or a sound outside the supported list. It can also hold no letters or carry a weight that is not a number. On a Paxa TTS Flash voice that takes emotion tags, the bracketed word is outside the tag list, or its weight is not a number. Nothing was charged. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `idempotency_in_flight` (409): Another request with this Idempotency-Key is in flight right now. - `idempotency_refunded` (409): The original request under this Idempotency-Key failed and was refunded. - `idempotency_mismatch` (422): This Idempotency-Key was already used for a different request. - `unspeakable_text` (422): The text contains nothing the model can voice: only tags, emoji, punctuation, or a script it does not speak. A text of tags alone is refused before the charge; any other is charged and refunded. - `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example curl: ```bash curl -X POST https://api.paxalabs.com/v1/audio/speech \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "paxa-tts-flash-v2", "input": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok" }' \ --output speech.mp3 ``` TypeScript: ```typescript import { writeFile } from "node:fs/promises"; import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.paxalabs.com/v1", apiKey: process.env.PAXA_API_KEY, }); const response = await client.audio.speech.create({ model: "paxa-tts-flash-v2", voice: "khanomkrok", input: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", }); await writeFile("speech.mp3", Buffer.from(await response.arrayBuffer())); ``` Python: ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.paxalabs.com/v1", api_key=os.environ["PAXA_API_KEY"], ) with client.audio.speech.with_streaming_response.create( model="paxa-tts-flash-v2", voice="khanomkrok", input="สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", ) as response: response.stream_to_file("speech.mp3") ``` # Live speech `WS wss://api.paxalabs.com/v1/tts/live` WS /v1/tts/live holds one WebSocket for many syntheses: text frames in, audio frames out. Billed cumulatively per character, one minimum per connection. Connect once, send `start`, then interleave `text`, `flush`, `cancel`, and `end`. An open connection holds one of the plan's [concurrent speech slots](https://paxalabs.com/docs/rate-limits#concurrency) until it closes. The [Live streaming guide](https://paxalabs.com/docs/live-streaming) covers the lifecycle and billing. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. See authentication at https://paxalabs.com/docs/authentication. ## Connect `wss://api.paxalabs.com/v1/tts/live`. Binary frames carry audio in the session's format. JSON text frames carry control, billing, and timing events. ## Client messages **start**: Configures the session. Must be the first frame; exactly one per connection. - `type` ("start", required) - `model` (string, required): TTS model id, for example paxa-tts-flash-v2. - `voice` (string, required): Voice id, resolved as on POST /v1/tts. A designed voice serves the connection until it closes, even if it is deleted meanwhile. - `format` ("mp3" or "opus" or "wav" or "pcm" or "mulaw" or "alaw", optional, default "mp3"): Audio format for the session's binary frames, as format on POST /v1/tts. - `sample_rate` (8000 or 16000 or 22050 or 24000 or 32000 or 44100 or 48000, optional, example 16000): Sample rate of pcm and wav, in Hz. Paxa TTS Flash v2 serves 8000, 16000, 22050, 24000, 32000, 44100, 48000, and Paxa TTS Flash serves 24000 only. For any other format the value may only restate that format's own rate, which is 8000 for mulaw and alaw. Fixed for the connection, like format. A rate the model doesn't serve answers unsupported_format. A request costs the same at every rate. - `timestamps` ("word" or "sentence" or "utterance", optional): Adds timing events to the session at 1.25x the standard rate, applied to the connection's cumulative character count. - `speed` (number, optional, default 1, min 0.5, max 1.5, example 1.1): Speaking rate, as a multiplier on the voice's own design speed. It runs from 0.5 to 1.5, and 1 is the voice as designed. Rate changes how the audio sounds. It does not change how many characters were sent. A request costs the same at every rate. Values round to the nearest 0.05. Fixed for the connection, like model and voice. - `language` ("auto" or "th" or "en" or "zh", optional, default "auto", example "en"): Sets how codes, digits, and symbols are read. Words keep the language of their script, and a number inside Thai or Chinese text keeps that text's language. Elsewhere, "th" reads them in Thai (AB2039 with Thai letter names and digits) and "en" in English. "zh" reads codes in English, a number next to Chinese characters in Mandarin, and other digits and symbols in Thai. "auto", the default, takes the language most of the text is written in, counting English only in words of three or more letters, and Thai on a tie. Under "auto", a bare number, AB2039, or 250 ml reads as Thai, and ISO9001 or 100 USD as English. Text with no Thai, Chinese, digits, or counted English word reads as English when a two-letter word has a lowercase letter, such as Hi or I'm OK. Pin "th" or "en" to choose how a short number or code reads. A request costs the same in every language. Fixed for the connection, like model and voice. "auto" resolves on each leg's own text, and a leg that is only a number reads as Thai. - `temperature` (number, optional, default 0.5, min 0, max 2, example 0.5): Sets how much the pacing, pauses, breaths, and melody of a reading vary from one request to the next. It runs from 0 to 2, and 0.5 is the default. They vary least at 0 and most at 2. Paxa TTS Flash v2 reads it. Any other model accepts only a value that rounds to 0.5, and any other answers validation. A request costs the same at every temperature. Values round to the nearest 0.1. Fixed for the connection, like model and voice. A refused value answers an in-band validation error, and the connection stays open for a corrected start frame. **text**: Appends text. The server auto-flushes on a newline, sentence-ending punctuation, or a buffered-length threshold, holding back a tag still open at the end of the buffer for up to that threshold. - `type` ("text", required) - `text` (string, required, at least 1 character): Text appended to the synthesis buffer. A frame longer than the model's max_chars answers an in-band text_too_long error. Every Paxa TTS Flash v2 voice reads tags, as does a Paxa TTS Flash (paxa-tts-flash-v1) voice whose emotion_tags list is not empty. On those voices, tags that no words follow yet wait in the buffer, and each leg opens with the tags the previous leg ended in. A frame that would take the waiting tags and the opening tags past max_chars also answers text_too_long, as does one that would leave opening tags longer than half of max_chars. A frame whose tag the voice cannot read answers an in-band tag_invalid error and leaves the buffer as it was. Billing is cumulative per connection: the total charged equals what one POST /v1/tts call carrying the text of every charged leg would cost. **flush**: Synthesizes the buffered text now. A flush with nothing to speak in the buffer (empty, or tags alone) answers flushed immediately. - `type` ("flush", required) **cancel**: Stops the leg being spoken and discards the buffered text. Use it when the speaker interrupts. The leg in flight was charged before its first byte and stays charged. The discarded buffer was never charged. A session that sends one sentence per frame pays only for the sentence it interrupted. The server answers cancelled, and the session stays open for more text. - `type` ("cancel", required) **end**: Flushes any remaining text, then the server sends done and closes with code 1000. - `type` ("end", required) ## Server events Audio frames: Raw audio bytes in the session's format for the current leg, in delivery order between that leg's charged and flushed events. **started**: The session is configured and ready for text. - `type` ("started", required) - `connection` (string, required): Connection id; quote it in support requests (the x-request-id analog). - `model` (string, required): The model serving this session. - `voice` (string, required): Canonical voice id the session resolved to. - `format` ("mp3" or "opus" or "wav" or "pcm" or "mulaw" or "alaw", required): The audio format every binary frame of the session carries. - `sample_rate` (integer, required): The sample rate of the session's audio in Hz, which is the rate the start frame asked for or else the format's own. Check it before playing raw pcm. - `timestamps` ("word" or "sentence" or "utterance", optional): Echoed when the session requested timing. - `speed` (number, optional): Echoed when the session asked for a rate of its own. - `language` ("th" or "en" or "zh", optional): Echoed when the session pinned a language. - `temperature` (number, optional): Echoed when the session asked for a temperature of its own. **charged**: Billing receipt, sent before the leg's audio. Receipts replace the x-credits-charged header. - `type` ("charged", required) - `seq` (integer, required): The synthesis leg this receipt covers, starting at 1. - `chars` (integer, required): Characters synthesized in this leg. - `credits` (number, required): Credits charged for this leg, exact to a hundredth. The 0.1-credit request floor applies once per connection, on the first charged leg. - `total_credits` (number, required): Total credits charged on this connection so far. **timing**: Timing spans; sent only when the session set timestamps. - `type` ("timing", required) - `seq` (integer, required): The leg these spans belong to. - `spans` (array of objects, required): Times are relative to the start of this leg's audio (each leg is an independent audio stream). - `spans[].text` (string, required): The spoken input text this span covers. An emotion tag takes no span. - `spans[].start` (number, required): Where the span begins in the audio, in seconds. - `spans[].end` (number, required): Where the span ends in the audio, in seconds. **flushed**: The buffer is drained; safe to end or keep sending. - `type` ("flushed", required) - `seq` (integer, required): All audio and timing through leg seq have been sent and the buffer is empty. 0 when no leg has run yet. **cancelled**: A cancel frame was applied. The stopped leg keeps the charge its receipt reported; the discarded buffer was never charged. The session stays open, and the next text frame starts a new leg. - `type` ("cancelled", required) - `seq` (integer, required): The leg that stopped. No further audio or timing arrives for it, and any already received is safe to discard. 0 when no leg was in flight and only the buffer was discarded. **error**: Something failed. Recoverable errors leave the connection open; after a fatal one the server closes with the code listed below. - `type` ("error", required) - `code` (string, required): Stable machine-readable code from the error catalog. Branch on this field; the errors reference documents every code. - `seq` (integer, optional): The leg the error belongs to, when it belongs to one. - `retry_after` (integer, optional): Seconds to wait before re-flushing; sent with rate_limited. **done**: Every leg is delivered; the server closes with code 1000 after this event. A connection that closes without done did not finish delivering. - `type` ("done", required) - `total_chars` (integer, required): Characters synthesized on this connection. - `total_credits` (number, required): Credits charged on this connection, exact to a hundredth. ## Close codes | Code | Meaning | | --- | --- | | 1000 | Normal close after done, or client-initiated. | | 1001 | The server is restarting. Reconnect and resume. | | 1009 | A frame exceeded 64 KiB. | | 4400 | A frame violated the session protocol, or the start frame named an unknown model or voice, or a format or sample rate the model does not serve. A frame that fails validation answers an error event, and the connection stays open. | | 4401 | The API key stopped being valid mid-session. | | 4402 | Insufficient credits for the next leg. | | 4403 | The key's spending cap was reached. | | 4408 | No frame arrived and no audio was delivered for 300 seconds. | | 4413 | The client read too slowly and the outbound buffer overflowed. | | 4429 | Too many concurrent live connections for the account. Close an existing connection and reconnect. | | 4500 | Internal server error. | | 4503 | The start frame's model is not served right now. Nothing was charged; retry once GET /health reports it served. | ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `live_protocol` (400): A frame on a live WebSocket violated the session protocol. The frame arrived before start, repeated start, or followed end. - `unknown_model` (400): The model field does not name a served model. - `unsupported_format` (400): The model does not serve this format, or this format at the requested sample rate. - `unknown_voice` (400): The voice id names no voice on the model's roster and no designed voice this account holds. - `text_too_long` (400): The text exceeds the model's per-request character ceiling. - `tag_invalid` (400): The text holds a tag the model refuses. On Paxa TTS Flash v2, the bracket asks for what the model does not do, such as an accent, singing, or a sound outside the supported list. It can also hold no letters or carry a weight that is not a number. On a Paxa TTS Flash voice that takes emotion tags, the bracketed word is outside the tag list, or its weight is not a number. Nothing was charged. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `unspeakable_text` (422): The text contains nothing the model can voice: only tags, emoji, punctuation, or a script it does not speak. A text of tags alone is refused before the charge; any other is charged and refunded. - `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example TypeScript: ```typescript // Runs on Bun as-is; on Node, `import WebSocket from "ws"` accepts // the same constructor options. Browsers cannot send this header, and // API keys never ship to browsers anyway. const socket = new WebSocket("wss://api.paxalabs.com/v1/tts/live", { headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}` }, }); socket.binaryType = "arraybuffer"; socket.onopen = () => { socket.send(JSON.stringify({ type: "start", model: "paxa-tts-flash-v2", voice: "khanomkrok", timestamps: "word", })); // Send text whenever it becomes available, token by token if you like. socket.send(JSON.stringify({ type: "text", text: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs" })); socket.send(JSON.stringify({ type: "flush" })); socket.send(JSON.stringify({ type: "end" })); }; socket.onmessage = (event) => { if (typeof event.data === "string") { const message = JSON.parse(event.data); if (message.type === "done") socket.close(); return; } player.feed(new Uint8Array(event.data)); // binary frames are audio }; ``` Python: ```python import json import os from websockets.sync.client import connect # pip install websockets with connect( "wss://api.paxalabs.com/v1/tts/live", additional_headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, ) as socket: socket.send(json.dumps({"type": "start", "model": "paxa-tts-flash-v2", "voice": "khanomkrok"})) socket.send(json.dumps({"type": "text", "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs"})) socket.send(json.dumps({"type": "flush"})) socket.send(json.dumps({"type": "end"})) with open("speech.mp3", "wb") as f: for frame in socket: if isinstance(frame, bytes): f.write(frame) # binary frames are audio continue event = json.loads(frame) if event["type"] == "done": break ``` Server events: ```json {"type":"started","connection":"7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a","model":"paxa-tts-flash-v2","voice":"khanomkrok","format":"mp3","sample_rate":44100,"timestamps":"word"} {"type":"charged","seq":1,"chars":36,"credits":0.45,"total_credits":0.45} {"type":"timing","seq":1,"spans":[{"text":"สวัสดี","start":0,"end":0.51},{"text":"ครับ","start":0.51,"end":0.85}]} {"type":"flushed","seq":1} {"type":"done","total_chars":36,"total_credits":0.45} ``` # Design a voice `POST https://api.paxalabs.com/v1/voices/design` POST /v1/voices/design makes a voice from a written description, or refines a draft with an instruction. It returns the voice's words, its example line as MP3, and a draft to refine or save. Describe who is speaking and how they sound, such as their age, gender, pitch, pace, manner, and the language they speak. The voice speaks the language the description names, else the language the description is written in. Its name, role, and description come back in the language the description is written in. To change the voice, send its `draft` back with an `instruction` such as "a little lower and slower". Every response carries a new draft, and a voice exists only once you [save](https://paxalabs.com/docs/api/save-voice) its draft. Each call costs 5 credits, the example line included, charged before the work and refunded automatically when no voice comes back. A retry designs a new voice and is charged again. The endpoint refuses an [Idempotency-Key](https://paxalabs.com/docs/idempotency) with [400 validation](https://paxalabs.com/docs/errors#validation). A call takes a rate-limit token and one of the plan's [concurrent speech slots](https://paxalabs.com/docs/rate-limits#concurrency). ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. See authentication at https://paxalabs.com/docs/authentication. ## Body - `description` (string, optional, 1 to 1,000 characters, example "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai"): Who the voice is and how it sounds, in English, Thai, or Chinese, up to 1,000 characters: age, gender, pitch, pace, manner, and the language it speaks. The voice speaks the language this names, else the language this is written in, and its name, role, and description come back in the language this is written in. - `draft` (string, optional, 1 to 4,096 characters): A draft an earlier design response returned, to refine that voice. Send it with instruction and without description. - `instruction` (string, optional, 1 to 500 characters): What to change about the draft's voice, up to 500 characters, such as "a little lower and slower". The rest of the voice stays as it was. ## Response - `draft` (string, required): The voice, sealed. Send it back with an instruction to refine the voice, or to POST /v1/voices/designed to save it. It opens only for this account. - `voice` (object, required): The designed voice's words. - `voice.name` (string, required): A name for the voice, which saving keeps unless you send another. - `voice.role` (string, required): Who the voice is, in a few words. - `voice.description` (string, required): How the voice sounds, in one sentence. - `voice.gender` ("f" or "m", required): Voice gender, "f" or "m". - `voice.language` ("th" or "en" or "zh", required): The one language the voice speaks: "th" (Thai), "en" (English), or "zh" (Mandarin Chinese, Simplified script). - `voice.line` (string, required): The example line audio speaks, up to 120 characters in the voice's language. - `voice.model` (string, required): The model the voice speaks on, paxa-tts-flash-v2. - `audio` (string, required): The example line spoken in the voice, as base64 MP3. - `usage` (object, required): What the request was billed for. - `usage.credits` (number, required): What this turn cost, 5 credits. ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `draft_invalid` (400): The draft field holds no draft this account received from POST /v1/voices/design. It is altered or cut short, belongs to another account, or no longer opens after a platform update. Nothing was charged. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `content_blocked` (422): The upstream safety system declined to process this content. The charge was refunded. - `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example curl: ```bash curl -X POST https://api.paxalabs.com/v1/voices/design \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai" }' \ -o design.json jq -r .audio design.json | base64 --decode > line.mp3 ``` TypeScript: ```typescript import { writeFile } from "node:fs/promises"; const headers = { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }; // Design, then refine the draft with an instruction. let response = await fetch("https://api.paxalabs.com/v1/voices/design", { method: "POST", headers, body: JSON.stringify({ description: "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai", }), }); if (!response.ok) throw new Error(`Design failed: ${response.status}`); let turn = await response.json(); response = await fetch("https://api.paxalabs.com/v1/voices/design", { method: "POST", headers, body: JSON.stringify({ draft: turn.draft, instruction: "A little slower, with a softer ending", }), }); if (!response.ok) throw new Error(`Refine failed: ${response.status}`); turn = await response.json(); await writeFile("line.mp3", Buffer.from(turn.audio, "base64")); ``` Python: ```python import base64 import os import requests URL = "https://api.paxalabs.com/v1/voices/design" HEADERS = {"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"} # Design, then refine the draft with an instruction. turn = requests.post( URL, headers=HEADERS, json={"description": "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai"}, ) turn.raise_for_status() turn = requests.post( URL, headers=HEADERS, json={ "draft": turn.json()["draft"], "instruction": "A little slower, with a softer ending", }, ) turn.raise_for_status() with open("line.mp3", "wb") as f: f.write(base64.b64decode(turn.json()["audio"])) ``` Response: ```json { "draft": "kD3v9QxW2mN7pL0sT5yB8cR1fH4jK6zA...", "voice": { "name": "Nok", "role": "Pharmacist", "description": "A calm, warm voice that explains each step slowly.", "gender": "f", "language": "th", "line": "ทานยานี้หลังอาหารวันละสามครั้งนะคะ ถ้ามีผื่นขึ้นให้หยุดยาแล้วกลับมาหาเราได้เลยค่ะ", "model": "paxa-tts-flash-v2" }, "audio": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYy...", "usage": { "credits": 5 } } ``` # List designed voices `GET https://api.paxalabs.com/v1/voices/designed` GET /v1/voices/designed lists the account's designed voices, oldest first, with the slots in use and the plan's limit. Each `id` is the value the `voice` field accepts with `model` set to `paxa-tts-flash-v2`. After a downgrade, `slots.used` can exceed `slots.limit`. Every saved voice keeps speaking, and a save waits until a voice is deleted. Listing costs nothing and takes no rate-limit token. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. See authentication at https://paxalabs.com/docs/authentication. ## Response - `voices` (array of objects, required): The account's designed voices, oldest first. - `voices[].id` (string, required): The voice id, starting "voice_". Send it as voice with model paxa-tts-flash-v2 on POST /v1/tts, POST /v1/audio/speech, or the /v1/tts/live socket. - `voices[].name` (string, required): Display name, which PATCH changes. The voice field does not accept it. - `voices[].role` (string, required): Who the voice is, in a few words. - `voices[].description` (string, required): How the voice sounds, in one sentence. - `voices[].gender` ("f" or "m", required): Voice gender, "f" or "m". - `voices[].language` ("th" or "en" or "zh", required): The one language the voice speaks: "th" (Thai), "en" (English), or "zh" (Mandarin Chinese, Simplified script). - `voices[].model` (string, required): The model the voice speaks on. - `voices[].created_at` (string, required): Save time, ISO 8601. - `slots` (object, required): The account's voice slots. - `slots.used` (integer, required): Designed voices the account holds. - `slots.limit` (integer, required): Designed voices the plan allows. After a downgrade, used can exceed it: every saved voice keeps speaking, and saving waits until used is below limit. ## Errors - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. ## Example curl: ```bash curl https://api.paxalabs.com/v1/voices/designed \ -H "Authorization: Bearer $PAXA_API_KEY" ``` Response: ```json { "voices": [ { "id": "voice_3f9a1c2e7b4d4e8a9c0b1d2e3f4a5b6c", "name": "Nok", "role": "Pharmacist", "description": "A calm, warm voice that explains each step slowly.", "gender": "f", "language": "th", "model": "paxa-tts-flash-v2", "created_at": "2026-10-23T08:41:16.204Z" } ], "slots": { "used": 1, "limit": 3 } } ``` # Save a designed voice `POST https://api.paxalabs.com/v1/voices/designed` POST /v1/voices/designed saves a draft from POST /v1/voices/design as one of the account's voices and returns it with its id. Send `name` to choose the voice's name, or leave it out to keep the name the design call gave. Saving the same draft again returns the same voice and takes no slot. A retry is safe. A full account answers [403 voice_slots_full](https://paxalabs.com/docs/errors#voice_slots_full), and [Designed voices](https://paxalabs.com/docs/designed-voices#plans) lists each plan's slots. Saving costs nothing and takes no rate-limit token. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. See authentication at https://paxalabs.com/docs/authentication. ## Body - `draft` (string, required, 1 to 4,096 characters): The draft a design response returned. Saving one draft again returns the voice it made and takes no slot. - `name` (string, optional, 1 to 60 characters): A name for the voice, up to 60 characters. Omitted, the voice keeps the name the design response gave it. A catalog voice's name answers 400 voice_name_taken. ## Response - `id` (string, required): The voice id, starting "voice_". Send it as voice with model paxa-tts-flash-v2 on POST /v1/tts, POST /v1/audio/speech, or the /v1/tts/live socket. - `name` (string, required): Display name, which PATCH changes. The voice field does not accept it. - `role` (string, required): Who the voice is, in a few words. - `description` (string, required): How the voice sounds, in one sentence. - `gender` ("f" or "m", required): Voice gender, "f" or "m". - `language` ("th" or "en" or "zh", required): The one language the voice speaks: "th" (Thai), "en" (English), or "zh" (Mandarin Chinese, Simplified script). - `model` (string, required): The model the voice speaks on. - `created_at` (string, required): Save time, ISO 8601. ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `draft_invalid` (400): The draft field holds no draft this account received from POST /v1/voices/design. It is altered or cut short, belongs to another account, or no longer opens after a platform update. Nothing was charged. - `voice_name_taken` (400): The name is a catalog voice's name, which no designed voice can take. Nothing was saved. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `voice_slots_full` (403): The account holds as many designed voices as its plan allows. Nothing was saved. ## Example curl: ```bash curl -X POST https://api.paxalabs.com/v1/voices/designed \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"draft\": \"$(jq -r .draft design.json)\", \"name\": \"Nok\"}" ``` TypeScript: ```typescript // draft is the string POST /v1/voices/design returned. Saving it // again returns the same voice. const response = await fetch("https://api.paxalabs.com/v1/voices/designed", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ draft, name: "Nok" }), }); if (!response.ok) throw new Error(`Save failed: ${response.status}`); const voice = await response.json(); console.log(voice.id); ``` Python: ```python import os import requests # draft is the string POST /v1/voices/design returned. Saving it # again returns the same voice. voice = requests.post( "https://api.paxalabs.com/v1/voices/designed", headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, json={"draft": draft, "name": "Nok"}, ) voice.raise_for_status() print(voice.json()["id"]) ``` Response: ```json { "id": "voice_3f9a1c2e7b4d4e8a9c0b1d2e3f4a5b6c", "name": "Nok", "role": "Pharmacist", "description": "A calm, warm voice that explains each step slowly.", "gender": "f", "language": "th", "model": "paxa-tts-flash-v2", "created_at": "2026-10-23T08:41:16.204Z" } ``` # Rename a designed voice `PATCH https://api.paxalabs.com/v1/voices/designed/{id}` PATCH /v1/voices/designed/{id} changes a designed voice's name and returns the voice. The id and the sound stay the same. A catalog voice's name answers [400 voice_name_taken](https://paxalabs.com/docs/errors#voice_name_taken), and an id the account does not hold answers [400 unknown_voice](https://paxalabs.com/docs/errors#unknown_voice). Renaming costs nothing and takes no rate-limit token. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. See authentication at https://paxalabs.com/docs/authentication. ## Path - `id` (string, required): The designed voice's id, starting "voice_". ## Body - `name` (string, required, 1 to 60 characters): The new name, up to 60 characters. A catalog voice's name answers 400 voice_name_taken. ## Response - `id` (string, required): The voice id, starting "voice_". Send it as voice with model paxa-tts-flash-v2 on POST /v1/tts, POST /v1/audio/speech, or the /v1/tts/live socket. - `name` (string, required): Display name, which PATCH changes. The voice field does not accept it. - `role` (string, required): Who the voice is, in a few words. - `description` (string, required): How the voice sounds, in one sentence. - `gender` ("f" or "m", required): Voice gender, "f" or "m". - `language` ("th" or "en" or "zh", required): The one language the voice speaks: "th" (Thai), "en" (English), or "zh" (Mandarin Chinese, Simplified script). - `model` (string, required): The model the voice speaks on. - `created_at` (string, required): Save time, ISO 8601. ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `unknown_voice` (400): The voice id names no voice on the model's roster and no designed voice this account holds. - `voice_name_taken` (400): The name is a catalog voice's name, which no designed voice can take. Nothing was saved. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. ## Example curl: ```bash curl -X PATCH https://api.paxalabs.com/v1/voices/designed/voice_3f9a1c2e7b4d4e8a9c0b1d2e3f4a5b6c \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Nok, night shift"}' ``` Response: ```json { "id": "voice_3f9a1c2e7b4d4e8a9c0b1d2e3f4a5b6c", "name": "Nok, night shift", "role": "Pharmacist", "description": "A calm, warm voice that explains each step slowly.", "gender": "f", "language": "th", "model": "paxa-tts-flash-v2", "created_at": "2026-10-23T08:41:16.204Z" } ``` # Delete a designed voice `DELETE https://api.paxalabs.com/v1/voices/designed/{id}` DELETE /v1/voices/designed/{id} deletes a designed voice and frees its slot. Deleting cannot be undone. New requests in the voice answer [400 unknown_voice](https://paxalabs.com/docs/errors#unknown_voice), including a retry under an earlier [Idempotency-Key](https://paxalabs.com/docs/idempotency), and a live connection already open in the voice keeps it until the connection closes. Deleting costs nothing and takes no rate-limit token. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. See authentication at https://paxalabs.com/docs/authentication. ## Path - `id` (string, required): The designed voice's id, starting "voice_". ## Response - `id` (string, required): The deleted voice's id. ## Errors - `unknown_voice` (400): The voice id names no voice on the model's roster and no designed voice this account holds. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. ## Example curl: ```bash curl -X DELETE https://api.paxalabs.com/v1/voices/designed/voice_3f9a1c2e7b4d4e8a9c0b1d2e3f4a5b6c \ -H "Authorization: Bearer $PAXA_API_KEY" ``` Response: ```json { "id": "voice_3f9a1c2e7b4d4e8a9c0b1d2e3f4a5b6c" } ``` # Transcribe a recording `POST https://api.paxalabs.com/v1/stt` POST /v1/stt transcribes a recording of Thai speech, or of another language, verbatim, with word timestamps and speaker turns on request. Charged in credits by recording length before transcription, refunded on failure. Send the file as base64 in `audio`. The format and the length are read from the bytes. The [transcription guide](https://paxalabs.com/docs/speech-to-text) covers the formats, the two conventions, the two styles, word timestamps, speaker turns, the vocabulary, and the length and size limits. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. - `idempotency-key` (string, optional, 1 to 200 characters, pattern ^[!-~]+$): Makes a retry safe: two requests carrying the same key are charged once. One request per key runs at a time. A concurrent duplicate answers 409 idempotency_in_flight. Reuse a key only to retry an identical request. A changed payload is rejected with 422. Accepts up to 200 printable ASCII characters other than space. See authentication at https://paxalabs.com/docs/authentication. ## Body - `audio` (string, required, 1 to 41,000,000 characters): The recording to transcribe, as the base64 encoding of an MP3, WAV, FLAC, Ogg (Opus or Vorbis), M4A, AAC, or WebM file. The format and the length are read from the file itself. No declared field is consulted. Cost is 500 credits per hour of audio, 8.33 per minute, billed by exact length with a minimum of 0.1 credits per request, charged before transcription and refunded automatically when transcription fails. A recording may run up to 60 minutes, and longer answers 400 audio_too_long. The decoded file may be up to 26,214,400 bytes, and larger answers 413 audio_too_large. A file that cannot be read as one of these formats, or whose length cannot be read from its container, answers 400 audio_invalid. A recording with no speech is delivered as an empty transcript and billed for its length. GET /v1/models reports the ceilings as max_duration_seconds and max_bytes. - `model` (string, required, 1 to 100 characters, example "paxa-stt-lite-v1-preview"): Transcription model id, for example paxa-stt-lite-v1-preview. GET /v1/models lists the served catalog. - `language` (string, optional, up to 16 characters, pattern ^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$, example "th"): The language the speech is expected to be in, as a BCP 47 tag such as "th" or "en". The hint filters nothing. Speech in any other language is still transcribed as spoken, and Thai that switches into English mid-sentence needs no setting. Omitted, the model detects the language itself. Set the tag when detection has picked the wrong language. - `timestamps` ("word", optional): Adds a words array to the response, one span per word with its start and end in seconds on the recording's own clock. Thai words are segmented for you. Latin words and numerals arrive whole. A punctuation mark joins the word before it, or the word after it when it opens. The span shape matches the speech API's timing spans. Captions round-trip between the two. Costs nothing extra. Omitted, the response carries the transcript alone. - `diarization` (boolean, optional, default false): true adds a segments array to the response, one entry per speaker turn with its speaker number, text, and start and end in seconds. Speakers are numbered from 0 in order of first appearance. Nobody is identified. With word timestamps on as well, every word span names its speaker too. Costs nothing extra. A diarized recording is read in one pass, which is slower to return and holds the recording under 9 minutes. One that long or longer answers 400 diarization_audio_too_long, uncharged. The same recording transcribes normally with this field off. GET /v1/models reports the ceiling as diarization_limit_seconds. - `style` ("verbatim" or "clean", optional, default "verbatim"): How faithfully the transcript follows the speech. "verbatim", the default, writes exactly what was said: fillers, false starts, and self-corrections included. "clean" drops fillers and merges broken phrases into one, for a transcript that is easier to read. It is a readability choice. A clean transcript departs further from what was said, and our own measurement found it less exact. Any other value answers 400 validation. - `convention` ("spoken" or "written", optional, default "spoken"): How the transcript writes what was said. "spoken", the default, writes numbers as they were said (สามสิบเปอร์เซ็นต์) and a word said twice as two words, which is what a verbatim transcript, a subtitle, or a language model reading the text needs. "written" keeps display conventions: digits and units (30%), the repetition mark ๆ, for dictated text that will be read on a page. The word timestamps are the same under both. - `vocabulary` (array of strings, optional, example ["Paxa Labs","กรุงเทพฯ","quarterly review"]): Terms the speech is likely to contain that the model should recognize as written: product names, people, places, jargon, in Thai or any other language. Up to 50 terms of 50 characters each, and a longer list or term answers 400 validation. The terms bias recognition and are never inserted; a term that was not said does not appear. Costs nothing extra and combines with every other option. - `subtitles` ("srt" or "vtt", optional): Renders the transcript as a subtitle file and returns it in the response's subtitles field. "srt" writes SubRip, "vtt" writes WebVTT. Cues break at a speaker change, at sentence punctuation, and at a pause, then at the line budget, over at most two lines. Every break falls on a word boundary. That is the whole difference on Thai. Thai writes no spaces, and a subtitle cut by character count splits words down the middle. The word spans say where a word ends. Word timing is read for you and timestamps need not be set; setting it as well returns the words array beside the file. Costs nothing extra. A recording with no speech renders an empty SubRip file, or a WebVTT file carrying its signature alone. - `subtitle_line_chars` (integer, optional, min 10, max 120): Columns per subtitle line, 10 to 120. A Chinese, Japanese, or Korean character or full-width punctuation mark takes two columns. Thai vowels and tone marks sit above and below their consonant and take none. A line of the same figure fills about the same box in every script. Omitted, a line holds 60 columns, or 32 (16 Chinese characters) when most of the transcript's letters are Chinese, Japanese, or Korean. Ignored when subtitles is not set. ## Response - `text` (string, required): The transcript, verbatim in the language spoken, with no labels, timestamps, or sound annotations. Empty when the recording carried no speech. - `words` (array of objects, optional): One span per recognized word, in spoken order, each with its text and its start and end in seconds. Present when the request set timestamps to "word"; empty for a recording with no speech. Each span names its speaker when the request set diarization to true. - `words[].text` (string, required): The spoken input text this span covers. An emotion tag takes no span. - `words[].start` (number, required): Where the span begins in the audio, in seconds. - `words[].end` (number, required): Where the span ends in the audio, in seconds. - `words[].speaker` (integer, optional, min 0): The speaker, numbered from 0 in order of first appearance in the recording. Nobody is identified. - `segments` (array of objects, optional): One entry per speaker turn, in spoken order. Present when the request set diarization to true; empty for a recording with no speech. - `segments[].speaker` (integer, required, min 0): The speaker, numbered from 0 in order of first appearance in the recording. Nobody is identified. - `segments[].text` (string, required): The turn's text, exactly as the transcript renders it. Consecutive phrases of one speaker are one turn. - `segments[].start` (number, required): Where the turn begins in the recording, in seconds. - `segments[].end` (number, required): Where the turn ends in the recording, in seconds. - `subtitles` (string, optional): The subtitle file in the format the request asked for, ready to write to disk. Present when the request set subtitles. - `usage` (object, required): What the request was billed for. - `usage.seconds` (number, required): Length of the recording this request was billed for, in seconds, exact to a thousandth. - `usage.credits` (number, required): What this delivery cost, in credits, exact to a hundredth. An idempotent replay reports the ORIGINAL request's charge, since that one charge is what paid for this delivery too; your balance moves only once. ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `unknown_model` (400): The model field does not name a served model. - `audio_invalid` (400): The audio field could not be read as an MP3, WAV, FLAC, Ogg, M4A, AAC, or WebM recording, or its length could not be read from the container. A damaged or truncated file answers this code. Nothing was charged. - `audio_too_large` (413): The decoded recording exceeds the model's size ceiling. Nothing was charged. - `audio_too_long` (400): The recording is longer than the model's per-request ceiling. Nothing was charged. - `diarization_audio_too_long` (400): The recording reaches the length at which speaker attribution stops, which is well below the transcription ceiling. Nothing was charged. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `idempotency_in_flight` (409): Another request with this Idempotency-Key is in flight right now. - `idempotency_refunded` (409): The original request under this Idempotency-Key failed and was refunded. - `idempotency_mismatch` (422): This Idempotency-Key was already used for a different request. - `content_blocked` (422): The upstream safety system declined to process this content. The charge was refunded. - `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example curl: ```bash # Encode without line wrapping: wrapped base64 breaks the JSON string. AUDIO=$(base64 < meeting.m4a | tr -d '\n') # --max-time covers an hour-long recording; curl defaults to no limit. curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\"}" ``` TypeScript: ```typescript import { readFile } from "node:fs/promises"; const audio = (await readFile("meeting.m4a")).toString("base64"); const response = await fetch("https://api.paxalabs.com/v1/stt", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ audio, model: "paxa-stt-lite-v1-preview" }), // A long recording can run for minutes; give it room. signal: AbortSignal.timeout(300_000), }); if (!response.ok) throw new Error(`Transcription failed: ${response.status}`); const { text, usage } = await response.json(); console.log(text, `${usage.credits} credits`); ``` Python: ```python import base64 import os import requests with open("meeting.m4a", "rb") as file: audio = base64.b64encode(file.read()).decode() response = requests.post( "https://api.paxalabs.com/v1/stt", headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, json={"audio": audio, "model": "paxa-stt-lite-v1-preview"}, # A long recording can run for minutes; give it room. timeout=300, ) response.raise_for_status() body = response.json() print(body["text"], body["usage"]["credits"], "credits") ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "usage": { "seconds": 11.952, "credits": 1.66 } } ``` # Realtime transcription `WS wss://api.paxalabs.com/v1/stt/live` WS /v1/stt/live transcribes speech as it is spoken and detects the end of each turn on its own. Built for voice agents. Send the microphone and receive one final transcript per turn. Charged in credits by audio received. Connect once, send `start`, then stream raw audio as binary frames. The connection reports `speech_started` when the user begins talking. It reports `speech_ended` and `transcript` once the turn is over. An agent interrupts its own reply on the first event and answers on the last. An open connection holds one of the plan's [concurrent transcription slots](https://paxalabs.com/docs/rate-limits#concurrency) until it closes. The [turn detection section](https://paxalabs.com/docs/speech-to-text#realtime) of the transcription guide covers the events and billing. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. See authentication at https://paxalabs.com/docs/authentication. ## Connect `wss://api.paxalabs.com/v1/stt/live`. Binary frames carry audio in. JSON text frames carry control in, and turn, transcript, and billing events out. ## Client messages **start**: Configures the session. Must be the first frame; exactly one per connection. - `type` ("start", required) - `model` (string, required, example "paxa-stt-lite-realtime-v1-preview"): Realtime transcription model id, for example paxa-stt-lite-realtime-v1-preview. GET /v1/models lists the served catalog; a recording model answers unknown_model here. - `audio` (object, required): What the binary frames carry, fixed for the connection. The bytes are read exactly as declared. A wrong declaration transcribes noise. - `audio.encoding` ("pcm_s16le" or "mulaw", required): "pcm_s16le" is signed 16-bit little-endian PCM, "mulaw" is G.711 mu-law as telephony delivers it. Mono. - `audio.sample_rate` (16000 or 48000 or 8000, required): Samples per second of the binary frames: 16000 or 48000 for pcm_s16le, 8000 for mulaw. Any other pairing answers validation. - `language` (string, optional, up to 16 characters, pattern ^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$, example "th"): The language the speech is expected to be in, as a BCP 47 tag such as "th" or "en". The hint filters nothing. Speech in any other language is still transcribed as spoken, and Thai that switches into English mid-sentence needs no setting. Omitted, the model detects the language itself. Set the tag when detection has picked the wrong language. - `timestamps` ("word", optional): Adds a words array to every final transcript, one span per word with its start and end in seconds on the connection's clock. Thai words are segmented for you. Costs nothing extra. - `style` ("verbatim" or "clean", optional, default "verbatim"): How faithfully the transcript follows the speech. "verbatim", the default, writes exactly what was said: fillers, false starts, and self-corrections included. "clean" drops fillers and merges broken phrases into one, for a transcript that is easier to read. It is a readability choice. A clean transcript departs further from what was said, and our own measurement found it less exact. Any other value answers 400 validation. - `convention` ("spoken" or "written", optional, default "spoken"): How the transcript writes what was said. "spoken", the default, writes numbers as they were said (สามสิบเปอร์เซ็นต์) and a word said twice as two words, which is what a verbatim transcript, a subtitle, or a language model reading the text needs. "written" keeps display conventions: digits and units (30%), the repetition mark ๆ, for dictated text that will be read on a page. The word timestamps are the same under both. - `vocabulary` (array of strings, optional, example ["Paxa Labs","กรุงเทพฯ","quarterly review"]): Terms the speech is likely to contain that the model should recognize as written: product names, people, places, jargon, in Thai or any other language. Up to 50 terms of 50 characters each, and a longer list or term answers 400 validation. The terms bias recognition and are never inserted; a term that was not said does not appear. Costs nothing extra and combines with every other option. **playback**: Tells the turn detector when your agent is speaking. Its own voice reaching the microphone then cannot open a turn. While speaking is true, opening a turn takes more sustained speech from the user; a real interruption still opens one and arrives as speech_started. Send it on every change. A session that never sends it treats the room as quiet. - `type` ("playback", required) - `speaking` (boolean, required): true while your agent's audio is playing into the same room as the microphone, false once it stops. **end**: Closes the turn in progress, if any, and delivers its transcript, then the server sends done and closes with code 1000. Audio after end is ignored. - `type` ("end", required) ## Server events Audio frames: Audio bytes in the encoding and sample rate the start frame declared, in any chunking and at any cadence, up to 64 KiB a frame. Sending faster than real time is fine. **started**: The session is configured and listening. - `type` ("started", required) - `connection` (string, required): Connection id; quote it in support requests (the x-request-id analog). - `model` (string, required): The model serving this session. **speech_started**: The user started speaking. This is the interruption signal. Stop your agent's audio and cancel the reply it was generating. It arrives once enough speech has been heard to be sure. The time it reports is therefore earlier than the moment it arrives. - `type` ("speech_started", required) - `turn` (integer, required, min 1): The turn this event belongs to, counting from 1 per connection. A higher number is a later turn: an agent that already replied to turn 3 discards turn 3 and answers turn 4. - `time` (number, required): Where the user started speaking. In seconds of audio received since the connection's first binary frame, exact to a thousandth. **transcript**: A transcript of one turn. The final transcript is the turn: send its text to your language model. An interim gives an earlier look at the same turn and never needs handling on its own. - `type` ("transcript", required) - `turn` (integer, required, min 1): The turn this event belongs to, counting from 1 per connection. A higher number is a later turn: an agent that already replied to turn 3 discards turn 3 and answers turn 4. - `is_final` (boolean, required): true for the turn's finished transcript, sent exactly once per turn. false for an interim transcript of the turn so far, sent at a natural pause and replaced by the final. - `text` (string, required): The verbatim transcript of the turn, or of the turn so far. Empty on a final when the turn held no speech the model could read. - `start` (number, required): Where the transcribed audio starts. In seconds of audio received since the connection's first binary frame, exact to a thousandth. - `end` (number, required): Where the transcribed audio ends. In seconds of audio received since the connection's first binary frame, exact to a thousandth. - `words` (array of objects, optional): Word spans on the connection's clock, present on a final when the session set timestamps. - `words[].text` (string, required): The spoken input text this span covers. An emotion tag takes no span. - `words[].start` (number, required): Where the span begins in the audio, in seconds. - `words[].end` (number, required): Where the span ends in the audio, in seconds. **speech_ended**: The user stopped speaking and the turn is closed. Its final transcript follows. Use it to show that the agent is thinking. - `type` ("speech_ended", required) - `turn` (integer, required, min 1): The turn this event belongs to, counting from 1 per connection. A higher number is a later turn: an agent that already replied to turn 3 discards turn 3 and answers turn 4. - `time` (number, required): Where the turn ended. In seconds of audio received since the connection's first binary frame, exact to a thousandth. **charged**: Billing receipt. The connection is charged for the audio it receives, silence included. A period closes at the end of each turn, after a minute of audio with no turn, and at the end of the session. Receipts replace the x-credits-charged header. - `type` ("charged", required) - `seq` (integer, required): The billing period this receipt covers, starting at 1. - `seconds` (number, required): Audio received in this period, in seconds, exact to a thousandth. - `credits` (number, required): Credits charged for this period, exact to a hundredth. The 0.1-credit request floor applies once per connection, on the first receipt. - `total_credits` (number, required): Credits charged on this connection so far, the audio of failed turns excluded, exact to a hundredth. **error**: Something failed. An error with a turn leaves the connection open; after a fatal one the server closes with the code listed below. - `type` ("error", required) - `code` (string, required): Stable machine-readable code from the error catalog. Branch on this field; the errors reference documents every code. - `turn` (integer, optional): The turn the error belongs to, when it belongs to one. That turn delivers no transcript; the session keeps listening. - `not_billed` (number, optional): Credits this turn's audio is held back from the bill, exact to a hundredth. It comes off what the periods after this turn owe. A turn that fails with little audio behind it returns less than this. Sent with a turn. **done**: Every turn is delivered; the server closes with code 1000 after this event. A connection that closes without done did not finish delivering. - `type` ("done", required) - `total_seconds` (number, required): Audio received on this connection, in seconds, exact to a thousandth. - `turns` (integer, required): Turns the connection delivered or failed. - `total_credits` (number, required): Credits charged on this connection, the audio of failed turns excluded, exact to a hundredth. Zero when no turn was delivered, which returns the connection's charge in full. ## Close codes | Code | Meaning | | --- | --- | | 1000 | Normal close after done, or client-initiated. | | 1001 | The server is restarting. Reconnect and resume. | | 1009 | A binary frame exceeded 64 KiB. | | 4400 | A frame violated the session protocol, or the start frame named an unknown model or a sample rate its encoding does not take. Another frame that fails validation answers an error event, and the connection stays open. | | 4401 | The API key stopped being valid mid-session. | | 4402 | The account cannot cover the request floor at start, or the next billing period. | | 4403 | The key's spending cap was reached. | | 4408 | No frame arrived and no event was sent for 300 seconds. | | 4413 | The client read too slowly and the outbound buffer overflowed. | | 4429 | Too many concurrent live connections for the account. Close an existing connection and reconnect. | | 4500 | Internal server error. | | 4502 | The transcription backend failed for the whole connection. What the delivered periods charged stays charged; a connection that delivered no turn is refunded in full. Reconnect. | | 4503 | The transcription backend could not be reached at start. Nothing was charged; retry. | ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `live_protocol` (400): A frame on a live WebSocket violated the session protocol. The frame arrived before start, repeated start, or followed end. - `unknown_model` (400): The model field does not name a served model. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example TypeScript: ```typescript import { open } from "node:fs/promises"; // Runs on Bun as-is; on Node, `import WebSocket from "ws"` accepts // the same constructor options. Browsers cannot send this header, and // API keys never ship to browsers anyway: relay the audio through your // server. const socket = new WebSocket("wss://api.paxalabs.com/v1/stt/live", { headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}` }, }); socket.onopen = async () => { socket.send(JSON.stringify({ type: "start", model: "paxa-stt-lite-realtime-v1-preview", audio: { encoding: "pcm_s16le", sample_rate: 16000 }, language: "th", })); // 16 kHz signed 16-bit mono is 32,000 bytes a second. A microphone // delivers frames at this pace on its own; a file has to be paced. const file = await open("caller.pcm"); const frame = new Uint8Array(640); // 20 ms for (;;) { const { bytesRead } = await file.read(frame, 0, frame.length); if (bytesRead === 0) break; socket.send(frame.subarray(0, bytesRead)); await new Promise((resolve) => setTimeout(resolve, 20)); } socket.send(JSON.stringify({ type: "end" })); }; socket.onmessage = (event) => { const message = JSON.parse(String(event.data)); switch (message.type) { case "speech_started": // The user is talking: stop the agent's audio, cancel its reply. break; case "transcript": if (message.is_final) console.log(`turn ${message.turn}: ${message.text}`); break; case "done": socket.close(); } }; ``` Python: ```python import json import os import time from websockets.sync.client import connect # pip install websockets with connect( "wss://api.paxalabs.com/v1/stt/live", additional_headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, ) as socket: socket.send(json.dumps({ "type": "start", "model": "paxa-stt-lite-realtime-v1-preview", "audio": {"encoding": "pcm_s16le", "sample_rate": 16000}, "language": "th", })) # 16 kHz signed 16-bit mono is 32,000 bytes a second; a file has to # be paced the way a microphone paces itself. with open("caller.pcm", "rb") as audio: while frame := audio.read(640): # 20 ms socket.send(frame) time.sleep(0.02) socket.send(json.dumps({"type": "end"})) for message in socket: event = json.loads(message) if event["type"] == "transcript" and event["is_final"]: print(event["turn"], event["text"]) elif event["type"] == "done": break ``` Server events: ```json {"type":"started","connection":"7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a","model":"paxa-stt-lite-realtime-v1-preview"} {"type":"speech_started","turn":1,"time":0.3} {"type":"speech_ended","turn":1,"time":12.252} {"type":"charged","seq":1,"seconds":12.252,"credits":2.56,"total_credits":2.56} {"type":"transcript","turn":1,"is_final":true,"text":"สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ","start":0.3,"end":12.252} {"type":"done","total_seconds":12.252,"turns":1,"total_credits":2.56} ``` # Transcribe a recording (OpenAI-compatible) `POST https://api.paxalabs.com/v1/audio/transcriptions` POST /v1/audio/transcriptions serves the OpenAI audio.transcriptions request shape. The recording rides as a multipart file part, and response_format returns JSON, plain text, SubRip, or WebVTT. Point an OpenAI SDK at `https://api.paxalabs.com/v1` and call `audio.transcriptions.create`. Pricing, ceilings, and refunds match [POST /v1/stt](https://paxalabs.com/docs/api/stt), which is also where the transcript conventions, speaker turns, word timing, and the vocabulary live: this alias reads none of them. The [OpenAI compatibility guide](https://paxalabs.com/docs/openai-compatibility) covers the field mapping. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. - `idempotency-key` (string, optional, 1 to 200 characters, pattern ^[!-~]+$): Makes a retry safe: two requests carrying the same key are charged once. One request per key runs at a time. A concurrent duplicate answers 409 idempotency_in_flight. Reuse a key only to retry an identical request. A changed payload is rejected with 422. Accepts up to 200 printable ASCII characters other than space. See authentication at https://paxalabs.com/docs/authentication. ## Body - `file` (file, required): The recording, as a file part. MP3, WAV (PCM), FLAC, Ogg (Opus or Vorbis), M4A, AAC (ADTS), or WebM; the format and the length are read from the bytes. The part's filename and content type are ignored. Same ceilings, pricing, and refusals as the audio field on POST /v1/stt: up to 60 minutes and 26,214,400 bytes. - `model` (string, required, 1 to 100 characters, example "paxa-stt-lite-v1-preview"): Transcription model id, for example paxa-stt-lite-v1-preview. OpenAI's whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe, gpt-4o-mini-transcribe-2025-03-20, gpt-4o-mini-transcribe-2025-12-15 are accepted and run on paxa-stt-lite-v1-preview. - `language` (string, optional, up to 16 characters, pattern ^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$, example "th"): The same field OpenAI defines. A BCP 47 tag the recording is expected to be in, such as "th". A hint that filters nothing. Omitted, the model detects the language itself. - `response_format` ("json" or "verbose_json" or "text" or "srt" or "vtt", optional, default "json"): What the response body carries. "json" (the default) answers application/json with the transcript in a text field. "verbose_json" answers OpenAI's verbose shape: the duration, the text, and the segments and words that timestamp_granularities names. "text" answers text/plain with the transcript alone. "srt" and "vtt" answer the subtitle file, cut the same way POST /v1/stt cuts it, at the default line length. Every format is billed the same, by the recording's length. - `timestamp_granularities` (array of "segment" or "word"s, optional): The same field OpenAI defines, sent as one timestamp_granularities[] form field per value, and read only with verbose_json. "segment" fills segments and "word" fills words; omitted, segments alone are filled. - `stream` ("false", optional): The OpenAI field of the same name. Streamed transcript events are not served here: "true" answers validation, and the transcript arrives as one body. WS /v1/stt/live transcribes as the audio arrives. ## Response - `text` (string, required): The transcript, verbatim in the language spoken. Empty when the recording carried no speech. - `language` (string, optional): The language hint the request sent, named as OpenAI names languages, such as "thai", or an empty string when it sent none. Carried by verbose_json. - `duration` (number, optional): The recording's length in seconds, carried by verbose_json. - `segments` (array of objects, optional): The transcript cut into the cues POST /v1/stt cuts for subtitles, carried by verbose_json. OpenAI's decoding statistics are not measured. Their fields carry 0 and an empty tokens list, for clients that require OpenAI's full segment shape. - `segments[].id` (integer, required): The segment's position, from 0. - `segments[].seek` (integer, required): Always 0. - `segments[].start` (number, required): Start, in seconds. - `segments[].end` (number, required): End, in seconds. - `segments[].text` (string, required): The segment's text, as the transcript renders it. - `segments[].tokens` (array of integers, required): Always empty. - `segments[].temperature` (number, required): Always 0. - `segments[].avg_logprob` (number, required): Always 0. - `segments[].compression_ratio` (number, required): Always 0. - `segments[].no_speech_prob` (number, required): Always 0. - `words` (array of objects, optional): One entry per word, carried by verbose_json when timestamp_granularities names "word". - `words[].word` (string, required): The word, with Thai segmented. - `words[].start` (number, required): Start, in seconds. - `words[].end` (number, required): End, in seconds. ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `unknown_model` (400): The model field does not name a served model. - `audio_invalid` (400): The audio field could not be read as an MP3, WAV, FLAC, Ogg, M4A, AAC, or WebM recording, or its length could not be read from the container. A damaged or truncated file answers this code. Nothing was charged. - `audio_too_large` (413): The decoded recording exceeds the model's size ceiling. Nothing was charged. - `audio_too_long` (400): The recording is longer than the model's per-request ceiling. Nothing was charged. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `idempotency_in_flight` (409): Another request with this Idempotency-Key is in flight right now. - `idempotency_refunded` (409): The original request under this Idempotency-Key failed and was refunded. - `idempotency_mismatch` (422): This Idempotency-Key was already used for a different request. - `content_blocked` (422): The upstream safety system declined to process this content. The charge was refunded. - `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example curl: ```bash # The recording rides as a file part; nothing is base64 encoded. curl -X POST https://api.paxalabs.com/v1/audio/transcriptions \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -F file=@meeting.m4a \ -F model=paxa-stt-lite-v1-preview \ -F response_format=srt \ -o meeting.srt ``` TypeScript: ```typescript import { createReadStream } from "node:fs"; import { writeFile } from "node:fs/promises"; import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.paxalabs.com/v1", apiKey: process.env.PAXA_API_KEY, }); const subtitles = await client.audio.transcriptions.create({ file: createReadStream("meeting.m4a"), model: "paxa-stt-lite-v1-preview", response_format: "srt", }); await writeFile("meeting.srt", subtitles); ``` Python: ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.paxalabs.com/v1", api_key=os.environ["PAXA_API_KEY"], ) with open("meeting.m4a", "rb") as file: subtitles = client.audio.transcriptions.create( file=file, model="paxa-stt-lite-v1-preview", response_format="srt", ) with open("meeting.srt", "w") as out: out.write(subtitles) ``` # Read a document `POST https://api.paxalabs.com/v1/ocr` POST /v1/ocr reads a PDF or image and returns its text as Markdown or typed layout blocks, one entry per page. Charged in credits per page before reading, refunded on failure. Send the file as base64 in `document`. An image counts as one page. The [OCR guide](https://paxalabs.com/docs/ocr) covers the two output shapes, the block types, and the page and size limits. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. - `idempotency-key` (string, optional, 1 to 200 characters, pattern ^[!-~]+$): Makes a retry safe: two requests carrying the same key are charged once. One request per key runs at a time. A concurrent duplicate answers 409 idempotency_in_flight. Reuse a key only to retry an identical request. A changed payload is rejected with 422. Accepts up to 200 printable ASCII characters other than space. See authentication at https://paxalabs.com/docs/authentication. ## Body - `document` (string, required, 1 to 41,000,000 characters): The document to read, as the base64 encoding of a PDF, PNG, JPEG, or WebP file. An image counts as one page. Cost is 6.5 credits per page, charged before reading and refunded automatically when reading fails. A PDF may carry up to 50 pages, and more answers 400 too_many_pages. The decoded file may be up to 10,485,760 bytes, and larger answers 413 document_too_large. A file that cannot be read as one of these formats answers 400 document_invalid, and a PDF that needs a password to open answers 400 document_password_required. GET /v1/models reports the ceilings as max_pages and max_bytes. - `model` (string, required, 1 to 100 characters, example "paxa-ocr-lite-v1"): OCR model id, for example paxa-ocr-lite-v1. GET /v1/models lists the served catalog. - `output` ("markdown" or "structured", optional, default "markdown"): Shape of the result. "markdown", the default, returns each page as GitHub-flavored Markdown. "structured" returns each page as typed layout blocks, for callers that feed the reading into a pipeline. ## Response - `pages` (array of objects, required): One entry per page of the document, in page order. - `pages[].page` (integer, required): Page number, starting at 1. An image request has exactly one page. - `pages[].markdown` (string, optional): The page's content as GitHub-flavored Markdown, in reading order. Present when output is "markdown". Join pages with a blank line to rebuild the document. - `pages[].blocks` (array of objects, optional): The page's content as typed blocks in reading order. Present when output is "structured". - `pages[].blocks[].type` (string, required): What the block is: "heading", "paragraph", "list", "table", or "figure". Blocks arrive in reading order. - `pages[].blocks[].text` (string, optional): The block's text. Present on "heading", "paragraph", and "figure" blocks. A figure's text is its caption or nearby label, and is empty when it has none. - `pages[].blocks[].level` (integer, optional): Heading depth, starting at 1 for the most prominent. Present on "heading" blocks. - `pages[].blocks[].items` (array of strings, optional): The list entries in order. Present on "list" blocks. - `pages[].blocks[].rows` (array of array of stringss, optional): The table cells as rows of column values, first row first. Present on "table" blocks. - `usage` (object, required): What the request was billed for. - `usage.pages` (integer, required): Pages this request was billed for. - `usage.credits` (number, required): What this delivery cost, in credits, exact to a hundredth. An idempotent replay reports the ORIGINAL request's charge, since that one charge is what paid for this delivery too; your balance moves only once. ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `unknown_model` (400): The model field does not name a served model. - `document_invalid` (400): The document field could not be read as a PDF, PNG, JPEG, or WebP file. A damaged or truncated PDF answers this code. Nothing was charged. - `document_password_required` (400): The PDF needs a password to open. A PDF that carries permissions-only encryption, the kind that opens without being asked for a password, is read normally. Nothing was charged. - `document_too_large` (413): The decoded document exceeds the model's size ceiling. Nothing was charged. - `too_many_pages` (400): The document has more pages than the model's per-request ceiling. Nothing was charged. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `idempotency_in_flight` (409): Another request with this Idempotency-Key is in flight right now. - `idempotency_refunded` (409): The original request under this Idempotency-Key failed and was refunded. - `idempotency_mismatch` (422): This Idempotency-Key was already used for a different request. - `content_blocked` (422): The upstream safety system declined to process this content. The charge was refunded. - `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example curl: ```bash # Encode without line wrapping: wrapped base64 breaks the JSON string. DOC=$(base64 < invoice.pdf | tr -d '\n') # --max-time covers a multi-page document; curl defaults to no limit. curl -X POST https://api.paxalabs.com/v1/ocr \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"document\": \"$DOC\", \"model\": \"paxa-ocr-lite-v1\"}" ``` TypeScript: ```typescript import { readFile } from "node:fs/promises"; const document = (await readFile("invoice.pdf")).toString("base64"); const response = await fetch("https://api.paxalabs.com/v1/ocr", { method: "POST", headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ document, model: "paxa-ocr-lite-v1" }), // A multi-page document can run for minutes; give it room. signal: AbortSignal.timeout(300_000), }); if (!response.ok) throw new Error(`OCR failed: ${response.status}`); const { pages, usage } = await response.json(); console.log(pages[0].markdown, `${usage.credits} credits`); ``` Python: ```python import base64 import os import requests with open("invoice.pdf", "rb") as file: document = base64.b64encode(file.read()).decode() response = requests.post( "https://api.paxalabs.com/v1/ocr", headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, json={"document": document, "model": "paxa-ocr-lite-v1"}, # A multi-page document can run for minutes; give it room. timeout=300, ) response.raise_for_status() body = response.json() print(body["pages"][0]["markdown"], body["usage"]["credits"], "credits") ``` Response: ```json { "pages": [ { "page": 1, "markdown": "# ใบเสร็จรับเงิน\n\nร้านข้าวแกงบ้านสวน สาขาสีลม\n\n| รายการ | จำนวน | ราคา |\n| --- | --- | --- |\n| ข้าวแกงเขียวหวานไก่ | 1 | 60 |\n| น้ำเปล่า | 1 | 10 |\n\nรวมทั้งสิ้น 70 บาท" } ], "usage": { "pages": 1, "credits": 6.5 } } ``` # Extract fields from a document `POST https://api.paxalabs.com/v1/extract` POST /v1/extract reads a PDF or image and fills in a schema of typed fields, each value the span printed in the document or null with the reason. Charged in credits per page at a rate the schema's leaf count selects, before reading, refunded on failure. Send the file as base64 in `document` and the fields as `schema`. An image counts as one page. The [Document Extraction guide](https://paxalabs.com/docs/extraction) covers the schema dialect, every leaf type, the leaf-counting rule, and how to read `missing`, `unverified`, `truncated`, and `assumed`. A schema outside the dialect answers [400 schema_invalid](https://paxalabs.com/docs/errors#schema_invalid) with `path` and `reason`, uncharged. ## Headers - `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended). - `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present. - `idempotency-key` (string, optional, 1 to 200 characters, pattern ^[!-~]+$): Makes a retry safe: two requests carrying the same key are charged once. One request per key runs at a time. A concurrent duplicate answers 409 idempotency_in_flight. Reuse a key only to retry an identical request. A changed payload is rejected with 422. Accepts up to 200 printable ASCII characters other than space. See authentication at https://paxalabs.com/docs/authentication. ## Body - `document` (string, required, 1 to 41,000,000 characters): The document to extract from, as the base64 encoding of a PDF, PNG, JPEG, or WebP file. An image counts as one page. Cost is 13 credits per page for a schema of up to 50 leaf fields and 19.5 credits per page beyond that, charged before extraction and refunded automatically when extraction fails. A PDF may carry up to 20 pages, and more answers 400 too_many_pages. The decoded file may be up to 10,485,760 bytes, and larger answers 413 document_too_large. A file that cannot be read as one of these formats answers 400 document_invalid, and a PDF that needs a password to open answers 400 document_password_required. Send one document per request. A file holding several documents gets one object, with no promise which document it describes. GET /v1/models reports the ceilings as max_pages, max_bytes, and max_leaves. - `model` (string, required, 1 to 100 characters, example "paxa-doc-extract-v1"): Extraction model id, for example paxa-doc-extract-v1. The id is the contract version: the dialect, the response shape, and the leaf-counting rule are fixed under it, and a change ships as a new id served beside it. GET /v1/models lists the served catalog. - `schema` (object, required): The fields to extract, as an object with one key, fields, mapping each field name to its definition. A definition carries a type, an optional required flag, an optional description of up to 200 characters the model reads, and the keys its type offers. Leaf types: "string", "integer", "number", "date", "time", "enum", "id", "thai_id", "email", "phone", "postal_code", "province", "bank", "insurer", "card_scheme", "payment_method", "legal_form", "currency", "unit", "amount_words". Containers: "object" with its own fields, and "array" with items (a leaf or an object) and a required max_items of 1 to 200. Names are 1 to 64 characters of letters, digits, and underscores. At most 3 containers sit below the root. An array holds leaves or objects, and an array inside an array is refused. The schema's leaf count is every leaf once plus every leaf inside an array once per element the array is sized for, and it must stay within the model's max_leaves. A schema outside the dialect answers 400 schema_invalid with the offending path and a reason, uncharged. A serialized schema over 131,072 bytes answers the same code with reason too_large. The Document Extraction guide is the full reference for every type, its keys, and its delivered form. - `include_pages` (boolean, optional, default false): true adds a pages array to the response: the text read from each page as GitHub-flavored Markdown, the reading the fields were extracted from, at no extra charge. Omitted, the response carries the fields alone. - `include_evidence` (boolean, optional, default false): true adds an evidence object to the response, mapping each field path to the printed span its value was read from, whether or not a value could be read from it. Costs nothing extra. Omitted, the response carries no spans. ## Response - `status` ("complete" or "incomplete", required): "complete" when every required field carries a value, "incomplete" otherwise. Computed from the fields alone. A file that is not what the schema describes comes back "incomplete" with most required fields in missing, which is the wrong-document signal. - `fields` (object, required): Your schema's tree with plain values. A string field carries the printed text and a number or integer field a number. A date is ISO YYYY-MM-DD and a time is 24-hour HH:MM, with seconds when printed. An enum carries one listed value, and a closed-set field the form its format asked for. Null wherever nothing was read. Every value is a span printed in the document, or null. Nothing is inferred, computed, or looked up. - `missing` (array of strings, required): Every required field delivered as null, for any reason. - `unverified` (array of objects, required): Every field delivered as null because its span could not be read as the field's type, each with its reason. Empty when every value stood. - `unverified[].path` (string, required): The field delivered as null. Paths are in dot form, array elements indexed: "items[2].amount". - `unverified[].reason` (string, required): Why the span could not stand as the field's value: one of not_in_readout, not_as_printed, no_number, ambiguous_number, not_an_integer, no_date, ambiguous_date, no_time, ambiguous_time, not_an_enum_value, ambiguous_match, not_13_digits, bad_checksum, wrong_digit_count, not_an_email, not_a_phone, not_a_postal_code, no_amount_words, below_min, above_max, too_many_decimals, too_short, too_long, pattern_mismatch, before_min, after_max, in_the_future. The Document Extraction reference states each one. - `truncated` (array of strings, required): Every array whose document carried more elements than it was sized for. The first max_items elements are in fields, in reading order. Raise max_items to read the rest. Empty when every array fit. - `assumed` (array of objects, required): Every date whose year was printed with two digits, with the digits and the year delivered. Always present and empty when nothing was assumed. A caller who would rather decide the era reads it here. - `assumed[].path` (string, required): The date field. Paths are in dot form, array elements indexed: "items[2].amount". - `assumed[].printed` (string, required): The two digits of the year as printed, for example "69". - `assumed[].read_as` (integer, required): The four-digit year delivered in the field, in the Common Era. - `evidence` (object, optional): Field path to the printed span the value was read from, for every field whose span the reading carries. Present when the request set include_evidence to true. - `pages` (array of objects, optional): One entry per page of the document, in page order. The fields were extracted from this reading. Present when the request set include_pages to true. - `pages[].page` (integer, required): Page number, starting at 1. An image request has exactly one page. - `pages[].markdown` (string, required): The page's content as GitHub-flavored Markdown, in reading order. The fields were read from this text. - `usage` (object, required): What the request was billed for. - `usage.pages` (integer, required): Pages this request was billed for. - `usage.leaves` (integer, required): The schema's leaf count, which decides the per-page rate: up to 50 bills at credits_per_page, more at large_schema_credits_per_page. - `usage.credits` (number, required): What this delivery cost, in credits, exact to a hundredth. An idempotent replay reports the ORIGINAL request's charge, since that one charge is what paid for this delivery too; your balance moves only once. ## Errors - `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. - `unknown_model` (400): The model field does not name a served model. - `schema_invalid` (400): The extraction schema is outside the dialect. The problem body carries two extra fields: path, the offending field in dot form (empty for the schema as a whole), and reason, one stable code from the schema reasons list. Nothing was charged. - `document_invalid` (400): The document field could not be read as a PDF, PNG, JPEG, or WebP file. A damaged or truncated PDF answers this code. Nothing was charged. - `document_password_required` (400): The PDF needs a password to open. A PDF that carries permissions-only encryption, the kind that opens without being asked for a password, is read normally. Nothing was charged. - `document_too_large` (413): The decoded document exceeds the model's size ceiling. Nothing was charged. - `too_many_pages` (400): The document has more pages than the model's per-request ceiling. Nothing was charged. - `unauthorized` (401): The request carried no API key, or the key is invalid or disabled. - `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged. - `key_limit` (403): This API key reached its spending cap. Nothing was charged. - `idempotency_in_flight` (409): Another request with this Idempotency-Key is in flight right now. - `idempotency_refunded` (409): The original request under this Idempotency-Key failed and was refunded. - `idempotency_mismatch` (422): This Idempotency-Key was already used for a different request. - `content_blocked` (422): The upstream safety system declined to process this content. The charge was refunded. - `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. - `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. - `internal` (500): Request state was inconsistent on the server. - `provider_error` (502): Model inference failed after the request was charged. - `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged. ## Example curl: ```bash # Encode without line wrapping: wrapped base64 breaks the JSON string. DOC=$(base64 < invoice.pdf | tr -d '\n') # --max-time covers a multi-page document; curl defaults to no limit. curl -X POST https://api.paxalabs.com/v1/extract \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d @- < and marks play where they are written. Its 50 voices each speak English, Thai, and Mandarin, at the 10 credits per 1,000 characters of Paxa TTS Flash. ## Speech costs a third less 2026-10-01 · Text to speech Paxa TTS Flash now costs 10 credits per 1,000 characters, which is $10 per million, down from 15 credits. The new rate applies to every speech request and to the Speech tool in the console, and timestamps and live streaming fall in the same proportion. Credits already in a balance or a plan now voice 50 percent more text. ## A language setting for speech 2026-10-01 · Text to speech A new language setting lets the voice read Thai, English, and Mandarin Chinese text more accurately. Set it to the language your text is written in, and numbers, symbols, and codes such as AB2039 are read to suit that language. Auto, the default, works it out from the text. The setting works on POST /v1/tts, live streaming, and the Speech tool in the console, and does not change the charge. ## Feedback through the API 2026-09-28 · Platform POST /v1/feedback reports a wrong output, an API problem, or a platform issue, tied to the x-request-id values it names. GET /v1/feedback lists the account's reports with their status and the team's note. A rating on one request is a complete report, and feedback costs no credits. ## Emotion tags on two experimental voices 2026-09-25 · Text to speech Khanom Chan and Kaprao join Paxa TTS Flash as experimental voices that read emotion tags written into the text, bringing the roster to 34. There are 26 tags, such as [happy], [sad], and [furious], each with an optional weight from 0 to 1, as in [sad:0.5]. Tag characters bill as text, every other voice skips the tags, and GET /v1/voices adds experimental and emotion_tags. ## Realtime transcription with turn detection 2026-09-23 · Speech to text Paxa STT Lite Realtime (paxa-stt-lite-realtime-v1-preview) transcribes speech as it is spoken over WS /v1/stt/live and detects the end of each turn itself. Built for voice agents: `speech_started` arrives when the user begins talking, `speech_ended` and one final `transcript` when the turn is over. 16-bit PCM or telephony mu-law in, the language hint, style, convention, vocabulary, and word timestamps of POST /v1/stt fixed per connection. Pricing is 12.5 credits per minute of audio received, silence included, charged as the conversation runs. ## Mandarin Chinese and six new voices 2026-09-14 · Text to speech Paxa TTS Flash now reads Mandarin Chinese in Simplified script beside Thai and English. The roster grows to 32 voices with Tao Huay and Oolong in Mandarin, Espresso and Mocha in English, and Khao Niao and Tom Yum in Thai. The language field of GET /v1/voices adds "zh". ## Document Extraction fills a schema from a page 2026-09-12 · OCR Paxa Document Extraction (paxa-doc-extract-v1) reads a PDF or image through POST /v1/extract and returns the schema of typed fields you send, filled in. Every value is the span printed on the page or null with the reason. 13 credits per page for a schema of up to 50 leaf fields and 19.5 for a larger one, the reading included. Charged before reading and refunded automatically on failed requests. ## Subtitles come back as a file 2026-09-10 · Speech to text POST /v1/stt takes `subtitles`, `"srt"` or `"vtt"`, and returns the whole file. Cues break at a speaker change, at sentence punctuation, and at a pause, then at 60 characters over at most two lines, and `subtitle_line_chars` sets your own figure. Every line break falls on a word boundary. A Thai caption never splits a word. The same request also works from an OpenAI SDK: POST /v1/audio/transcriptions takes the recording as a file part and answers json, text, srt, or vtt. Neither costs anything extra. ## Transcription is live as a research preview 2026-09-09 · Speech to text Paxa STT Lite (paxa-stt-lite-v1-preview) transcribes Thai and English recordings through POST /v1/stt, verbatim in the language spoken. Word timestamps cover whole Thai words. Speaker turns are numbered from 0. The style is verbatim or clean, the convention spoken or written. The language is detected unless a hint is sent. A vocabulary of up to 50 terms combines with every other option. Pricing is 8.33 credits per minute of audio, charged first and refunded automatically on a failed request. ## MCP server for agents 2026-09-06 · Text to speech @paxalabs/mcp gives Claude Code, Claude Desktop, Cursor, and other MCP clients the API as tools: speech through the machine's speakers, speech to file, translation into Thai, and OCR. A paxa say hook speaks up when Claude Code needs you. ## Interrupt a live reply 2026-09-01 · Text to speech A cancel frame on /v1/tts/live stops the leg being spoken and drops whatever text is still buffered. An assistant can stop the moment the speaker talks over it. The stopped leg keeps the charge its receipt reported; the discarded buffer was never charged. ## The OCR API ships 2026-08-24 · OCR Paxa OCR Lite (paxa-ocr-lite-v1) reads PDFs and images through POST /v1/ocr, as Markdown or typed layout blocks, at 6.5 credits per page with charge-first billing and automatic refunds on failed requests. ## Glossaries stop costing what they used to 2026-08-21 · Translation Send a whole glossary. Terms whose source does not appear in the text are dropped before the request runs. They are never sent to the model and never billed. Context, examples, and glossaries now cost 8 credits per 1,000 characters, down from the translation rate, and the ceilings rise to 5,000 glossary terms and 200,000 billable characters per request. A new instructions field takes free-form direction the fixed options cannot express. ## Bigger requests, one billing rule 2026-08-12 · Translation Every character of a request is now billed at the one rate, context, examples, and glossary included. The field limits open up in return: 200 segments, 20,000 characters of text, 1,000 glossary terms, and 40,000 billable characters per request. ## The translation API ships 2026-08-10 · Translation Paxa Translate Lite (paxa-translation-lite-v1) translates 14 pinnable source languages, plus auto-detect, into Thai, with formality, glossaries, borrowed-word control, and document context. ## Timestamps and live streaming 2026-08-09 · Text to speech Requests can return word, sentence, or utterance timing, and the /v1/tts/live websocket streams speech while your text is still arriving. ## The speech API ships 2026-08-03 · Text to speech Paxa TTS Flash (paxa-tts-flash-v1) opens to the public through POST /v1/tts: Thai and English speech with charge-first billing and automatic refunds on failed requests. > Dates are ship dates. Anything not listed here is documented in the guides as it has always behaved. # Temperature and pause tags on Paxa TTS Flash v2 Paxa TTS Flash v2 now takes a temperature that keeps a reading steadier or lets it vary more, and pause tags that put a set silence after any word. Published 2026-10-28. Paxa Labs. The same line in the same voice used to come back paced a little differently on every request. A narrator can use that variety. A phone menu, an app prompt that plays thousands of times, or a line rendered again to match an edit needs a read close to the one the team approved. Two controls arrive on Paxa TTS Flash v2 today. Temperature sets how much a reading varies from one request to the next. Pause tags put a set length of silence where you write them. Both work in the console and on the API. One line read by Foi Thong, twice at 0, 0.5, and 2: - Temperature 0, take 1, [Foi Thong](https://paxalabs.com/voices/foithong), Narrator: https://paxalabs.com/audio/directions/temperature/foithong-t0-s1-0-1.m4a - Temperature 0, take 2, [Foi Thong](https://paxalabs.com/voices/foithong), Narrator: https://paxalabs.com/audio/directions/temperature/foithong-t0-s2-0-2.m4a - Temperature 0.5, take 1, [Foi Thong](https://paxalabs.com/voices/foithong), Narrator: https://paxalabs.com/audio/directions/temperature/foithong-default-s1-0.5-1.m4a - Temperature 0.5, take 2, [Foi Thong](https://paxalabs.com/voices/foithong), Narrator: https://paxalabs.com/audio/directions/temperature/foithong-default-s2-0.5-2.m4a - Temperature 2, take 1, [Foi Thong](https://paxalabs.com/voices/foithong), Narrator: https://paxalabs.com/audio/directions/temperature/foithong-t2-s1-2-1.m4a - Temperature 2, take 2, [Foi Thong](https://paxalabs.com/voices/foithong), Narrator: https://paxalabs.com/audio/directions/temperature/foithong-t2-s2-2-2.m4a ## Temperature Temperature runs from 0 to 2 in steps of 0.1, and 0.5 is the default. It changes the pacing, pauses, breaths, and melody of a reading from one request to the next. A request that leaves it out reads at 0.5, which varies less than requests did before. - At `0`, a reading varies least. The same text in the same voice comes back closest in timing and melody from one request to the next, and two takes can still differ. - At `0.5`, the default, a reading varies a little between requests. Leaving the field out is the same request as sending `0.5`. - At `2`, a reading varies the most. ### Temperature with a direction A direction sets the feeling, and temperature sets how much that reading varies. Kaprao reads a line under `[sad]` and another under `[angry]`, each twice at 0 and twice at 2. Two lines read by Kaprao under [sad] and [angry], each twice at 0 and 2: - [sad] at temperature 0, take 1, [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/directions/temperature/kaprao-sad-t0-s1-0-1.m4a - [sad] at temperature 0, take 2, [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/directions/temperature/kaprao-sad-t0-s2-0-2.m4a - [sad] at temperature 2, take 1, [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/directions/temperature/kaprao-sad-t2-s1-2-1.m4a - [sad] at temperature 2, take 2, [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/directions/temperature/kaprao-sad-t2-s2-2-2.m4a - [angry] at temperature 0, take 1, [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/directions/temperature/kaprao-angry-t0-s1-0-1.m4a - [angry] at temperature 0, take 2, [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/directions/temperature/kaprao-angry-t0-s2-0-2.m4a - [angry] at temperature 2, take 1, [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/directions/temperature/kaprao-angry-t2-s1-2-1.m4a - [angry] at temperature 2, take 2, [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/directions/temperature/kaprao-angry-t2-s2-2-2.m4a A low temperature suits a directed line that plays many times, such as an app prompt or a game character's reply. A high one gives a scene several different takes to choose from. In the console, the Speech tool shows Temperature as a slider under Speed, marked More stable at 0 and More creative at 2. Between them it reads Default at 0.5 and offers a Reset button anywhere else. A deep link or a shared link keeps the value. On the API, `temperature` is a field of POST /v1/tts, of [the OpenAI-compatible speech endpoint](https://paxalabs.com/docs/openai-compatibility), and of the start frame on [a live connection](https://paxalabs.com/docs/live-streaming). A request costs the same at every temperature. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "temperature": 0 }' \ --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: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", voice: "khanomkrok", model: "paxa-tts-flash-v2", temperature: 0, }), }); 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": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs", "voice": "khanomkrok", "model": "paxa-tts-flash-v2", "temperature": 0}, ) response.raise_for_status() with open("speech.mp3", "wb") as f: f.write(response.content) ``` Paxa TTS Flash v2 reads temperature. Paxa TTS Flash keeps its own variation and accepts only a value that rounds to `0.5`. Any other answers `400 validation` before anything is charged. [The temperature guide](https://paxalabs.com/docs/temperature) covers designed voices and retries under an Idempotency-Key. ## Pause tags A pause tag adds silence after the word before it. Write a named length, `[short pause]`, `[pause]`, or `[long pause]`, or an exact one such as `[pause 300ms]`. Every Paxa TTS Flash v2 voice reads them, and Paxa TTS Flash reads none. One line read by Nom Yen, with each pause: - No pause, [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/directions/winner/nomyen-plain.m4a - [short pause], [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/directions/winner/nomyen-short.m4a - [pause], [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/directions/winner/nomyen-pause.m4a - [long pause], [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/directions/winner/nomyen-long.m4a - [pause 500ms], [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/directions/winner/nomyen-exact.m4a A named pause takes its length from the language of the word before the tag. | Language | `[short pause]` | `[pause]` | `[long pause]` | | --- | --- | --- | --- | | Thai | 130 ms | 180 ms | 460 ms | | Chinese | 100 ms | 140 ms | 360 ms | | English and other languages | 120 ms | 170 ms | 460 ms | - `[pause 300ms]`, `[pause 0.3s]`, and `[pause:300]` are the same pause. A number with no unit is milliseconds. - An exact length rounds to the nearest 10 ms, up to 500 ms. A longer one, such as `[pause 1s]`, plays at 500 ms. - A pause adds to the gap the reading already leaves after that word. After a full stop, the same tag makes a longer silence than inside a sentence. - A pause keeps its length at every speed. Pause tags are text. Their characters count toward the request's length and its charge like any other. [The pause guide](https://paxalabs.com/docs/pauses) has the rest, including live connections. ## Also in this release `[emphasis]` now reads as a direction, like `[whispering]` or `[excited]`. ## Questions ### Does temperature change the price? A request costs the same at every temperature, from 0 to 2. ### How close are two takes at 0? Closer than at any other setting, in timing, pauses, and melody. They are not guaranteed to match. ### Can a pause run longer than 500 ms? One tag plays at most 500 ms. A longer tag plays at 500 ms and is not refused. # How to describe a voice for AI voice design, with examples What to put in a voice description, weak and stronger examples, how to describe a Thai or Mandarin voice, and how to change a voice in words once you hear it. Published 2026-10-23. Paxa Labs. A voice design tool turns a sentence about a voice into the voice. The sentence is the whole brief. What it says and what it leaves out decide what you hear back. The examples here were written for [Voice design](https://paxalabs.com/app/voice-design) in the Paxa console, where each design or change costs 5 credits. Advice from other vendors links their own pages, as we read them in October 2026. ## What to put in a description Most descriptions that work cover four things: who is speaking, how the voice sounds, how the person speaks, and where the words will be heard. Name a language too when the voice should speak one other than the language you write in. | Part | Words that carry it | | --- | --- | | Who is speaking | Age, gender, and role, such as a pharmacist in her thirties, a teenage gamer, or a retired sea captain | | How the voice sounds | Pitch and texture, such as low, bright, husky, breathy, clear, or gravelly | | How they speak | Pace, energy, and manner, such as slow and unhurried, quick and excited, or calm enough to settle a nervous passenger | | Where it will be heard | A late night audiobook, a game lobby, a store announcement, a bedtime story | | Language | English, Thai, or Mandarin, named outright | One or two sentences is enough. [Google's voice design guide](https://ai.google.dev/gemini-api/docs/voice-design) asks for one to two clear sentences, and [the ElevenLabs guide](https://elevenlabs.io/docs/creative-platform/voices/voice-design) opens its template on the language before anything else. Paxa takes up to 1,000 characters, but a short description that names the parts above does more than a long one that circles them. ## Weak and stronger descriptions A weak description names a category. A stronger one names a person in a moment, and the voice follows the person. | Weak | Stronger | | --- | --- | | A female voice | A woman in her thirties who hosts a cooking show, warm and lively, and laughs easily. | | A scary voice | A villain who speaks slowly and quietly, almost a whisper, and sounds pleased when the listener is afraid. | | A news voice | An evening news anchor in her forties, clear, formal, and steady, who reads each headline at an even pace. | | An excited voice | A football commentator in his thirties, fast and loud, who gets more excited with every attack. | [The voice design page](https://paxalabs.com/voice-design) plays descriptions like these beside the voices they made, each with the line the voice spoke first. ## Describing a Thai or Mandarin voice Name the language the voice should speak. A description can be written in English, Thai, or Chinese. The voice speaks the language the description names. An English description of a Bangkok tour guide who speaks Thai gets a Thai voice. A description that names no language gets a voice in the language it is written in. A setting then does the work a list of traits would. A vendor at a Bangkok night market, a kindergarten teacher reading to her class, and a morning market briefing each tell the voice how to speak in a few words. These three were written in Thai and Chinese: - แม่ค้าส้มตำหน้าตลาด วัยสี่สิบกว่า เสียงดัง อารมณ์ดี ทักลูกค้าเป็นกันเอง (a som tam vendor in her forties, loud and cheerful, greeting customers like friends) - ผู้ชายวัยสามสิบ อ่านสรุปข่าวตลาดหุ้นตอนเช้าเป็นภาษาอังกฤษ ชัด กระชับ ฟังสบาย (a man in his thirties reading the morning market summary in English, clear, brief, and easy to hear) - 幼儿园女老师,用普通话给孩子们读绘本,温柔又有耐心。 (a kindergarten teacher reading a picture book in Mandarin, gentle and patient) ## What to leave out - Sound effects and recording conditions. [ElevenLabs](https://elevenlabs.io/docs/creative-platform/voices/voice-design) lists words such as reverb and phone among its pitfalls. Add effects in your editor after the voice is made. - A real person's name. [OpenAI's custom voice guide](https://developers.openai.com/api/docs/guides/custom-voices) keeps named people out of voice prompts. In Paxa, a description that names someone gets an original voice under a name the tool makes up. Describe the qualities you want instead. - Traits that pull against each other, such as calm and frantic at once. Pick the one the script needs most. - Instructions about what the voice should say. [OpenAI's guide](https://developers.openai.com/api/docs/guides/custom-voices) keeps behavior rules out of the voice prompt. The script decides the words; the description decides the voice. ## Change a voice in words The first voice is a draft. Listen to its line, then type one change, such as warmer, a little older, or slower, and hear the next version. [OpenAI's guide](https://developers.openai.com/api/docs/guides/custom-voices) also recommends refining one quality at a time against the same passage, which keeps it clear what each change did. In Paxa, each change keeps the rest of the voice as it was. Save the version you want, and it joins the Speech tool under Your voices with an id for the API. Speech in a saved voice costs 1.25 times the rate of a catalog voice. ## Common questions ### How long should a voice description be? One or two sentences that name who is speaking and how they sound. Paxa accepts up to 1,000 characters. ### Can I describe a voice in Thai? Yes. Write the description in English, Thai, or Chinese. The voice speaks the language the description names, or the one it is written in when it names none. ### What does a design cost? 5 credits for each design or change, its first line included. A new account's free credits cover several, with no card. ### Can I describe a voice like a celebrity's? Describe the qualities you want to hear. A description that names a real person gets an original voice built from what the name suggests, under a made-up name. # Voice design vs voice cloning for a voice of your own Voice cloning copies a person from recordings; voice design makes a voice from a written description. What each needs, when each fits, consent, and what a design costs. Published 2026-10-23. Paxa Labs. A voice that is not in any catalog can come from two places. Voice cloning copies a real person's voice from recordings of them speaking. Voice design makes a voice from a written description, with no recording at all. Every vendor fact below links that vendor's own page, as we read it in October 2026. ## The difference at a glance | | Voice cloning | Voice design | | --- | --- | --- | | Starts from | Recordings of one person speaking | A sentence or two about the voice | | Needs | Clean audio and the speaker's consent | Nothing to record | | Gives you | A voice that sounds like that person | A voice that fits the description | | To change it | Record again | Describe the change | ## When cloning fits Clone when the voice has to be one particular person's: your own voice on your own channel, or a narrator who has agreed to lend theirs. The clone is only as good as the recording behind it. [Fish Audio's cloning docs](https://docs.fish.audio/features/voice-cloning) ask for at least 10 seconds per clip and say a minute or two of clear, single-speaker speech improves fidelity. [ElevenLabs](https://elevenlabs.io/docs/overview/capabilities/voices) describes Instant Voice Cloning from samples under two minutes, and Professional Voice Cloning trained on extended audio. [Inworld's comparison guide](https://inworld.ai/resources/voice-design-vs-voice-cloning) puts its instant method at five to 15 seconds of authorized audio and its professional method, in preview, at ten minutes or more. ## When design fits Design when the voice does not need to sound like anyone in particular: a game character, a narrator for a faceless channel, a brand voice, a bedtime storyteller. [Inworld's guide](https://inworld.ai/resources/voice-design-vs-voice-cloning) makes design the default when resemblance to a person is unnecessary. Design also gives you a voice of your own without recording one. In [Paxa's Voice design tool](https://paxalabs.com/app/voice-design), you describe the voice in English, Thai, or Chinese, hear its first line, and change it in words until it fits. A saved voice gets an id for the API, and an account keeps 1 on the free plan and up to 50 on the largest. [How to describe a voice](https://paxalabs.com/guides/how-to-describe-a-voice-for-ai) covers what to write. ## Consent and impersonation A clone copies a real person. It starts with their permission. [Inworld's guide](https://inworld.ai/resources/voice-design-vs-voice-cloning) says consent should be explicit, documented, and scoped to languages, channels, use cases, duration, and storage. Design needs no recording, but a description can still aim at a person. [OpenAI](https://developers.openai.com/api/docs/guides/custom-voices) keeps named people out of voice prompts, and [Fish Audio](https://fish.audio/blog/voice-design/) asks for a no-impersonation confirmation before each designed voice is saved. In Paxa, a description that names a real person gets an original voice under an invented name, and impersonating anyone is against [the rules every account accepts](https://paxalabs.com/responsible-ai). ## What a design costs Vendors charge for a design in different ways. [ElevenLabs](https://elevenlabs.io/docs/creative-platform/voices/voice-design) charges the preview text's characters once per generation, though three previews come back. [Fish Audio's API](https://docs.fish.audio/developer-guide/models-pricing/pricing-and-rate-limits) charges $0.01 per successful request, whatever the number of candidates. [MiniMax](https://platform.minimax.io/docs/guides/pricing) charges $3 per designed voice, on its first use for speech. Paxa charges 5 credits for each design or change, its first line included. Speech in a saved voice costs 1.25 times the rate of a catalog voice, and [the pricing page](https://paxalabs.com/pricing) lists that rate. ## Common questions ### Is voice design the same as voice cloning? No. Cloning copies a real person's voice from recordings of them. Design makes a voice from a written description and needs no recording. ### Can I make an AI voice of my own without recording? Yes, with voice design. Describe the voice you want, refine it in words, and save it. It sounds the way your description says. ### Does Paxa offer voice cloning? No. Paxa accepts no reference audio. Voices come from the catalog or are designed from a description. ### Can I use a designed voice commercially? The free credits on a new Paxa account are for personal use and evaluation. Buy any plan or credit pack, and the account's audio is yours to use commercially, including audio made before the purchase. [The Terms](https://paxalabs.com/terms#content-and-output) state the rule. # Best AI voice design tools for Thai voices, saved voices, and voice agents Which AI voice design tool fits a voice that speaks Thai, a voice saved and called by id, several takes at once, or a realtime voice agent, from each tool's own pages. Published 2026-10-23. Paxa Labs. A voice design tool makes a voice from a written description, with no recording. The tools differ in what languages a designed voice speaks, whether the voice is kept, how many takes come back, and where the voice can be used afterward. Every tool fact below links that tool's own page, as we read it in October 2026. No tool is scored or ranked, and a tool appears under a job only where its own page states the fit. ## For a voice that speaks Thai [Paxa's Voice design](https://paxalabs.com/voice-design) takes a description in English, Thai, or Chinese, and each voice speaks Thai, English, or Mandarin, the one its description names. [The guide to describing a voice](https://paxalabs.com/guides/how-to-describe-a-voice-for-ai) has Thai and Chinese examples. [ElevenLabs' models page](https://elevenlabs.io/docs/overview/models) lists its eleven_ttv_v3 design model at more than 70 languages, linked to the Eleven v3 language list, which includes Thai. [Google's speech generation page](https://ai.google.dev/gemini-api/docs/speech-generation) lists Thai for Gemini 3.8 Flash TTS, a model its [voice design page](https://ai.google.dev/gemini-api/docs/voice-design) supports. [Inworld](https://docs.inworld.ai/tts/capabilities/multilingual) places Thai in its second language tier and recommends designing a voice in the target language, while [its voice design page](https://inworld.ai/voice-design) asks for descriptions written in English. ## For a voice you save and call by id A saved Paxa voice gets an id that works in [the Speech tool](https://paxalabs.com/app/speech) and the API on Paxa TTS Flash v2. An account keeps 1 on the free plan and up to 50 on the largest, and [the designed voices docs](https://paxalabs.com/docs/designed-voices) cover the id. [ElevenLabs](https://elevenlabs.io/docs/creative-platform/voices/voice-design) saves a design into a voice slot, and [its slot table](https://elevenlabs.io/docs/help-center/account/general/how-many-voice-slots-do-i-get-per-tier-and-how-can-i-increase-it) runs from 3 on the free plan through 10, 30, and 160 on Starter, Creator, and Pro. [Inworld](https://docs.inworld.ai/tts/voice-design) gives a published voice a permanent voiceId, and [its pricing](https://inworld.ai/pricing) lists 100 custom voice slots on demand. [Gemini](https://ai.google.dev/gemini-api/docs/voice-design) stores up to 200 voices per project, each for one year. Two tools keep a design for less time. [MiniMax](https://platform.minimax.io/docs/api-reference/api-overview) deletes a designed voice that no speech request has used for seven days. [Fish Audio's design API](https://docs.fish.audio/features/voice-design) creates no saved voice and points to cloning for a voice that persists. ## For several takes from one description [ElevenLabs](https://elevenlabs.io/docs/creative-platform/voices/voice-design) returns three options per generation, and [Inworld](https://docs.inworld.ai/tts/voice-design) up to three previews. [Fish Audio's API](https://docs.fish.audio/features/voice-design) returns one to four candidates, two by default, and [Hume's platform](https://dev.hume.ai/docs/voice/voice-design) generates three. Paxa returns one voice per description and changes it in words: type warmer or slower, and hear the next version. [ElevenLabs Remix](https://elevenlabs.io/docs/overview/capabilities/voice-remixing) also changes a voice from a description of the change, as a separate feature. ## For a realtime voice agent A saved Paxa voice speaks on [a live connection](https://paxalabs.com/docs/live-streaming), which takes text as it arrives and streams audio back. [OpenAI's prompt voices](https://developers.openai.com/api/docs/guides/custom-voices) work only with GPT-Live, up to 100 per organization. [Hume](https://dev.hume.ai/docs/voice/voice-design) makes a saved voice usable in its EVI voice interface as well as its TTS. ## For knowing what a design costs Paxa charges 5 credits for each design or change, its first line included, and speech in a saved voice costs 1.25 times the rate of a catalog voice. [Inworld](https://inworld.ai/voice-design) bills designed voices at the same rate as its library voices. [Alibaba Cloud](https://www.alibabacloud.com/help/en/model-studio/qwen-tts-voice-design) makes creating a voice free and bills speech at the target model's rate, in the China (Beijing) region it lists. ElevenLabs, Fish Audio, and MiniMax each price a design differently, per generation, per request, or per voice. [The design and cloning guide](https://paxalabs.com/guides/voice-design-vs-voice-cloning#cost) sets out all three. ## Before you build on one [ElevenLabs' help center](https://elevenlabs.io/docs/help-center/product/voices/voice-design/what-is-voice-design) says its voice design is still experimental. [Hume's voice design](https://dev.hume.ai/docs/voice/voice-design) requires its Octave 1 model, which [its FAQ](https://dev.hume.ai/docs/text-to-speech-tts/faq) says supports English and Spanish only. A designed Paxa voice works with Paxa TTS Flash v2 and no other model. ## Common questions ### What is an AI voice design tool? A tool that makes a voice from a written description of it, such as a calm narrator in her fifties, with no recording of a real person. ### Which voice design tools speak Thai? Paxa designs voices that speak Thai, English, or Mandarin. ElevenLabs links its eleven_ttv_v3 design model to a language list that includes Thai, Google lists Thai for Gemini 3.8 Flash TTS, and Inworld places Thai in its second language tier. ### Can I use a designed voice through an API? Yes, at Paxa, ElevenLabs, Inworld, and Gemini once the voice is saved. A saved Paxa voice works in the console and the API, on single requests and live connections, and the Paxa API also designs, saves, renames, and deletes voices. ### Is voice design free? It depends on the tool. Alibaba Cloud makes voice creation free. A Paxa design costs 5 credits, and a new account's free credits cover several, with no card. # Voice design is available now Voice design turns a written description into a new Paxa TTS Flash v2 voice that you can change in words and save for the console and the API. Published 2026-10-22. Paxa Labs. Until today, every Paxa speech request used one of the 50 Paxa TTS Flash v2 voices in the catalog. A product that needed a particular sound took the closest of them. Voice design adds voices made to a description. Type one in the console, such as “A documentary narrator in his fifties with a low, even voice and an unhurried pace.” The tool answers with one voice. It has a name, a role, and a short description, and it speaks a line written for it. Type what to change, and the next version plays. Save the one you want, and it works in the Speech tool and on the API. Voice design is open to every account, free accounts included. Each design or refine turn costs 5 credits, its spoken line included. Speech in a saved voice runs on Paxa TTS Flash v2 at 1.25 times the standard rate, which is $12.50 per million characters. ## How a voice is designed 1. Describe it: Type who is speaking and how they sound, or start from one of the four examples in the tool: a documentary narrator, a café barista, a support agent, and a bedtime storyteller. 2. Listen: The name, role, and description fill in first. The voice then speaks a short line in character, in the language it was designed for. 3. Refine it: Type a change such as “warmer” or “a little older”, and the next version plays. A line you type, up to 120 characters, plays in the current version. 4. Save it: Name the voice and save it. It appears in the Speech tool under Your voices and gets an id for the API. ## What to put in a description A description can name who is speaking, how they speak, the language, and where the voice will be used. The tool chooses whatever the description leaves out. | Short description | Fuller description | | --- | --- | | A friendly woman | A friendly pharmacist in her thirties who explains doses slowly and clearly in Thai | | A deep male voice | A late night radio host in his forties, deep and relaxed, close to the microphone, speaking English | | An energetic voice for ads | A quick, bright host for a Bangkok street food channel who laughs between lines, speaking Thai | | A narrator | A calm narrator for a history podcast, unhurried, with a slight smile in the voice, speaking English | Name the language the voice should speak, English, Thai, or Mandarin. Without one, the tool takes it from the rest of the description. A café in Bangkok, for example, can produce a Thai speaker. A famous voice is best described by its qualities. A description that names a real person gets an original voice, under a name the tool makes up. ## Refining a voice A refinement is one instruction about the current voice, such as “make her sound a little older” or “slow the pace down”. The next version changes what the instruction asks for and keeps the rest. Every version stays in the history panel, and going back to one costs nothing. A typed line plays in the current version without changing the voice, billed by the character at the designed voice rate. ## Directions in a designed voice A designed voice speaks on Paxa TTS Flash v2 and reads its directions. `[whispering]` before a line, or `[excited]` before a sentence, works in a saved voice as it does in a catalog voice. The [direction guide](https://paxalabs.com/docs/directions) lists every direction and sound, and the [Paxa TTS Flash v2 launch post](https://paxalabs.com/blog/paxa-tts-flash-v2) plays them. ## Descriptions for common uses | Use | A description to start from | | --- | --- | | Product videos and ads | A confident presenter in her thirties for a skincare brand, warm and quick, speaking Thai | | Audiobooks and audio drama | An elderly innkeeper with a rough, slow voice who speaks English | | Support lines and voice agents | A patient customer support agent with a warm, clear voice who speaks Thai at a steady pace | | YouTube and TikTok narration | A narrator in his forties for a history channel, even and unhurried, speaking English | | Children's apps | A grandfather telling a bedtime story in Thai, soft and slow, with a little humor | A saved voice cannot be edited. A series made in it keeps one voice from the first episode to the last, and an ad cut into three lengths speaks in the same voice in each. ## Using a saved voice - In the console, pick it in the Speech tool, where saved voices head the picker under Your voices. - On `POST /v1/tts`, send its id, which starts with `voice_`, as `voice` with `model` set to `paxa-tts-flash-v2`. - The [OpenAI-compatible](https://paxalabs.com/docs/openai-compatibility) `POST /v1/audio/speech` takes the same id. An OpenAI SDK pointed at the Paxa API speaks in the voice unchanged. - The [live socket](https://paxalabs.com/docs/live-streaming), `/v1/tts/live`, takes the id in its `start` frame. The id works for the account that saved it, from any of its API keys. Model marketplaces that resell Paxa Labs speech, and the demo on this site, serve catalog voices. The [designed voices guide](https://paxalabs.com/docs/designed-voices) has every rule of the id. Renaming a voice keeps its id. Deleting one frees its place on the plan, and requests that still name it answer `unknown_voice`. After a downgrade, every saved voice keeps speaking, and new saves wait until the account is under its plan's limit again. ## Pricing Each design or refine turn costs 5 credits, its spoken line included. A turn that fails without a voice is refunded, and a turn you stop keeps its charge. A new account's 100 free credits cover 20 turns. Speech in a designed voice bills at 1.25 times the Paxa TTS Flash v2 rate, or 1.5 times with word timestamps. | Voice | Credits per 1,000 characters | | --- | --- | | A catalog voice | 10 | | A designed voice | 12.5 | | A designed voice with timestamps | 15 | The plan sets how many designed voices an account keeps. | Plan | Designed voices | | --- | --- | | Free | 1 | | Starter | 3 | | Standard | 10 | | Pro | 50 | Plans and their monthly credits are on the [pricing page](https://paxalabs.com/pricing). ## What it does not do Voice design works from text alone. It takes no recording and cannot reproduce a person's voice from one. A description the tool cannot design from is refused, and the turn's credits are refunded. Open [Voice design](https://paxalabs.com/app/voice-design) in the console to start. ## Questions ### Can I try voice design for free? Yes. Every account can design voices and keep 1, free accounts included. A turn costs 5 credits, and a new account's 100 free credits cover 20 turns. ### How many voices can I keep? Free accounts keep 1. Starter keeps 3. Standard keeps 10. Pro keeps 50. ### Can I edit a saved voice? You can rename it. Its sound stays as you saved it, and a different sound is a new design. ### Does a designed voice work on Paxa TTS Flash? No. Designed voices speak on Paxa TTS Flash v2. A Paxa TTS Flash request that names one answers `unknown_voice`. ### Can voice design clone my voice? No. Voice design takes no recording. It designs a new voice from a description. ### Which language does a designed voice speak? The one named in the description, English, Thai, or Mandarin. The description itself can be written in any of the three. ### Can I use a designed voice in the API? Yes. Send its id as `voice` with `model` set to `paxa-tts-flash-v2`, from any API key on the account that saved it. # How to make an AI voice, from script to download Pick a voice, write the script, add a direction in square brackets, then generate and download, and see how Google AI Studio, Canva, and CapCut do it. Published 2026-10-18, updated 2026-10-23. Paxa Labs. An AI voice reads the script you write, in a voice you choose, and gives you an audio file. It needs no microphone or recording session. People use one for a video, a lesson, a shop announcement, or a message to family. In Paxa, you make it in [the Speech tool](https://paxalabs.com/app/speech) of the console. Every Paxa TTS Flash v2 voice speaks English, Thai, and Mandarin, and a new account starts with 100 free credits. ## Make an AI voice, step by step 1. Pick a voice: Sign in and open [the Speech tool](https://paxalabs.com/app/speech). It opens on Paxa TTS Flash v2, the model whose voices read directions. Open the voice picker and play the samples, which cost nothing. If none fits, design one first. 2. Write the text: Type or paste the script, up to 5,000 characters in one run. Write numbers and names the way they should be said. The cost in credits shows under the text before you generate. 3. Add a direction: Write how the words should sound in square brackets, such as `[warm, unhurried]` or `[whispering]`. A direction holds until the next one, and the brackets are not spoken. 4. Generate and listen: Under Format, choose MP3 for most uses or WAV for a video edit. Then select Generate speech and play the clip. 5. Fix and download: If a name or number comes back wrong, respell it in the text and generate again. When the take sounds right, select the Download icon on the player. ## Write a direction On Paxa TTS Flash v2, a direction is any words in square brackets, in English, Thai, or Chinese. It can be one word, such as `[sad]`, or a description of the whole read, such as `[product advertisement, punchy and upbeat]`. Text before the first direction keeps the voice's own delivery. A direction holds from where it stands until the next one, and `[neutral]` returns to the voice's own delivery. two directions in one script: ``` [warm, unhurried] Welcome back to the kitchen. [excited] Today we make green curry paste from scratch. ``` [The narrator voice page](https://paxalabs.com/narrator-voice) plays directions written into real narration, and [the voices page](https://paxalabs.com/voices) plays every voice. ## Design a voice when none fits When no catalog voice suits the script, open [Voice design](https://paxalabs.com/app/voice-design) in the console. Write who is speaking and how they sound, such as "a woman in her fifties who reads ghost stories late at night, low and a little husky". A new voice comes back with a name and a line in character. Type what to change, such as warmer or a little older, and the next version plays. Save the version you want, and it appears under Your voices in the Speech tool's voice picker, ready for the steps above. Each design or change costs 5 credits, and speech in a saved voice runs at 1.25 times the rate of a catalog voice. [The voice design page](https://paxalabs.com/voice-design) plays descriptions beside the voices they made. ## Other places to make an AI voice Google AI Studio, Canva, and CapCut also come up when people look for a way to make an AI voice. Every fact about them below links the vendor's own page, as we read it in October 2026. ### Google AI Studio and Gemini TTS [Google's speech generation guide](https://ai.google.dev/gemini-api/docs/speech-generation) says the Gemini API turns text into audio with one speaker or two. It suggests testing the Gemini TTS models in Google AI Studio before you start building. The guide lists 30 prebuilt voices. Its table of supported languages checks Thai for Gemini 3.8 Flash TTS and leaves it blank for Gemini 3.8 Flash-Lite TTS. The guide sets delivery, such as emotion, pacing, and volume, in a `style` field kept apart from the words to be spoken. ### Canva [Canva's help center](https://www.canva.com/help/canva-ai-voice/) places AI Voice inside a design. In the side panel, select Elements, scroll to Audio, and select Generate AI Voice, or open the Apps tab and select AI Voice. Type the script, choose a language and a voice style under Select Voice, and select Generate AI voice. The voice track lands on the design's audio timeline. ### CapCut [CapCut's help center](https://www.capcut.com/help/text-to-speech-feature) turns a text layer in a video project into speech. In the mobile app, tap Text and add your words, then tap the text layer on the timeline. Select Text-to-Speech from the bottom menu, choose a voice style, and tap Generate. The same page gives the steps for the desktop app and the web editor. ## What the free credits allow A new account gets 100 free credits, granted once. They voice about 11 minutes of English narration in any Paxa TTS Flash v2 voice. Audio made on the free credits alone is for personal use and evaluation. Buy any plan or credit pack, and all of the account's audio is yours to use commercially, including audio made before the purchase. [The Terms](https://paxalabs.com/terms#content-and-output) state the rule, and [the pricing page](https://paxalabs.com/pricing) lists the plans and packs. Each other tool sets its own license for the audio it makes. Read it before you publish. ## Common questions ### Can I make an AI voice for free? Yes, for personal use and evaluation, with a new account's 100 free credits. [The free text to speech page](https://paxalabs.com/tts) also reads a line in 9 voices with no account. ### How much text does one run read? Up to 5,000 characters. Split a longer script at a paragraph, and join the clips in an audio editor. ### Can I make an AI voice of my own voice? Paxa Labs clones no voice from a recording. Pick a catalog voice in the voice picker, or [design a new voice](https://paxalabs.com/voice-design) from a written description and save it to your account. ### Do directions cost extra? Directions are text. Every character in the brackets counts toward the cost, at the same rate as the words. # How to have AI read a novel aloud Paste a chapter, choose a narrator, give each character a voice or a direction, and listen, with what the free credits cover and when a book needs its rights holder's permission. Published 2026-10-18. Paxa Labs. Some novels are easier to finish by ear. A web serial updates while you commute, and a long series can tire your eyes by the third book. An AI voice reads the chapter you paste, in a voice you choose, and you listen wherever you like. In Paxa, that happens in [the Speech tool](https://paxalabs.com/app/speech) of the console. Every Paxa TTS Flash v2 voice reads English, Thai, and Mandarin in the same voice. ## Read a chapter aloud, step by step 1. Paste the chapter: Copy the chapter as text and paste it into [the Speech tool](https://paxalabs.com/app/speech). One run reads up to 5,000 characters, about 5 minutes of English narration. Paste a longer chapter in parts, breaking at a scene or a paragraph. 2. Choose a narrator: Open the voice picker and play the samples, which cost nothing. Pick a voice you could listen to for an hour. 3. Mark the dialogue: Before a line that needs it, write a direction in square brackets, such as `[whispering]`. Write `[neutral]` after the line to return to the narrator's own delivery. 4. Generate and listen: Set the Speed slider first for a slower or faster read, at the same price. Then select Generate speech and play the part. 5. Keep the parts in order: Select the Download icon on each part's player, and number the files in reading order. ## Choose a narrator voice A narrator stays with the listener for the whole book. The catalog casts these voices for narration, audiobooks, long reads, and audio novels. | Voice | Role | Cast for | | --- | --- | --- | | [Khanom Krok](https://paxalabs.com/voices/khanomkrok) | Lead voice | Calm, unhurried elder voice and the roster's male lead: documentary, heritage storytelling, and narration. | | [Foi Thong](https://paxalabs.com/voices/foithong) | Narrator | Captivating narrator for long reads and explainers. | | [Espresso](https://paxalabs.com/voices/espresso) | Long-form reader | Deep, dark, unhurried English reader for long fiction, documentaries, and trailers. | | [Tao Huay](https://paxalabs.com/voices/taohuay) | Drama lead | Tender, melancholy Mandarin voice for drama recaps, audio novels, and companion speech. | | [Oolong](https://paxalabs.com/voices/oolong) | Audio fiction | Low, rich Mandarin narrator for audio novels, brand reads, and late-night stories. | Hear a few pages in each before you settle. [The narrator voice page](https://paxalabs.com/narrator-voice) plays narrators reading in English, Thai, and Mandarin, and [the voices page](https://paxalabs.com/voices) plays every voice. ## Give characters a voice or a direction Dialogue is where one even read loses a listener. A direction is the quicker fix. Keep one narrator, and write how a line should sound in square brackets before it. The direction holds until the next one, and `[neutral]` returns to the narrator's own delivery. a direction on the dialogue, then back to the narrator: ``` [warm, unhurried] The ferry left the pier at dusk, and Mali watched the temple lights come on one by one. [whispering] "Don't look back," her brother said. "They're still on the pier." [neutral] She looked anyway. ``` A voice of their own takes more steps. Each run speaks in one voice. Generate each character's lines in that character's voice, then join the clips in order in an audio editor. [The audio novels page](https://paxalabs.com/use-cases/audio-novels) casts a catalog voice for each character of a serial. [The bedtime stories page](https://paxalabs.com/bedtime-stories) plays a story in which a grandfather narrates and two animals talk. ## Listen on free credits A new account gets 100 free credits, granted once, enough for about 11 minutes of English narration. On free credits alone, [the Terms](https://paxalabs.com/terms#content-and-output) license the audio for personal, non-commercial use and for evaluating the service. A long book takes more credits than the free grant. [The pricing page](https://paxalabs.com/pricing) lists the plans and credit packs. ## Publishing someone else's book as audio To publish an audio version of a book someone else wrote, you need permission from whoever holds its rights, usually the author or the publisher. Once you buy any plan or credit pack, the Terms assign you all of the account's audio for any use, commercial included, while you comply with them. That includes audio made before the purchase. A purchase covers the audio. Permission for the book's words still comes from its rights holder. [The Terms](https://paxalabs.com/terms#content-and-output) state the purchase rule. ## Common questions ### Can AI read a whole novel aloud? Yes, chapter by chapter. One run reads up to 5,000 characters, about 5 minutes of English narration. A long chapter goes in as several runs. ### Can I listen on the free credits? Yes, the free credits pay for runs like any other credits. On free credits alone, [the Terms](https://paxalabs.com/terms#content-and-output) license the audio for personal, non-commercial use and for evaluating the service. ### Can I post the audio of a novel I like? Only with permission from whoever holds the book's rights, usually the author or the publisher. Once you buy any plan or credit pack, the Terms assign you the audio for any use, commercial included, while you comply with them. Permission for the words still comes from their rights holder. ### Can it read a novel in Thai or Mandarin? Yes. Every Paxa TTS Flash v2 voice reads English, Thai, and Mandarin in the same voice. # Best Thai text to speech for apps, videos, and phone lines Which Thai text to speech service fits an app, a narrated video, a phone line, a live voice agent, or a cloned brand voice, from each vendor's own documentation. Published 2026-10-17, updated 2026-10-26. Paxa Labs. Teams buying Thai text to speech usually have one job in mind. A banking app that reads balances aloud needs different things from a hotline menu played over a phone line. The job decides which vendors stay on the list, because each one documents different voices, output formats, and connections. Every vendor fact below links that vendor's own documentation, as our comparison pages verified it in October 2026. No vendor is scored or ranked. Published rates for each one are on [the text to speech API comparison](https://paxalabs.com/compare/text-to-speech-apis), re-checked every month. ## Check that Thai is on the list Several widely used speech APIs have no Thai voice. [Amazon Polly's language table](https://docs.aws.amazon.com/polly/latest/dg/supported-languages.html) has no Thai code. [Deepgram's Aura voices](https://developers.deepgram.com/docs/tts-models) cover seven languages, and Thai is not one of them. [SpeechifyAI](https://docs.speechify.ai/build/guides/text-to-speech/language-support) lists six languages for simba-3.0 and English alone for simba-3.2, with Thai on neither list. Others list Thai among dozens of languages. [ElevenLabs](https://elevenlabs.io/docs/overview/models) names it among the 90+ languages of its newest speech models, [Cartesia](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) among the 44 of Sonic 3.6, and [MiniMax](https://platform.minimax.io/docs/api-reference/speech-t2a-http) among the 40 values of its language setting. A language list says a vendor accepts Thai text. How a voice reads an English brand name inside a Thai sentence, Thai digits, or an abbreviation such as พ.ศ. is something to hear on your own script. [How to judge a Thai AI voice](https://paxalabs.com/guides/judging-a-thai-ai-voice) lists what to listen for, and [the script guide](https://paxalabs.com/guides/writing-for-thai-text-to-speech) covers numbers and English words in a Thai line. ## For an app or a website that speaks Thai Every voice on Paxa TTS Flash v2 speaks Thai, English, and Mandarin, including the English words inside a Thai sentence. [The voices page](https://paxalabs.com/voices) plays all 50, and each voice keeps its slug for as long as it serves. An app already written for the OpenAI SDK can call [the compatible endpoint](https://paxalabs.com/docs/openai-compatibility) by changing the base URL and key, and its OpenAI model and voice names keep working. [Botnoi Voice](https://voice.botnoi.ai/) lists more than 20 languages, Thai, English, and Chinese among them, and links a page of named speakers for each. [iApp's ChindaTTS documentation](https://iapp.co.th/docs/speech/text-to-speech/text-to-speech-v3) describes reading Thai, or Thai with English inside, on its standard voice, and limits that voice to Thai and English. On the cloud platforms, [Google's Gemini-TTS](https://docs.cloud.google.com/text-to-speech/docs/gemini-tts) lists Thai (th-TH) as generally available. [Microsoft's voice table](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts) lists three standard th-TH voices for Azure AI Speech, plus two in its Neural HD tiers marked public preview. An app plays the same short lines many times. Test the strings your screens really show, such as a balance, a date, and a product name. Then check that the output format plays in your app with no conversion step. ## For narrated videos, courses, and captions Narration needs timing as much as a voice, because captions have to land on the words. Paxa TTS Flash v2 returns [word, sentence, or utterance timestamps](https://paxalabs.com/docs/timestamps) with the audio on the same request. Word spans split Thai text that has no spaces, and [the subtitles page](https://paxalabs.com/subtitles) shows the result on a caption line. [Narakeet's API](https://www.narakeet.com/docs/automating/text-to-speech-api/) can return SRT and VTT subtitle files with the audio, synchronized on paragraph boundaries, and Narakeet keeps [a separate voice list for Thai](https://www.narakeet.com/languages/). [Microsoft's Speech SDK](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis) raises a WordBoundary event at each spoken word, punctuation mark, and sentence, with its time offset in the audio. [Cartesia's API](https://docs.cartesia.ai/api-reference/tts/tts) documents an `add_timestamps` parameter. A course often runs long. One Paxa TTS Flash v2 request reads up to 5,000 characters, and a chapter goes out as several requests whose audio you join. Narakeet takes scripts longer than 1 KB through its polling API. Some teams build clips by hand in a web studio and call no API at all. [Botnoi Voice](https://voice.botnoi.ai/) offers a web studio beside its API, and [UVoice AI](https://uvoice.app/th/) offers a clip studio beside its own. [The creators page](https://paxalabs.com/use-cases/creators) and [the short-video voiceover guide](https://paxalabs.com/guides/ai-voiceover-for-tiktok-reels-and-shorts) cover a channel's own workflow. ## For phone lines and IVR menus Telephony carries audio as 8 kHz μ-law or A-law. [ElevenLabs' create speech reference](https://elevenlabs.io/docs/api-reference/text-to-speech/convert) lists `ulaw_8000` and `alaw_8000` among its output formats and notes that μ-law is common for Twilio audio inputs. [Murf](https://murf.ai/api/docs/text-to-speech/overview) lists ALAW and ULAW outputs with 8000 Hz among its sample rates, and [its voice library](https://murf.ai/api/docs/voices-styles/voice-library) has a Thai section. Paxa TTS Flash v2 returns MP3, Opus, WAV, or raw PCM, with PCM and WAV at 8 to 48 kHz through the sample_rate field. For phone lines it returns G.711 μ-law and A-law at 8 kHz directly. The audio goes into a telephony stack with no resampling step. An IVR menu changes rarely. Generate each prompt once, store the file, and play it from storage on every call. A call that answers the caller in real time needs a live connection. [The call centers page](https://paxalabs.com/use-cases/call-centers) and [the announcements page](https://paxalabs.com/use-cases/announcements) cover both. ## For a live voice agent A voice agent speaks an LLM's reply while the model is still writing it, and stops when the caller talks over it. The Paxa [live WebSocket](https://paxalabs.com/docs/live-streaming) takes text fragments as they arrive and synthesizes at natural boundaries. A cancel frame stops the reply in progress and keeps the connection open. [Botnoi Voice's API reference](https://voice.botnoi.ai/developer/api) documents a realtime websocket that returns audio in base64 chunks. [Cartesia's API](https://docs.cartesia.ai/api-reference/tts/tts) has a websocket endpoint for realtime synthesis. [Fish Audio's streaming guide](https://docs.fish.audio/features/realtime-streaming) describes a WebSocket that buffers text until it has enough context, with a flush event that forces generation. Its [S2 Pro page](https://fish.audio/s2/) names Thai among the added languages. Thai text carries no sentence-final punctuation, and a streaming voice that waits for a full stop waits for the whole reply. [The sentence breaks guide](https://paxalabs.com/guides/thai-sentence-breaks-for-streaming-speech) shows where to cut a Thai reply. [The realtime speech page](https://paxalabs.com/realtime-tts) and [the voice agents page](https://paxalabs.com/use-cases/voice-agents) cover the Paxa side. ## For a voice of your own [iApp's ChindaTTS](https://iapp.co.th/docs/speech/text-to-speech/text-to-speech-v3) clones a voice from a 10 to 20 second Thai clip, and its cloned voices read Thai only. [ElevenLabs](https://elevenlabs.io/docs/overview/capabilities/voices) describes cloned voices beside its Voice Library and voices designed from a text prompt. [Fish Audio's cloning guide](https://docs.fish.audio/features/voice-cloning) asks for clips of at least 10 seconds each, saved as a voice model or sent with every request. [MiniMax's Speech-02 announcement](https://www.minimax.io/news/speech-02-series) describes voice cloning and names Thai among its added languages. [UVoice AI](https://uvoice.app/th/) pairs a clip studio that clones voices with its API. Clone a voice only with the speaker's written consent, and read each vendor's terms on commercial use of a clone before you ship. Paxa Labs offers no voice cloning. Its catalog voices keep their slugs while they serve, and a new voice comes from [voice design](https://paxalabs.com/voice-design), which builds one from a written description with no recording. ## For Thai beside English and Mandarin Every Paxa TTS Flash v2 voice reads English and Simplified Chinese in the same voice it uses for Thai. Paxa TTS Flash adds 4 voices in [Isan, Northern, and Southern Thai](https://paxalabs.com/voices/accents), for a campaign that speaks to one region. [MiniMax's API](https://platform.minimax.io/docs/api-reference/speech-t2a-http) sets Thai, English, Chinese, and Cantonese as separate language values. [Narakeet](https://www.narakeet.com/languages/) keeps separate voice lists for Thai, Mandarin Chinese, Taiwanese Mandarin, and Cantonese. [Gemini-TTS](https://docs.cloud.google.com/text-to-speech/docs/gemini-tts) lists Thai as generally available and Mandarin (cmn-CN) in Preview. Decide first whether the brand keeps one voice across every language or one voice per language. A vendor whose voices each speak a single language can offer only the second. [The English voices page](https://paxalabs.com/english-voices) and [the Chinese voices page](https://paxalabs.com/chinese-voices) play each set. ## Jobs and vendors at a glance | Job | Vendors whose documentation states the fit | | --- | --- | | Apps and websites in Thai | Azure AI Speech, Botnoi Voice, Google Gemini-TTS, iApp, Paxa TTS Flash v2 | | Narration with captions | Azure AI Speech, Cartesia, Narakeet, Paxa TTS Flash v2 | | Phone lines and IVR | ElevenLabs, Murf | | Live voice agents | Botnoi Voice, Cartesia, Fish Audio, Paxa TTS Flash v2 | | A cloned voice | ElevenLabs, Fish Audio, iApp, MiniMax, UVoice AI | | Thai, English, and Mandarin | Google Gemini-TTS, MiniMax, Narakeet, Paxa TTS Flash v2 | ## Test on your own script - Write one script with what your product really says, such as an English brand name, an amount in baht, a date, and a phone number. - Run it through two or three vendors listed under your job, with voices of the same gender. - Listen where your users will, on a phone speaker for an app and on a handset for a hotline. - Check the output format, the request size limit, and the connection type against your player or phone system. - Hear 9 Paxa TTS Flash v2 voices with no account on [the text to speech page](https://paxalabs.com/tts). ## Common questions ### Which Thai text to speech is best? The one that fits your job. Find your job above, then test the two or three vendors it names on your own script, on the device your users hear it on. ### Does Paxa Labs clone voices? No. Catalog voices keep fixed slugs, and [voice design](https://paxalabs.com/voice-design) builds a new voice from a written description, with no recording. iApp, ElevenLabs, Fish Audio, MiniMax, and UVoice AI document voice cloning. ### Can one product use more than one text to speech vendor? Yes. Each request stands alone. An app can send its screen reading to one vendor and its phone prompts to another, as long as both voices suit the brand. ### Where are the prices? On [the text to speech API comparison](https://paxalabs.com/compare/text-to-speech-apis), which lists each vendor's published rate beside ours and is re-checked every month. ### Do you publish quality or latency comparisons? No. We publish no accuracy, quality, or latency figures, for our models or anyone else's. Listen to each candidate on your own text before you choose. # Best Thai speech to text for meetings, subtitles, and voice agents Which speech to text service fits Thai meetings, recorded calls, subtitles, long recordings, mixed Thai and English, or a live voice agent, from each vendor's own documentation. Published 2026-10-17. Paxa Labs. Thai recordings bring their own problems to transcription. Words run together with no spaces between them. English product names arrive mid-sentence, and a number can come back as words or as digits. Which of these matters most depends on the job, and the job decides which vendors stay on the list. Every vendor fact below links that vendor's own documentation, as our comparison pages verified it in October 2026. No vendor is scored or ranked. Published rates for each one are on [the speech to text API comparison](https://paxalabs.com/compare/speech-to-text-apis), re-checked every month. > Paxa STT Lite and Paxa STT Lite Realtime are both in Research preview. Their transcripts and options may change between releases, and [the speech to text page](https://paxalabs.com/speech-to-text#changelog) records each change. ## Check which model reads Thai Read each vendor's language list model by model. [AssemblyAI](https://www.assemblyai.com/docs/pre-recorded-audio/supported-languages) lists Thai for Universal-2 and leaves it off Universal-3.5 Pro, which can fall back to Universal-2 for languages outside its set. Its [code switching guide](https://www.assemblyai.com/docs/pre-recorded-audio/code-switching) marks Universal-2 as legacy. [Deepgram](https://developers.deepgram.com/docs/models-languages-overview) lists Thai (th, th-TH) for Nova-3 and Nova-2. [Google's Chirp 3 page](https://docs.cloud.google.com/speech-to-text/v2/docs/chirp_3-model) lists Thai at Preview launch readiness. [Speechmatics](https://docs.speechmatics.com/speech-to-text/languages) lists Thai, and [ElevenLabs](https://elevenlabs.io/docs/overview/capabilities/speech-to-text) lists it among the 90+ languages of Scribe v2. ## For meetings, interviews, and recorded calls A meeting transcript needs to say who spoke. [Deepgram's diarization](https://developers.deepgram.com/docs/diarization) puts a speaker value on every word, with a confidence score on recorded audio, and works on every Nova batch model. [ElevenLabs](https://elevenlabs.io/docs/overview/capabilities/speech-to-text) diarizes up to 32 speakers on Scribe v2. [AssemblyAI](https://www.assemblyai.com/docs/pre-recorded-audio/label-speakers) labels each utterance with a letter, such as A, B, and C, and advises at least 30 seconds of continuous speech per speaker. [Google's Chirp 3](https://docs.cloud.google.com/speech-to-text/v2/docs/chirp_3-model) leaves Thai off its diarization language list. Paxa STT Lite, a Research preview, returns numbered speaker turns on a recording under 9 minutes, and nobody is identified by name. Split a longer meeting at a pause, and each part numbers its speakers afresh. The [meetings](https://paxalabs.com/use-cases/meetings) and [call recordings](https://paxalabs.com/use-cases/call-recordings) pages and [the speaker turns guide](https://paxalabs.com/guides/thai-call-transcripts-with-speaker-turns) go further. ## For product names, people, and jargon Product meetings and support calls are full of brand names and jargon. Each vendor in this section takes a list of terms to expect. [Speech adaptation on Google's Chirp 3](https://docs.cloud.google.com/speech-to-text/v2/docs/chirp_3-model) takes up to 1,000 phrases. [Deepgram's keyterm prompting](https://developers.deepgram.com/docs/keyterm) works on Nova-3 and on Flux, with up to 500 tokens of key terms per request. [ElevenLabs](https://elevenlabs.io/docs/overview/capabilities/speech-to-text) takes up to 1,000 terms on recorded audio and 50 on realtime. Paxa STT Lite, a Research preview, takes up to 50 terms of up to 50 characters each, in Thai or any other language. The terms bias recognition toward your spelling and are never inserted. ## For recordings that switch between Thai and English Thai speakers often say a product name or a technical term in English and carry on in Thai. A useful transcript keeps each word in the script it was spoken in. [Speechmatics](https://docs.speechmatics.com/speech-to-text/languages) lists Thai, and its Melia 1 model switches between the listed languages with no language selection. Its Enhanced and Standard models take one selected language or a fixed pack, and none of the packs includes Thai. On [AssemblyAI's Universal-2](https://www.assemblyai.com/docs/pre-recorded-audio/code-switching), a manual list holds at most two language codes, one of them English, and the model detects up to two languages per file. [Deepgram's multilingual option for Nova-3](https://developers.deepgram.com/docs/models-languages-overview) covers ten languages, and Thai is not among them. [Google's Chirp 3](https://docs.cloud.google.com/speech-to-text/v2/docs/chirp_3-model) set to auto transcribes the dominant language of a file. Paxa STT Lite, a Research preview, writes a Thai sentence that switches into English as it was spoken, with no language setting and no translation. [The code-switching guide](https://paxalabs.com/guides/transcribing-thai-english-code-switching) shows what that does for search. ## For subtitles on video Thai writes no spaces between words. A subtitle line broken at a character count can split a word in half. Paxa STT Lite, a Research preview, returns an SRT or WebVTT file in the same response as the transcript. Every line break falls on a word boundary, Thai included, and the line length is yours to set. [AssemblyAI](https://www.assemblyai.com/docs/pre-recorded-audio/export-transcripts-as-srt-vtt-or-text) exports a finished transcript as SRT or VTT from its own endpoint, with `chars_per_caption` setting the most characters in each caption. [Deepgram's captions guide](https://developers.deepgram.com/docs/automatically-generating-webvtt-and-srt-captions) turns the JSON transcript into WebVTT or SRT in your own code, with its `@deepgram/captions` package. Whichever you choose, open the file on a Thai clip and read where each line breaks. [The subtitles page](https://paxalabs.com/subtitles) and [the word timestamps guide](https://paxalabs.com/guides/thai-subtitles-from-word-timestamps) show a Thai cue broken between words, and [the captions page](https://paxalabs.com/use-cases/captions) covers the video workflow. ## For lectures, seminars, and other long recordings [ElevenLabs Scribe](https://elevenlabs.io/docs/overview/capabilities/speech-to-text) takes files up to 10 hours in standard mode, and [its API](https://elevenlabs.io/docs/api-reference/speech-to-text/convert) fetches a file from a source URL and can deliver results to a webhook. [Deepgram](https://developers.deepgram.com/docs/pre-recorded-audio) accepts files up to 2 GB and returns a 504 when processing on a Nova model runs past 10 minutes. [Speechmatics](https://docs.speechmatics.com/speech-to-text/batch/limits) takes a file of 1 GB or more through a fetch URL and recommends notifications for job status. On [Google's Chirp 3](https://docs.cloud.google.com/speech-to-text/v2/docs/chirp_3-model), BatchRecognize suits audio from one minute to an hour, or up to 20 minutes with word timestamps. Google's [batch recognition](https://docs.cloud.google.com/speech-to-text/v2/docs/batch-recognize) reads only audio stored in Cloud Storage. Paxa STT Lite, a Research preview, transcribes up to 60 minutes and 25 MiB in one request. Split a longer recording at a pause and send the parts as separate requests. [The lectures page](https://paxalabs.com/use-cases/lectures) shows the workflow. ## For live captions and voice agents A live transcript has to decide where each turn ends, and vendors put that decision in different places. Paxa STT Lite Realtime, a Research preview, decides it in the session. Over one WebSocket it reports when the user starts and stops talking, then sends one final transcript per turn, with no setting to tune. It takes 16-bit PCM at 16 or 48 kHz, or μ-law at 8 kHz as telephony delivers it. [ElevenLabs' realtime guide](https://elevenlabs.io/docs/eleven-api/guides/how-to/speech-to-text/realtime/transcripts-and-commit-strategies) commits transcript segments manually by default, or once a silence threshold is reached under its voice activity strategy. On [Speechmatics' Melia 1](https://docs.speechmatics.com/speech-to-text/models), realtime transcription offers channel diarization only, and the client sends ForceEndOfUtterance when the speaker has finished. Before you build, confirm that the live model itself lists Thai. [The live transcription page](https://paxalabs.com/use-cases/live-transcription), [the realtime turns guide](https://paxalabs.com/guides/realtime-thai-transcription-turns), and [the voice agents post](https://paxalabs.com/blog/realtime-transcription-for-thai-voice-agents) cover the Paxa side. ## For code written against the OpenAI SDK [OpenAI's file transcription guide](https://developers.openai.com/api/docs/guides/speech-to-text) recommends `gpt-transcribe` on /v1/audio/transcriptions, with files up to 25 MB. [Its deprecations page](https://developers.openai.com/api/docs/deprecations) lists `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, and `gpt-4o-transcribe-diarize` for removal from the API on February 26, 2027. Paxa's [/v1/audio/transcriptions](https://paxalabs.com/docs/openai-compatibility) serves the same `audio.transcriptions.create` call at a new base URL, with `model` set to `paxa-stt-lite-v1-preview`, a Research preview. ## Jobs and vendors at a glance | Job | Vendors whose documentation states the fit | | --- | --- | | Meetings and recorded calls | AssemblyAI, Deepgram, ElevenLabs, Paxa STT Lite (Research preview) | | Names and jargon | Deepgram, ElevenLabs, Google Chirp 3, Paxa STT Lite (Research preview) | | Thai mixed with English | AssemblyAI, Speechmatics, Paxa STT Lite (Research preview) | | Subtitle files | AssemblyAI, Deepgram, Paxa STT Lite (Research preview) | | Long recordings | Deepgram, ElevenLabs, Google Chirp 3, Speechmatics, Paxa STT Lite (Research preview) | | Live captions and voice agents | ElevenLabs, Speechmatics, Paxa STT Lite Realtime (Research preview) | | OpenAI SDK code | OpenAI, Paxa STT Lite (Research preview) | ## Test on your own recordings - Pick three recordings like your real traffic, such as a clean one, a noisy one, and one where Thai and English mix. - Run each through every vendor listed under your job, with the same settings. - Read the transcripts against the audio at names, figures, and negations. - Score Thai on characters or syllables. [The accuracy guide](https://paxalabs.com/guides/measuring-thai-transcription-accuracy) explains why word error rate misleads on Thai. - See a Thai recording transcribed word by word on [the transcription page](https://paxalabs.com/transcribe). ## Common questions ### Which Thai speech to text is best? The one that fits your job. A voice agent needs turn detection, and a video team needs subtitle files. Run your own recordings through the vendors listed under your job and read the transcripts side by side. ### What does Research preview mean for Paxa STT Lite? Paxa STT Lite and Paxa STT Lite Realtime are live on the API and in the console while they are evaluated. Their transcripts and options may change between releases, and the changelog on [the speech to text page](https://paxalabs.com/speech-to-text#changelog) records each change. ### Can one product use more than one speech to text vendor? Yes. Each vendor takes its own key and its own requests. A product can send live turns to one vendor and long recordings to another. ### Where are the prices? On [the speech to text API comparison](https://paxalabs.com/compare/speech-to-text-apis), which lists each vendor's published rate for recorded audio, re-checked every month. ### Do you publish accuracy comparisons? No. We publish no accuracy, quality, or latency figures, for our models or anyone else's. Transcribe the same recording on each service you are considering and read the transcripts side by side. # Best Thai OCR for PDFs, invoices, and scanned forms Which OCR or document extraction service fits Thai PDFs bound for an LLM, invoices and ID numbers, handwriting, Office files, or self-hosting, from each vendor's own documentation. Published 2026-10-17. Paxa Labs. A Thai PDF that prints perfectly can still give up broken text. Its text layer may drop the vowels and tone marks above and below a consonant, or store them out of order, and a scan has no text layer at all. Most Thai document work starts with OCR for that reason. [The Thai PDF guide](https://paxalabs.com/guides/thai-pdf-text-extraction) shows each failure with the string it produces. What comes after OCR depends on the job. A language model wants Markdown, and an accounting system wants typed fields. The output shape, the languages on the page, and where the service may run decide which vendors stay on the list. Every vendor fact below links that vendor's own documentation, as our comparison pages verified it in October 2026. No vendor is scored or ranked. Published rates for each one are on [the OCR API comparison](https://paxalabs.com/compare/ocr-apis), re-checked every month. ## Check that Thai is on the list [Amazon Textract](https://docs.aws.amazon.com/textract/latest/dg/limits-document.html) detects text in English, French, German, Italian, Portuguese, and Spanish, and reads handwriting in English only. Thai is not on its list. [Azure Document Intelligence](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/language-support/ocr) lists Thai for printed text and for handwriting on its Read and Layout models, and the language code is optional. [Mistral's language list](https://docs.mistral.ai/resources/languages#ocr) places Thai under Southeast Asia. [LlamaParse's OCR](https://developers.llamaindex.ai/llamaparse/parse/features/ocr-and-languages/) takes an optional list of expected languages, and its API reference includes `th`. Paxa OCR Lite reads Thai first, with other languages read as printed, including pages that mix Thai and English or Thai and Arabic digits. ## For Thai PDFs going into an LLM or a search index A language model reads text, and a table flattened into a run of words loses its rows. Markdown keeps headings, lists, and tables in plain characters. Paxa OCR Lite [returns each page](https://paxalabs.com/docs/ocr) as GitHub-flavored Markdown in reading order, with headings, lists, and tables, or as typed blocks of the same reading. One request reads up to 50 pages and 10 MiB of PDF, PNG, JPEG, or WebP. [Mistral OCR](https://docs.mistral.ai/capabilities/document_ai/basic_ocr) returns Markdown per page, with tables inline or as separate Markdown or HTML, and extracts the page's images beside the text. [LlamaParse](https://developers.llamaindex.ai/llamaparse/parse/) returns Markdown with headings and tables on every tier except Fast, which returns text without Markdown. [Azure Document Intelligence's Layout model](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/prebuilt/layout) can return Markdown, with tables written as HTML in v4.0. [Feeding Thai documents to an LLM](https://paxalabs.com/guides/thai-documents-for-llms) and [the search index guide](https://paxalabs.com/guides/thai-pdf-to-search-index) take the Markdown from there, and [the knowledge bases page](https://paxalabs.com/use-cases/knowledge-bases) shows the workflow. ## For fields you define Document extraction returns named values, such as a seller's tax ID or an invoice total, ready for a database row. Paxa Document Extraction [fills a typed schema](https://paxalabs.com/docs/extraction) of up to 100 leaf fields over up to 20 pages, in one response. Each value is text printed in the document, or null with the reason. It returns no confidence score. [Mistral's annotations](https://docs.mistral.ai/capabilities/document_ai/annotations) fill a Pydantic, Zod, or JSON schema for the whole document or for each image region, with nested objects, arrays, and enums. [LlamaParse Extract](https://developers.llamaindex.ai/llamaparse/general/limitations/) fills a JSON schema of up to 5,000 properties as a job you poll, and [can attach citations and a confidence score](https://developers.llamaindex.ai/llamaparse/extract/guides/response-format/) to each field. Write the same fields into each vendor's schema and run one real document through all three. Then compare which fields came back empty or wrong, and why. [The Document Extraction page](https://paxalabs.com/document-extraction) shows one Thai tax invoice filled in. ## For Thai invoices, receipts, and ID numbers [Azure Document Intelligence's prebuilt invoice model](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/language-support/prebuilt) lists Thai among its languages, with THB among its currencies, and its receipt model lists Thai too. [Amazon Textract's FAQs](https://aws.amazon.com/textract/faqs/) state that it processes invoices, receipts, and identity documents in English only. Paxa Document Extraction has built-in types for Thai documents. A 13-digit Thai identifier is verified by its check digit, and provinces, banks, insurers, and legal forms resolve against tables the service ships. Dates read in either the Buddhist or the Common Era. Whichever service reads the card or the invoice, keep a person on the fields that move money, such as the payee, the amount, and the account number. The [invoices](https://paxalabs.com/use-cases/invoices), [identity documents](https://paxalabs.com/use-cases/identity), and [contracts](https://paxalabs.com/use-cases/contracts) pages show each workflow. ## For handwritten forms [Azure Document Intelligence's v4.0 handwriting table](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/language-support/ocr) lists twelve languages, Thai among them, for its Read and Layout models. [Amazon Textract](https://docs.aws.amazon.com/textract/latest/dg/limits-document.html) reads handwriting in English only. Test with forms filled in by several people, photographed the way your users will photograph them. ## For Office files and very long documents [Azure Document Intelligence's Read and Layout models](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/service-limits) accept DOCX, PPTX, and XLS beside PDFs and images, and its Standard tier analyzes documents of up to 2,000 pages and 500 MB. [Mistral OCR](https://docs.mistral.ai/capabilities/document_ai/basic_ocr) accepts PPTX and DOCX, up to 50 MB and 1,000 pages. [LlamaParse](https://developers.llamaindex.ai/llamaparse/parse/) reads more than 130 file types, and [Parse takes files of up to 512 MB](https://developers.llamaindex.ai/llamaparse/general/limitations/) with no page cap. Paxa OCR Lite reads PDF, PNG, JPEG, and WebP, up to 50 pages and 10 MiB per request. Export an Office file to PDF first, and split a longer document into parts. ## For running on your own infrastructure Some teams must read documents inside their own network. [Azure Document Intelligence's container guide](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/containers/install-run) runs the Read and Layout models, and the invoice, receipt, and ID document models, as Docker containers on premises. [LlamaIndex's self-hosting page](https://developers.llamaindex.ai/llamaparse/self_hosting/) runs Parse and Extract on your own Kubernetes cluster through Helm charts, on Enterprise plans. [Mistral's OCR 4 announcement](https://mistral.ai/news/ocr-4/) describes a single-container deployment for self-hosting, available to enterprise customers. ## Jobs and vendors at a glance | Job | Vendors whose documentation states the fit | | --- | --- | | Thai PDFs to Markdown | Azure Document Intelligence, LlamaParse, Mistral OCR, Paxa OCR Lite | | A schema you write | LlamaParse Extract, Mistral annotations, Paxa Document Extraction | | Thai invoices, receipts, and ID numbers | Azure Document Intelligence, Paxa Document Extraction | | Thai handwriting | Azure Document Intelligence | | Office files and long documents | Azure Document Intelligence, LlamaParse, Mistral OCR | | Your own infrastructure | Azure Document Intelligence, LlamaParse, Mistral OCR | ## Test on your own documents - Collect real pages, such as a clean digital PDF, a phone photo, a faxed form, and a page with a table. - Run the same pages through each vendor listed under your job. - Check the Thai marks first, meaning the vowels and tone marks above and below each consonant and the order they come back in. - Check Thai digits, Buddhist Era years, and amounts against the printed page. - For extraction, list the fields that came back empty or wrong, and read why. - See a Thai tax invoice read into Markdown and into fields on [the OCR page](https://paxalabs.com/ocr). ## Common questions ### Which Thai OCR is best? The one that fits your job. Match your documents and the output you need to a job above, then run your own files through the vendors it names. ### What is the difference between OCR and document extraction? OCR returns the text of each page, as Markdown or as structured blocks. Document extraction fills named fields, such as an invoice number or a date, from the same file. Paxa OCR Lite does the first and Paxa Document Extraction the second. ### Can one pipeline use more than one of these services? Yes. Route each document by its job, for example slide decks to one service and scanned Thai forms to another. Your code then reads one response shape per service. ### Where are the prices? On [the OCR API comparison](https://paxalabs.com/compare/ocr-apis), which lists each vendor's published rate per 1,000 pages, re-checked every month. ### Do you publish accuracy comparisons? No. We publish no accuracy, quality, or speed figures for any model. Run the same documents through each service you are considering and compare the output. # Best AI voice generator for YouTube narration, faceless channels, and Shorts Which AI voice generator fits a narrated explainer, a faceless channel, a Short, captions, a second language, or a clone of your own voice, from each tool's own pages. Published 2026-10-17, updated 2026-10-23. Paxa Labs. A YouTube channel that runs on narration needs a voice it can use every week. A ten-minute explainer, a faceless channel that uploads daily, and a Short each ask something different of the voice and of the tool that makes it. Every tool fact below links that tool's own page, as we read it in October 2026. No tool is scored or ranked, and a tool appears under a job only where its own page states the fit. ## For narrated explainers and long videos Every voice on Paxa TTS Flash v2 speaks English, Thai, and Mandarin. The catalog holds 50 of them, and [the voices page](https://paxalabs.com/voices) plays a sample of each. Paste a script into [the Speech tool](https://paxalabs.com/app/speech), pick a voice, and download MP3, Opus, or WAV. One run reads up to 5,000 characters, about 5 minutes of English narration. A longer script goes in as parts that you join in your editor, and a paragraph break is a natural place to cut. The catalog casts these voices for documentaries, lessons, and explainers. | Voice | Role | Cast for | | --- | --- | --- | | [Khanom Krok](https://paxalabs.com/voices/khanomkrok) | Lead voice | Calm, unhurried elder voice and the roster's male lead: documentary, heritage storytelling, and narration. | | [Foi Thong](https://paxalabs.com/voices/foithong) | Narrator | Captivating narrator for long reads and explainers. | | [Kaprao](https://paxalabs.com/voices/kaprao) | Easygoing | Relaxed male voice for dialogue, voice assistants, and everyday explainers. | | [Matum](https://paxalabs.com/voices/matum) | Documentary | Unhurried, precise voice-over for documentaries, travel films, and museum guides. | | [Chu Chi](https://paxalabs.com/voices/chuchi) | Professor | Clear, knowledgeable lecturer for classes, e-learning, and explainers. | [ElevenLabs Studio](https://elevenlabs.io/docs/product-guides/products/studio) has video projects that combine video, images, speech, music, sound effects, text, and captions. [Narakeet](https://www.narakeet.com/create/text-to-speech-for-youtube-videos.html) turns a PowerPoint deck into a narrated video from a voiceover written in the presenter notes. It also exports the narration alone as MP3 or WAV. ## For a faceless channel with one steady voice A faceless channel is known by its narrator. Regular viewers learn the voice the way they would learn a host's face, and a new voice on every upload throws that away. Choose one voice and keep it. [ElevenLabs' Voice Library](https://elevenlabs.io/docs/overview/capabilities/voices) holds over 10,000 voices shared by its community. [Speechify Studio](https://speechify.com/pricing-studio) lists access to more than 1,000 voices on its free and paid plans. [Narakeet](https://www.narakeet.com/create/text-to-speech-for-youtube-videos.html) offers 900 voices in 100 languages for YouTube narration. Each Paxa TTS Flash v2 voice has its own page with a published sample, to hear before you commit a channel to it. [The creators page](https://paxalabs.com/use-cases/creators) shows a faceless channel putting one voice to work across its uploads. ## For Shorts voiceover A Short has room for a few sentences. Put the payoff in the first one and write the script to the length of the clip. [The short-video voiceover guide](https://paxalabs.com/guides/ai-voiceover-for-tiktok-reels-and-shorts) gives a script length for each clip length. In [the Speech tool](https://paxalabs.com/app/speech), the speed control quickens a read for a fast clip, and the price is the same at every speed. On Paxa TTS Flash v2, a direction in square brackets such as `[excited]` changes how the words after it are read. The catalog casts these voices for promos, shorts, recaps, and reviews. | Voice | Role | Cast for | | --- | --- | --- | | [Nom Yen](https://paxalabs.com/voices/nomyen) | Lead voice | Bright, energetic voice and the roster's female lead: promos, social clips, and everyday product speech. | | [Moo Ping](https://paxalabs.com/voices/mooping) | Street energy | Confident, streetwise voice for ads and shorts. | | [Tom Yum](https://paxalabs.com/voices/tomyum) | Best friend | Fast, hot, chatty best-friend voice for drama recaps, entertainment news, and social clips. | | [Salim](https://paxalabs.com/voices/salim) | Girl next door | Easy, chatty everyday voice for vlogs, reviews, and lifestyle clips. | | [Khai Jiao](https://paxalabs.com/voices/khaijiao) | Hype commentator | High-energy commentator for esports, match highlights, and launch trailers. | [CapCut's AI voice reader](https://www.capcut.com/tools/ai-voice-reader) works inside the editor where the Short is cut. It drops the voiceover onto the timeline and aligns it to the subtitles automatically. ## For captions from the same voiceover A viewer watching with the sound off gets only the captions. Upload the finished voice track to [the Transcribe tool](https://paxalabs.com/app/transcribe) and ask for subtitles. It returns an SRT or VTT file with the timing of every line. [Narakeet's YouTube page](https://www.narakeet.com/create/text-to-speech-for-youtube-videos.html) says it generates subtitle and closed caption files automatically. Video projects in [ElevenLabs Studio](https://elevenlabs.io/docs/product-guides/products/studio) carry captions beside the speech. [CapCut](https://www.capcut.com/tools/ai-voice-reader) aligns its voiceover to the subtitles already on its timeline. ## For a channel in more than one language Every Paxa TTS Flash v2 voice reads English, Thai, and Mandarin in the same voice. One narrator can carry an English channel and a Thai or Chinese channel beside it, from a script written in each language. [ElevenLabs Dubbing](https://elevenlabs.io/docs/product-guides/products/dubbing) lists more than 90 languages and keeps the original speakers' voices in the dub. Its Paste URL option takes a video straight from YouTube. [Speechify Studio](https://speechify.com/pricing-studio) lists a Dubbing Studio on its free and paid plans, and [Narakeet](https://www.narakeet.com/create/text-to-speech-for-youtube-videos.html) offers voices in 100 languages. ## For a clone of your own voice [ElevenLabs' voices documentation](https://elevenlabs.io/docs/overview/capabilities/voices) describes Instant Voice Cloning from samples under two minutes, and Professional Voice Cloning trained on extended audio. [CapCut's custom voice page](https://www.capcut.com/tools/custom-ai-voice) takes an upload of your own voice and learns its tone and speech pattern. Paxa Labs offers no voice cloning. [Voice design](https://paxalabs.com/voice-design) builds a new voice from a written description instead, which you save and reuse on every video. Impersonating a real person without their consent is against [the rules every account accepts](https://paxalabs.com/responsible-ai). ## Before the channel earns from it The free credits on a new Paxa account are for personal use and evaluation. Buy any plan or credit pack, and the account's audio is yours to use commercially, including audio made before the purchase. A monetized channel or a sponsored video belongs on a paid account. [The Terms](https://paxalabs.com/terms#content-and-output) state the rule. [ElevenLabs' help center](https://elevenlabs.io/docs/help-center/legal/can-i-publish-the-content-i-generate-on-the-platform) says its free plan includes no commercial license, and that content made outside a paid subscription, before or after one, cannot be used commercially. [Speechify Studio](https://speechify.com/pricing-studio) lists commercial usage rights on its Starter plan and none on its free plan. [Murf's help center](https://help.murf.ai/do-i-have-commercial-rights-over-the-voice-over-created) says all its paid plans carry commercial rights over voiceovers made in its Studio. [Narakeet](https://www.narakeet.com/docs/usage-rights-copyright/) allows commercial use of content created on a commercial account and none on a free account. Read YouTube's own rules on AI-made content before the first upload. [The short-video voiceover guide](https://paxalabs.com/guides/ai-voiceover-for-tiktok-reels-and-shorts) quotes its disclosure rule with the source. ## Jobs and tools at a glance | Job | Tools whose own pages state the fit | | --- | --- | | Narrated explainers and long videos | ElevenLabs Studio, Narakeet, Paxa TTS Flash v2 | | A faceless channel | ElevenLabs, Narakeet, Speechify Studio, Paxa TTS Flash v2 | | Shorts voiceover | CapCut, Paxa TTS Flash v2 | | Captions | CapCut, ElevenLabs Studio, Narakeet, Paxa Labs | | More than one language | ElevenLabs Dubbing, Narakeet, Speechify Studio, Paxa TTS Flash v2 | | A clone of your own voice | CapCut, ElevenLabs | | Commercial rights on a paid plan | ElevenLabs, Murf, Narakeet, Speechify Studio, Paxa TTS Flash v2 | ## Test on a real script - Take the script of a video you have already planned, with a name, a number, and a term from your niche in it. - Run it through two or three tools listed under your job, on voices of the same gender. - Listen on a phone speaker and on headphones, where your viewers will hear it. - Check the license on the plan you will publish from before the first upload. - Hear 9 Paxa TTS Flash v2 voices with no account on [the free text to speech page](https://paxalabs.com/tts). ## Common questions ### Which AI voice generator fits a YouTube channel? The one that fits the channel's main job. Find that job above, then test the two or three tools it names on a real script. ### Can I use free credits on a monetized channel? No. The free credits cover personal use and evaluation. Buy any plan or credit pack, and the audio is yours to use commercially, including audio made before the purchase. ### Does Paxa Labs clone voices? No. [Voice design](https://paxalabs.com/voice-design) builds a new voice from a written description instead. ElevenLabs documents voice cloning, and CapCut builds a custom voice from a recording you upload. ### Where are the prices? Our plans and credit packs are on [the pricing page](https://paxalabs.com/pricing). Each tool above publishes its own plans on its own site. ### Do you publish quality comparisons? No. We publish no accuracy, quality, or latency figures, for our models or anyone else's. Listen to each candidate on your own script before you choose. # Best free text to speech for personal projects and trying voices before you pay Which free text to speech fits hearing a line with no account, listening to a page, a video edit, a personal project, or a trial, and what each license allows. Published 2026-10-17. Paxa Labs. Free text to speech serves several needs. Some people want to hear one line read aloud right now. Others want a voice for a school presentation or a family video, or a proper trial of a voice before they pay for it. On a free tier, the license decides what you can do with the audio. Every tool fact below links that tool's own page, as we read it in October 2026, and no tool is scored or ranked. ## For hearing a line with no account [The free text to speech page](https://paxalabs.com/tts) reads a line you type in 9 Paxa TTS Flash v2 voices, with no account. Each of them speaks English, Thai, and Mandarin. [Narakeet's AI voice generator](https://www.narakeet.com/create/ai-voice-generator.html) makes 20 audio files free without registering. [Its pricing page](https://www.narakeet.com/docs/pricing/) caps a free script at 1 KB and says previews deduct no credits. ## For listening to a page yourself Sometimes the job is to listen to a page, with nothing to publish. Two readers come built into a browser and an operating system. [Microsoft Edge](https://support.microsoft.com/en-us/edge/use-immersive-reader-in-microsoft-edge) has Read aloud on its Reading mode toolbar, which reads the text of a webpage aloud. Its voice options change the reader's voice and pace. On [a Mac](https://support.apple.com/guide/mac-help/mh27448/mac), Speak selection reads selected text when you press Option-Esc, the default shortcut. ## For trying voices before you pay A new Paxa account gets 100 free credits, granted once. They voice about 11 minutes of English narration across any of the 50 voices in [the Speech tool](https://paxalabs.com/app/speech), enough to hear a voice read a whole script. [ElevenLabs' pricing page](https://elevenlabs.io/pricing) lists text to speech and Studio projects on its free plan. [Speechify Studio](https://speechify.com/pricing-studio) lists a Voiceover Studio, a Dubbing Studio, and access to more than 1,000 voices on its free plan. A free [Narakeet](https://www.narakeet.com/docs/pricing/) account runs 20 conversions. ## For a voiceover inside a video editor [CapCut's text to speech page](https://capcut.com/tools/ai-text-to-speech) says its core text to speech is free for all users, with some premium voices on its Pro subscription. [Its AI voice reader](https://www.capcut.com/tools/ai-voice-reader) drops the voiceover onto the editing timeline and aligns it to the subtitles. ## For personal projects A school presentation, a bedtime story for your own children, and a video for a family event are personal projects. Each free tier sets its own conditions on them. On Paxa, the free credits cover personal use and evaluation. [ElevenLabs' help center](https://elevenlabs.io/docs/help-center/legal/can-i-publish-the-content-i-generate-on-the-platform) asks that content published from its free plan carry `elevenlabs.io` or `11.ai` in its title. [Narakeet's usage rights page](https://www.narakeet.com/docs/usage-rights-copyright/) allows no commercial use of content made on a free account, and [Speechify Studio](https://speechify.com/pricing-studio) lists no commercial usage rights on its free plan. ## When a project starts to earn A monetized channel, an audiobook for sale, an ad, and a client job are commercial use. On Paxa, the free credits are for personal use and evaluation only. Buy any plan or credit pack, and all of the account's audio is yours to use commercially, including audio made before the purchase. [The Terms](https://paxalabs.com/terms#content-and-output) state the rule. [ElevenLabs' help center](https://elevenlabs.io/docs/help-center/legal/can-i-publish-the-content-i-generate-on-the-platform) says content made outside a paid subscription, before or after one, cannot be used commercially. [Murf's help center](https://help.murf.ai/do-i-have-commercial-rights-over-the-voice-over-created) says all its paid plans carry commercial rights over voiceovers made in its Studio. [Speechify Studio](https://speechify.com/pricing-studio) lists commercial usage rights on its Starter plan. [Narakeet](https://www.narakeet.com/docs/usage-rights-copyright/) allows commercial use of content created on a commercial account. [CapCut's text to speech page](https://capcut.com/tools/ai-text-to-speech) says the audio it makes can be used for commercial projects. ## Jobs and tools at a glance | Job | Tools whose own pages state the fit | | --- | --- | | Hearing a line with no account | Narakeet, Paxa TTS Flash v2 | | Listening to a page yourself | Mac Speak selection, Microsoft Edge Read aloud | | Trying voices before you pay | ElevenLabs, Narakeet, Speechify Studio, Paxa TTS Flash v2 | | A voiceover inside a video editor | CapCut | | Commercial use | CapCut, ElevenLabs, Murf, Narakeet, Speechify Studio, Paxa TTS Flash v2 | ## Make a free tier count - Try your real script, with the names and numbers it holds, before you judge a voice. - Play the samples first. Previews cost nothing on Paxa and on Narakeet. - Read the free tier's license before you publish anything made on it. - Buy before a project earns, or confirm that the tool's license covers audio made earlier. - Hear 9 voices on [the free text to speech page](https://paxalabs.com/tts), then try all 50 in [the Speech tool](https://paxalabs.com/app/speech) with the free credits. ## Common questions ### Is there free text to speech with no sign-up? Yes. [The free text to speech page](https://paxalabs.com/tts) reads your line in 9 voices with no account, and Narakeet's generator makes 20 files without registering. ### Can I use free text to speech on a YouTube channel? A monetized channel is commercial use. On Paxa, the free credits cover personal use and evaluation, and any purchase covers the account's audio, including videos voiced before it. Each other tool sets its own free license, linked above. ### Do the free credits renew? No. A new account gets 100 credits once. Plans and credit packs add more, and [the pricing page](https://paxalabs.com/pricing) lists them. ### Which voices can I try for free? All 50 Paxa TTS Flash v2 voices, in [the Speech tool](https://paxalabs.com/app/speech) with the free credits. 9 of them also play on [the free text to speech page](https://paxalabs.com/tts) with no account. # Best text to speech for audiobooks and long-form narration Which text to speech tool fits a whole book, a narrator, fiction with dialogue, editions in more than one language, or a book you plan to sell, from each tool's own pages. Published 2026-10-17, updated 2026-10-23. Paxa Labs. An audiobook keeps a listener with one voice for hours. A narrator who sounds right on one page can tire by the third chapter, and a tool built for short clips can make a whole book slow to assemble. Every tool fact below links that tool's own page, as we read it in October 2026. No tool is scored or ranked, and a tool appears under a job only where its own page states the fit. ## For a whole book, chapter by chapter [ElevenLabs Studio](https://elevenlabs.io/docs/product-guides/products/studio) has audio projects for long-form narration, podcasts, and audiobooks. A project holds up to 500 chapters in a sidebar. It exports as one file, or as a ZIP with a file for each chapter. [Narakeet's audiobook page](https://www.narakeet.com/create/text-to-voice-audiobooks.html) takes a book's chapters as Word documents, EPUB files, or PDFs, and converts them into audio. The result downloads as MP3, WAV, or M4A. In [the Speech tool](https://paxalabs.com/app/speech), one run reads up to 5,000 characters, about 5 minutes of English narration. A chapter goes in as several runs, and you join the clips in an audio editor. Download WAV when the chapter will be edited and mastered there. ## For choosing a narrator Every Paxa TTS Flash v2 voice speaks English, Thai, and Mandarin. The catalog casts these for narration, long reads, audio novels, and lessons. | Voice | Role | Cast for | | --- | --- | --- | | [Khanom Krok](https://paxalabs.com/voices/khanomkrok) | Lead voice | Calm, unhurried elder voice and the roster's male lead: documentary, heritage storytelling, and narration. | | [Foi Thong](https://paxalabs.com/voices/foithong) | Narrator | Captivating narrator for long reads and explainers. | | [Espresso](https://paxalabs.com/voices/espresso) | Long-form reader | Deep, dark, unhurried English reader for long fiction, documentaries, and trailers. | | [Oolong](https://paxalabs.com/voices/oolong) | Audio fiction | Low, rich Mandarin narrator for audio novels, brand reads, and late-night stories. | | [Matum](https://paxalabs.com/voices/matum) | Documentary | Unhurried, precise voice-over for documentaries, travel films, and museum guides. | | [Chu Chi](https://paxalabs.com/voices/chuchi) | Professor | Clear, knowledgeable lecturer for classes, e-learning, and explainers. | Play a whole page in each candidate before you decide, because a voice that suits one sentence can tire over a chapter. [The narrator voice page](https://paxalabs.com/narrator-voice) plays narrators reading in English, Thai, and Mandarin. [ElevenLabs' Voice Library](https://elevenlabs.io/docs/overview/capabilities/voices) holds over 10,000 voices shared by its community, and its Voice Design builds a new voice from attributes such as age, gender, accent, and tone. [Narakeet](https://www.narakeet.com/create/text-to-voice-audiobooks.html) offers 900 voices in 100 languages for an audiobook. ## For fiction with dialogue and characters [ElevenLabs Studio](https://elevenlabs.io/docs/product-guides/products/studio) can assign several voices within a single paragraph, each marked in its own color. It can also replace one voice across a whole project. On Paxa TTS Flash v2, a direction in square brackets changes how the text after it is read. `[whispering]` before a line of dialogue, or `[sad]` before a farewell, holds until the next direction. Each run speaks in one voice. Voice each character's lines in that character's voice, then lay the clips in order. The catalog casts these voices for folk tales, love stories, fantasy, and audio drama. | Voice | Role | Cast for | | --- | --- | --- | | [Hor Mok](https://paxalabs.com/voices/hormok) | Fantasy knight | Noble, resonant voice for fantasy heroes, epic audio drama, and games. | | [Krayasat](https://paxalabs.com/voices/krayasat) | Grandfather | Unhurried, kindly grandfather voice for bedtime stories, folk tales, and life lessons. | | [Miang Kham](https://paxalabs.com/voices/miangkham) | Grandmother | Stern, commanding elder voice for grandmother roles, folk tales, and ghost stories. | | [Chor Muang](https://paxalabs.com/voices/chormuang) | Bashful | Hesitant, heartfelt voice for love stories, visual novels, and tender scenes. | | [Khao Lam](https://paxalabs.com/voices/khaolam) | Soft romantic | Low, gentle male voice for love stories, audio drama, and sleep tracks. | | [Kaeng Pa](https://paxalabs.com/voices/kaengpa) | Villain | Rough, menacing villain voice for games, audio drama, and animation. | | [Makham](https://paxalabs.com/voices/makham) | Villainess | Cold, cutting voice for villainesses, sorceresses, and dark fantasy. | [The audio novels page](https://paxalabs.com/use-cases/audio-novels) gives each character of a serial its own catalog voice, and [the voice acting page](https://paxalabs.com/voice-acting) plays directions written into real lines. ## For a book in English, Thai, or Mandarin Every Paxa TTS Flash v2 voice reads English, Thai, and Mandarin in the same voice. A book published in two or three languages can keep one narrator across its editions, voiced from the translated text of each. [ElevenLabs' models page](https://elevenlabs.io/docs/overview/models) lists 70+ languages for Eleven v3 and 90+ for the Eleven v4 family. [Narakeet](https://www.narakeet.com/create/text-to-voice-audiobooks.html) offers voices in 100 languages. ## For a book you plan to sell The free credits on a new Paxa account are for personal use and evaluation. Buy any plan or credit pack, and the account's audio is yours to use commercially, including chapters voiced before the purchase. A book for sale belongs on a paid account. [The Terms](https://paxalabs.com/terms#content-and-output) state the rule. [ElevenLabs' help center](https://elevenlabs.io/docs/help-center/legal/can-i-publish-the-content-i-generate-on-the-platform) says every paid plan includes a commercial license, provided you are not using its Beta Services. It also says content made outside a paid subscription, before or after one, cannot be used commercially. [Murf's help center](https://help.murf.ai/do-i-have-commercial-rights-over-the-voice-over-created) says all its paid plans carry commercial rights over voiceovers made in its Studio, and [those rights stay valid](https://help.murf.ai/will-my-commercial-rights-and-business-license-be-valid-after-my-subscription-has-ended) after a subscription ends. A book sells for years, and that term matters. [Narakeet](https://www.narakeet.com/docs/usage-rights-copyright/) allows commercial use of content created on a commercial account. [Speechify Studio](https://speechify.com/pricing-studio) lists commercial usage rights on its Starter plan and none on its free plan. Read the current terms of every store you will sell through before you upload. ## Jobs and tools at a glance | Job | Tools whose own pages state the fit | | --- | --- | | A whole book in chapters | ElevenLabs Studio, Narakeet, Paxa TTS Flash v2 | | Choosing a narrator | ElevenLabs, Narakeet, Paxa TTS Flash v2 | | Fiction with dialogue | ElevenLabs Studio, Paxa TTS Flash v2 | | Editions in more than one language | ElevenLabs, Narakeet, Paxa TTS Flash v2 | | Commercial rights on a paid plan | ElevenLabs, Murf, Narakeet, Speechify Studio, Paxa TTS Flash v2 | ## Test on a real chapter - Pick one chapter that holds dialogue, a list, a number, and a name from another language. - Run it through two or three tools listed under your job, and hear each whole chapter in one sitting. - Listen on earbuds and on a car speaker, two places an audiobook is heard. - Check the license on the plan you will publish from, and the terms of the store you will sell through. - Hear 9 Paxa TTS Flash v2 voices with no account on [the free text to speech page](https://paxalabs.com/tts). ## Common questions ### Which text to speech fits an audiobook? The one that fits your book's main job. Find that job above, then test the two or three tools it names on a real chapter. ### Can I sell an audiobook made with free credits? Yes, once the account has made any purchase. The free credits alone cover personal use and evaluation. Buy any plan or credit pack, and the account's audio is yours to use commercially, including chapters voiced before the purchase. ### How much text does one run read? Up to 5,000 characters, about 5 minutes of English narration. A chapter goes in as several runs that you join in an audio editor. ### Does Paxa Labs clone a narrator's voice? No. Paxa Labs clones no voice; a narrator of your own comes from [voice design](https://paxalabs.com/voice-design), built from a written description. ElevenLabs documents Instant and Professional Voice Cloning. ### Do you publish quality comparisons? No. We publish no accuracy, quality, or latency figures, for our models or anyone else's. Listen to each candidate on your own chapter before you choose. # Paxa TTS Flash v2 is available now Paxa TTS Flash v2, nicknamed Pixie, performs the directions written into a line, such as a whisper or a laugh, in 50 voices that each speak English, Thai, and Mandarin, at the price of Paxa TTS Flash. Published 2026-10-16, updated 2026-10-23. Paxa Labs. A script tells a voice actor how each line should sound, whispered or close to tears. A speech model that reads every line in one even tone leaves those notes on the page, and the performance has to come from a recording session. Today we are releasing Paxa TTS Flash v2. Inside the lab it goes by Pixie, and the nickname sits beside the model name in the console's Speech tool. Pixie performs the notes a script carries. A direction in square brackets, written in English, Thai, or Chinese, sets how the words after it are delivered, and the delivery can change several times inside one sentence. Paxa TTS Flash v2 is available now on every speech route of the API and in the console, at $10 per million characters. That is the rate of Paxa TTS Flash, and requests to Paxa TTS Flash keep working as they do today. The launch film. Every voice in it was generated from the text on screen.: https://paxalabs.com/video/paxa-tts-flash-v2-launch-en.mp4 ## What changed from Paxa TTS Flash | | Paxa TTS Flash | Paxa TTS Flash v2 | | --- | --- | --- | | Delivery | Emotion tags such as `[happy]`, on 2 experimental voices | Directions in any words, on every voice | | Sounds | None | Sounds such as `[laughs]`, played where written | | Hesitations | None | `[um]` and `[hmm]`, spoken as words | | Emphasis | None | `` and `` spans | | Languages | Each voice made for one of English, Thai, and Mandarin | Every voice speaks all three | | Voices | 34 | 50 | | mp3 | 24 kHz, 64 kbps | 44.1 kHz, 128 kbps | | opus | 48 kHz, 48 kbps | 48 kHz, 64 kbps | | Price per 1,000 characters | 10 credits | 10 credits | ## One sentence on each model Foi Thong reads the same sentence twice. On Paxa TTS Flash the request carries the text alone. On Paxa TTS Flash v2 it carries two directions, `[whispering]` before the first half and `[ecstatic]` before the second. Foi Thong, one sentence on each model: - Paxa TTS Flash: “I can't believe it. We actually did it!” https://paxalabs.com/audio/takes/v1.m4a - Paxa TTS Flash v2: “[whispering] I can't believe it. [ecstatic] We actually did it!” https://paxalabs.com/audio/takes/v2.m4a ## Directions in plain words A direction is any words in square brackets, such as `[sad]`, `[speaking slowly, close to the microphone]`, or `[เสียงสดใส]`. It can be one word or a description of the whole read, as in `[product advertisement, punchy and upbeat]` or `[ผู้ประกาศข่าว น้ำเสียงเป็นทางการ]`. A direction itself is not spoken. A direction holds until the next one. Text before the first direction keeps the voice's own delivery, and `[neutral]` returns to it. Case, extra spaces, and full-width brackets make no difference: `[Sad]`, `[ sad ]`, and `[sad]` are one direction. The first line below changes delivery four times in one sentence. Foi Thong starts in a whisper, turns excited at a weight of 2, breaks into tears, and ends on a laugh. Directions inside a line: - Foi Thong: “[whispering] I was not going to tell anyone, [excited:2] but we got the house, [crying] after three years of saving. [laughs]” https://paxalabs.com/audio/takes/sw-2.m4a - Mocha: “[whispering] Thirty seconds. Don't touch anything.” https://paxalabs.com/audio/takes/heist-2.m4a - Mocha: “[shouting urgently] Run! Go, go, go!” https://paxalabs.com/audio/takes/heist-4.m4a - Sanae Chan: “[เสียงสั่น] กลับมาแล้วเหรอ [กลั้นน้ำตา] ฉันนึกว่าคุณจะไม่กลับมาแล้ว” https://paxalabs.com/audio/takes/novel-3.m4a ### Weights and blends A weight after a colon sets how strongly a direction or a sound applies, written as a number or a percentage: `[sad:0.5]`, `[sad:50%]`, `[laughs:0.3]`. A plus sign blends directions, each with its own weight, as in `[happy:0.7+sad:0.3]`. ## Sounds, hesitations, and emphasis Sounds play where they are written: `[sighs]`, `[laughs]`, `[chuckles]`, `[exhales sharply]`, `[gasps]`, `[inhales deeply]`, `[giggles]`, `[sobs]`, `[yawns]`, and `[clears throat]`, along with spellings such as `[inhales]` and `[scoffs]`. `[um]` and `[hmm]` are hesitations. Each is spoken as the word inside its brackets, and Thai spellings such as `[เอ่อ]` read the same way. `` puts the stress of a sentence on the words inside it, and `` whispers them. Its close returns the line to the delivery in force before the mark. Emphasis can also be written as a bracket, `[emphasize]` or `[เน้นเสียง]`. One stressed word: - Cookie: “[firm] I didn't eat your cookie.” https://paxalabs.com/audio/takes/em-cookie.m4a ## What it will not do A tag the model cannot perform is refused before anything is charged, with `400 tag_invalid`. A text that is only tags, with no words to speak, answers `422 unspeakable_text`, also before the charge. | Asked for | Examples | | --- | --- | | An accent | `[british accent]`, `[สำเนียงอีสาน]`, `[北京口音]` | | A different voice | `[robotic]`, `[old man voice]`, `[เสียงเด็ก]` | | Singing | `[singing]`, `[ร้องเพลง]`, `[唱歌]` | | A sound effect | `[door slams]`, `[applause]` | | A language | `[in thai]`, `[english]`, `[中文]` | | A sound outside the list | `[sniffs]`, `[gulps]`, `[coughs]`, and `[sneezes]` | The [direction guide](https://paxalabs.com/docs/directions) lists every sound, spelling, and refused tag. ## 50 voices, each in three languages Paxa TTS Flash v2 has 50 voices: 26 carried over from Paxa TTS Flash under the same ids, and 24 new. On Paxa TTS Flash, each voice is made for one language. On Paxa TTS Flash v2, every voice speaks English, Thai, and Mandarin, and one voice can read a product in all three markets. Pixie reads mixed-language text in one voice. Each word is read in the language of its script. A Thai sentence that names an English product, or a Mandarin notice with a flight number in it, goes out as one request. The optional `language` field sets how codes, digits, and symbols are read, such as AB2039 with Thai letter names under `"th"`. One ad in three languages, then one agent in two: - Khanom Krok: “[product advertisement, punchy and upbeat] New season. New colors. In stores this Friday.” https://paxalabs.com/audio/takes/loc-en.m4a - Khanom Krok: “[โฆษณาสินค้า น้ำเสียงกระฉับกระเฉง] ซีซันใหม่ สีใหม่ วางขายวันศุกร์นี้” https://paxalabs.com/audio/takes/loc-th.m4a - Khanom Krok: “[广告配音,热情洋溢] 新一季,新色彩,本周五上市。” https://paxalabs.com/audio/takes/loc-zh.m4a - Cookie: “[apologetic, sincere] I'm so sorry about the wait. I've moved you to TG628 at 15:40, at no charge.” https://paxalabs.com/audio/takes/call-2.m4a - Cookie: “[亲切的客服语气] 王太太您好,航班已改为TG628,15:40在12号登机口登机。” https://paxalabs.com/audio/takes/call-4.m4a ### The 24 new voices | Voice | Cast for | | --- | --- | | [Saku](https://paxalabs.com/voices/saku) | AI assistant | | [Satay](https://paxalabs.com/voices/satay) | Anime hero | | [Thong Yod](https://paxalabs.com/voices/thongyod) | Anime heroine | | [Bai Toey](https://paxalabs.com/voices/baitoey) | Mentor | | [Kluay Tod](https://paxalabs.com/voices/kluaytod) | Cartoon sidekick | | [Matum](https://paxalabs.com/voices/matum) | Documentary | | [Hor Mok](https://paxalabs.com/voices/hormok) | Fantasy knight | | [Nam Manao](https://paxalabs.com/voices/nammanao) | Fitness coach | | [Khao Tom](https://paxalabs.com/voices/khaotom) | Family man | | [Salim](https://paxalabs.com/voices/salim) | Girl next door | | [Krayasat](https://paxalabs.com/voices/krayasat) | Grandfather | | [Miang Kham](https://paxalabs.com/voices/miangkham) | Grandmother | | [Tod Mun](https://paxalabs.com/voices/todmun) | Host and MC | | [Nam Prik](https://paxalabs.com/voices/namprik) | Husky alto | | [Khai Jiao](https://paxalabs.com/voices/khaijiao) | Hype commentator | | [Chu Chi](https://paxalabs.com/voices/chuchi) | Professor | | [Krong Kraeng](https://paxalabs.com/voices/krongkraeng) | Robot | | [Chor Muang](https://paxalabs.com/voices/chormuang) | Bashful | | [Khao Lam](https://paxalabs.com/voices/khaolam) | Soft romantic | | [Pla Thu](https://paxalabs.com/voices/plathu) | Student | | [Kaeng Phet](https://paxalabs.com/voices/kaengphet) | Trailer voice | | [Kaeng Pa](https://paxalabs.com/voices/kaengpa) | Villain | | [Makham](https://paxalabs.com/voices/makham) | Villainess | | [Pui Fai](https://paxalabs.com/voices/puifai) | Virtual idol | Each voice's sample plays on the [voices page](https://paxalabs.com/voices). 25 pages play a whole scene in these voices, from [anime voices](https://paxalabs.com/anime-voices) and [bedtime stories](https://paxalabs.com/bedtime-stories) to [sports commentary](https://paxalabs.com/sports-commentary) and a [robot voice](https://paxalabs.com/robot-voice). ## Scenes from the film ### A support call A caller is frustrated by a long hold, the agent apologizes and rebooks the flight, and the caller asks for the news in Mandarin for his wife. Cookie answers in English and then in Mandarin, in one voice. A support call: - Espresso: “[frustrated, tense] I've been on hold for forty minutes. My flight leaves in an hour.” https://paxalabs.com/audio/takes/call-1.m4a - Cookie: “[apologetic, sincere] I'm so sorry about the wait. I've moved you to TG628 at 15:40, at no charge.” https://paxalabs.com/audio/takes/call-2.m4a - Espresso: “[relieved] Thank you. Could you tell my wife? She only speaks Mandarin.” https://paxalabs.com/audio/takes/call-3.m4a - Cookie: “[亲切的客服语气] 王太太您好,航班已改为TG628,15:40在12号登机口登机。” https://paxalabs.com/audio/takes/call-4.m4a ### An audio drama Two thieves move from a whispered countdown to panic, a shouted escape, and breathless relief. Each line carries one direction, and the last ends on a laugh. An audio drama: - Mocha: “[whispering] Thirty seconds. Don't touch anything.” https://paxalabs.com/audio/takes/heist-2.m4a - Latte: “[panicked, fast] Too late. I touched something.” https://paxalabs.com/audio/takes/heist-3.m4a - Mocha: “[shouting urgently] Run! Go, go, go!” https://paxalabs.com/audio/takes/heist-4.m4a - Latte: “[out of breath, relieved] We're out. Nobody saw us. [laughs]” https://paxalabs.com/audio/takes/heist-5.m4a ### A Thai audio novel A reunion in Thai, directed in Thai. The lines shake, hold back tears, and laugh through them. A Thai audio novel: - Sanae Chan: “[เสียงสั่น] กลับมาแล้วเหรอ [กลั้นน้ำตา] ฉันนึกว่าคุณจะไม่กลับมาแล้ว” https://paxalabs.com/audio/takes/novel-3.m4a - Massaman: “[อบอุ่น] ผมสัญญาไว้แล้วไง ว่าจะกลับมา” https://paxalabs.com/audio/takes/novel-4.m4a - Sanae Chan: “[หัวเราะทั้งน้ำตา] คนบ้า ปล่อยให้รอตั้งนาน” https://paxalabs.com/audio/takes/novel-5.m4a ### The evening news A Thai news read: - Thong Ek: “[ผู้ประกาศข่าว น้ำเสียงเป็นทางการ] สวัสดีครับ ข่าวภาคค่ำวันนี้ เริ่มต้นที่กรุงเทพมหานคร” https://paxalabs.com/audio/takes/reel-news.m4a ## Word timestamps Set `timestamps` to `"word"` and the response carries every word with its start and end in seconds, for captions and for words that light as they are spoken, as on this page. `"sentence"` and `"utterance"` time longer spans. The spans cover the spoken text, and a tag takes none. A buffered timed request returns JSON with the audio as base64 beside `timing.spans`. A streamed one returns one JSON line per chunk, each carrying its audio and the spans it completed. Timed requests bill at 125 percent of the speech rate, and the [timestamps guide](https://paxalabs.com/docs/timestamps) covers both shapes. ## Speed and streaming In our own measurements from Singapore, the first audio of a request arrived in as little as 90 ms. With `stream` set to true, a request starts playing while the rest of the line is still being synthesized. The [live socket](https://paxalabs.com/docs/live-streaming) speaks text while a language model is still writing it, and directions work there too. A tag still open when the socket flushes waits until its bracket closes, and each leg opens in the directions the last one ended in. A `cancel` frame stops the reply when a caller talks over it. The [real-time speech page](https://paxalabs.com/realtime-tts) walks through a session frame by frame. ## The same price Paxa TTS Flash v2 costs 10 credits per 1,000 characters, which is $10 per million, the rate of Paxa TTS Flash. A tag bills as the characters it is written with, brackets included. Timed requests, and live connections that ask for timestamps, bill at 125 percent of the rate on both models. | Line as sent | Characters | Credits | | --- | --- | --- | | I can't believe it. We actually did it! | 39 | 0.39 | | [whispering] I can't believe it. [ecstatic] We actually did it! | 63 | 0.63 | | [whispering] I was not going to tell anyone, [excited:2] but we got the house, [crying] after three years of saving. [laughs] | 125 | 1.25 | | [โฆษณาสินค้า น้ำเสียงกระฉับกระเฉง] ซีซันใหม่ สีใหม่ วางขายวันศุกร์นี้ | 69 | 0.69 | | [广告配音,热情洋溢] 新一季,新色彩,本周五上市。 | 26 | 0.26 | A new account's 100 free credits voice 10,000 characters, about 16 minutes of Thai speech. ## Start using it - `POST /v1/tts` returns buffered or streamed audio. Set `model` to `paxa-tts-flash-v2`. - The [OpenAI-compatible](https://paxalabs.com/docs/openai-compatibility) `POST /v1/audio/speech` reads tags in `input` under the same rules. An OpenAI SDK pointed at the Paxa API sends directions as written. - The [live socket](https://paxalabs.com/docs/live-streaming), `/v1/tts/live`, takes the model in its `start` frame. - In the console, the Speech tool lists Paxa TTS Flash v2, with Pixie beside its name. curl: ```bash curl -X POST https://api.paxalabs.com/v1/tts \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "[warm, unhurried] ยินดีต้อนรับกลับมาครับ [chuckles] วันนี้มีข่าวดี [เอ่อ] แต่ขอเก็บเป็นความลับไว้ก่อนนะครับ", "voice": "khanomkrok", "model": "paxa-tts-flash-v2" }' \ --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: "[warm, unhurried] ยินดีต้อนรับกลับมาครับ [chuckles] วันนี้มีข่าวดี [เอ่อ] แต่ขอเก็บเป็นความลับไว้ก่อนนะครับ", voice: "khanomkrok", model: "paxa-tts-flash-v2", }), }); 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": "[warm, unhurried] ยินดีต้อนรับกลับมาครับ [chuckles] วันนี้มีข่าวดี [เอ่อ] แต่ขอเก็บเป็นความลับไว้ก่อนนะครับ", "voice": "khanomkrok", "model": "paxa-tts-flash-v2"}, ) response.raise_for_status() with open("speech.mp3", "wb") as f: f.write(response.content) ``` A request takes up to 5,000 characters and returns mp3, opus, wav, and pcm, with wav and pcm at 24 kHz. `speed` runs from 0.5 to 1.5 and changes no charge. ## Moving from Paxa TTS Flash - Paxa TTS Flash stays available as `paxa-tts-flash-v1`, and its requests work as before. - To move a request, change `model` to `paxa-tts-flash-v2`. The 26 voices on both models keep their ids. - Pad Thai, Yoyo, Som Tam, Larb, Khao Soi, Roti, Donut, and Toast stay on Paxa TTS Flash. - On Paxa TTS Flash, only its experimental voices read emotion tags such as `[happy]`. On Paxa TTS Flash v2, every voice reads tags, and `[happy]` is a direction like any other. ## Questions ### Is Paxa TTS Flash v2 a preview? No. Paxa TTS Flash v2 is generally available, on the API and in the console. ### Does Paxa TTS Flash v2 cost more than Paxa TTS Flash? No. Both cost 10 credits per 1,000 characters. ### Do requests use the name Pixie? No. Pixie is the model's nickname. Requests name it by its id, `paxa-tts-flash-v2`. ### What can a direction say? Any delivery, in English, Thai, or Chinese words, such as `[tired, exhausted]` or `[ผู้ประกาศข่าว น้ำเสียงเป็นทางการ]`. A direction that asks for an accent, a different voice, singing, a sound effect, or a language answers `tag_invalid`. ### Can I clone a voice? No. Paxa TTS Flash v2 has 50 catalog voices and accepts no reference audio. A new voice comes from [voice design](https://paxalabs.com/voice-design), built from a written description. # What is text to speech? How AI voices work and where they are used Text to speech turns written text into spoken audio. How an AI voice is made, what businesses use it for, what it costs, and how to judge one before you buy. Published 2026-10-11, updated 2026-10-23. Paxa Labs. Text to speech, or TTS, is software that reads written text aloud. You give it a sentence, a script, or a whole book, choose a voice, and get audio back. The same technology speaks the next turn in a navigation app, the menu on a bank's phone line, and the narration on a great many short videos. The phrase covers a wide range of quality. The flat, robotic reader built into an old computer is text to speech. So is a voice a listener can mistake for a recording of a person. What changed between the two is how the voice is built. ## How text to speech works Every system does three jobs in order, whatever it is built from. 1. Read the text: The system turns what is written into what should be said. A price becomes a number in words, an abbreviation becomes the phrase it stands for, and a date is read as a date. In a language written without spaces, such as Thai, it also has to find where each word begins. 2. Decide how it sounds: Each word gets its pronunciation, and the sentence gets its melody. That means which syllable is stressed, where the pitch rises, and where the voice stops for breath. 3. Make the sound: The system generates the audio itself. It arrives as a file such as MP3 or WAV, or as a stream that starts playing while the rest is still being made. Older systems did the last step by cutting recorded speech into small pieces and joining them back together, and the joins made those voices sound mechanical. A modern voice is a neural network trained on many hours of recorded speech. It learns pronunciation and rhythm together and generates the audio whole. That is why it can pause for breath and lift the end of a question the way a person does. ## What businesses use text to speech for Any text that someone would otherwise read aloud into a microphone is a candidate. The common jobs make a short list. | Job | What the voice does | Read more | | --- | --- | --- | | Short video and social clips | Narrates a script over footage, with no recording session | [Creators](https://paxalabs.com/use-cases/creators) | | Ads and radio spots | Reads a spot, and reads it again when the offer changes | [Advertising](https://paxalabs.com/use-cases/advertising) | | E-learning and training | Voices every lesson in one voice, and re-reads a lesson after an update | [E-learning](https://paxalabs.com/use-cases/e-learning) | | Audiobooks and articles | Reads a whole book, or turns each article into audio | [Audiobooks](https://paxalabs.com/use-cases/audiobooks), [publishers](https://paxalabs.com/use-cases/publishers) | | Phone menus and announcements | Speaks the greeting, the queue number, and the closing-time notice | [Call centers](https://paxalabs.com/use-cases/call-centers), [announcements](https://paxalabs.com/use-cases/announcements) | | Voice agents and companions | Answers a caller or a user out loud while the reply is still being written | [Voice agents](https://paxalabs.com/use-cases/voice-agents), [companions](https://paxalabs.com/use-cases/companions) | | Accessibility | Reads a page or a document to someone who cannot read it on screen | [Accessibility](https://paxalabs.com/use-cases/accessibility) | What these jobs share is change. A recorded voice is fixed on the day of the session, and every edit means booking the session again. Generated speech treats the script as the source. Change a line and the new audio is one more request. ## What makes a language hard to speak, with Thai as the example A voice built for English does not turn into a Thai voice once it learns new letters. Each language sets its own problems, and Thai shows most of them at once. - Tone is part of the word. Thai has five tones, and one syllable said on a different tone is a different word. A voice that gets the melody slightly wrong has said something else. - Words run together. Thai is written without spaces between words, and the reader has to find each word before it can pronounce anything. - Numbers and dates follow local rules. A year is usually written in the Buddhist Era, and a price, a phone number, and a house number are each read a different way. - English arrives mid-sentence. Brand names and technical terms stay in Latin letters inside a Thai sentence, and the voice has to change language and change back without a pause. - Politeness has a gender. The particles that end a polite sentence differ for male and female speakers, and a script has to match the voice that reads it. Listen for exactly these when a provider says it supports a language. [How to judge a Thai AI voice in ten sentences](https://paxalabs.com/guides/judging-a-thai-ai-voice) turns them into a test anyone can run, and [the script guide](https://paxalabs.com/guides/writing-for-thai-text-to-speech) covers what the writer controls. Five voices from one catalog: - [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/nomyen.m4a - [Khanom Krok](https://paxalabs.com/voices/khanomkrok), Lead voice: https://paxalabs.com/audio/khanomkrok.m4a - [Som Tam](https://paxalabs.com/voices/somtam), Isan: https://paxalabs.com/audio/somtam.m4a - [Cookie](https://paxalabs.com/voices/cookie), Support agent: https://paxalabs.com/audio/cookie.m4a - [Tao Huay](https://paxalabs.com/voices/taohuay), Drama lead: https://paxalabs.com/audio/taohuay.m4a Paxa TTS Flash has 34 voices across English, Thai, and Mandarin, with regional Thai accents among them. [The voices page](https://paxalabs.com/voices) plays every one. ## How to judge a text to speech voice Sample clips are chosen to flatter. Judge a voice on your own text, and check six things before you commit. - Language. Paste a paragraph of your real content, with its numbers, names, and borrowed words, and listen for the ones it gets wrong. - Casting. A promo, a lesson, and a bedtime story want different readers, and the catalog should hold one that fits the brand. - Control. Look for a speaking-rate setting, pauses that follow the punctuation, and a way to set emotion where the script needs it. - Output. Check the file formats, and whether the audio can stream for a live conversation. - Rights. Read what the plan allows. Some services license commercial use only on paid plans. - Price unit. Most services charge per character of text. A few charge per minute of audio or sell a monthly bundle, and the three are hard to compare until you convert them. [The comparison pages](https://paxalabs.com/compare) convert the public list prices of the main providers into one unit. ## What text to speech costs Generated speech is priced by the amount of text. Paxa TTS Flash costs 10 credits per 1,000 characters, and 1,000 credits are one US dollar. A minute of English narration is about 897 characters, which comes to about 9 credits, or $0.009. A new account starts with 100 free credits, enough for about 11 minutes of narration. The smallest monthly plan is $6 for 8,100 credits, which is about 15 hours of audio. [The pricing page](https://paxalabs.com/pricing) lists every plan, and [the voiceover cost guide](https://paxalabs.com/guides/thai-voice-over-cost) sets these figures beside studio and freelance rates. The free credits are for personal use and evaluation. A first purchase of any size makes the account's audio yours to use commercially, including audio generated before it. ## Text to speech, speech to text, and voice cloning Three terms get mixed up, and they describe different things. - Text to speech turns text into audio, in a voice chosen from a catalog. - [Speech to text](https://paxalabs.com/speech-to-text) goes the other way. It listens to a recording and writes down what was said, which is how a meeting becomes minutes and a video gets subtitles. - Voice cloning builds a new voice from recordings of one particular person. It raises a question of consent that a catalog voice does not, because that person has to have agreed. Paxa TTS Flash does not clone voices. Every voice in the catalog is a fixed cast member. A new voice comes from [voice design](https://paxalabs.com/voice-design), built from a written description. [The Responsible AI page](https://paxalabs.com/responsible-ai) covers how the voices are made and the rules every account accepts. ## Common questions ### Is text to speech free? Phones and computers ship with a basic reader at no cost. For a natural voice, [the free text to speech page](https://paxalabs.com/tts) speaks a short line without an account, and a new account's 100 credits cover about 11 minutes before anything is paid. ### Can it speak in real time? Yes. Audio can stream as it is generated, which lets a voice agent start answering while the rest of its reply is still being written. [The voice agents page](https://paxalabs.com/use-cases/voice-agents) shows how the pieces fit. ### Can an AI voice read with emotion? Every voice has its own character, such as calm, bright, or theatrical. The catalog's experimental Thai emotion voices also take tags written into the text, such as `[happy]` or `[sad]`. [The emotional voices page](https://paxalabs.com/emotional-voices) plays each tag. ## Where a recorded human voice is still the right choice Some sessions are still worth booking. A campaign built around a known person buys that person. A director who wants one word landed differently gets it fastest from an actor in the booth. Singing is its own craft. Most voice work is none of these. It is lessons, product videos, phone menus, and the updates to all of them, and that is where generated speech does the most. ## Next steps - Type a line into [the box on the free text to speech page](https://paxalabs.com/tts) and hear it without an account. - Listen to all 34 voices on [the voices page](https://paxalabs.com/voices). - Read [how to choose a voice for the job](https://paxalabs.com/guides/choosing-a-thai-ai-voice) before you cast one. - Create an account and open [the Speech tool](https://paxalabs.com/app/speech) to voice a full script and download the audio. # How to add an AI voiceover to TikTok, Reels, and Shorts Script, voice, and caption a short video with an AI voice. How long the script should be, which voice fits, and what the platforms ask you to disclose. Published 2026-10-11, updated 2026-10-23. Paxa Labs. Most short videos are a script read over footage. A recipe, a product review, a news recap, and a story told over gameplay all run on narration, and the narration is the part many creators put off. Recording it takes a quiet room, a decent microphone, and a voice you are willing to post. An AI voice removes the recording. You write the script, pick a voice, and download the audio. The steps are the same for TikTok, Instagram Reels, YouTube Shorts, and Facebook Reels, because the result is an audio file that every editor accepts. ## The app's built-in voice, or your own TikTok and the other short-video editors include a text to speech button. You type a caption, tap the button, and a preset voice reads it. It costs nothing and takes seconds, and for a quick reply video it is enough. The limits show once a channel has a style. Every creator on the app has the same few voices, and regular viewers recognize them. The voice belongs to that app's editor, and the same clip cut in another editor gets a different one. Control ends at picking the voice. A voice from a speech catalog is a file. Paxa TTS Flash has 34 voices in English, Thai, and Mandarin, and the file drops into any editor. One voice can narrate every clip on every platform a channel posts to. ## Write the script to the length of the clip Narration runs at a steady pace, and the length of the script sets the length of the clip. English narration runs at about 897 characters a minute, spaces included. Count the characters before you cut the footage. | Clip length | Script length | Cost to voice | | --- | --- | --- | | 15 seconds | About 224 characters | 2.2 credits | | 30 seconds | About 449 characters | 4.5 credits | | 60 seconds | About 897 characters | 9 credits | Three habits make a short script work. Put the payoff in the first sentence, because a viewer decides in the first seconds whether to stay. Keep one idea to a sentence. Write the way you talk, and read the script aloud once before you generate anything. Write numbers, prices, and names the way you want them said. [The script guide](https://paxalabs.com/guides/writing-for-thai-text-to-speech) covers Thai scripts in detail, including where a space becomes a pause. ## Choose a voice that fits the content A voice sets the tone before the first word lands. A calm narrator on a flash-sale clip sounds wrong, and so does a hype voice on a news recap. The catalog states what each voice was cast for. | Voice | Role | Cast for | | --- | --- | --- | | [Nom Yen](https://paxalabs.com/voices/nomyen) | Lead voice | Bright, energetic voice and the roster's female lead: promos, social clips, and everyday product speech. | | [Massaman](https://paxalabs.com/voices/massaman) | Leading man | Deep, steady leading-man voice for trailers, drama, and announcements. | | [Thong Ek](https://paxalabs.com/voices/thongek) | News anchor | Crisp, authoritative read for news bulletins and corporate updates. | | [Moo Ping](https://paxalabs.com/voices/mooping) | Street energy | Confident, streetwise voice for ads and shorts. | | [Tom Yum](https://paxalabs.com/voices/tomyum) | Best friend | Fast, hot, chatty best-friend voice for drama recaps, entertainment news, and social clips. | | [Khanom Chan](https://paxalabs.com/voices/khanomchan) | Emotion tags | Natural conversational voice that changes mood with emotion tags written into the text: happy, sad, angry, or afraid. | | [Som Tam](https://paxalabs.com/voices/somtam) | Isan | Isan-accented voice with local warmth. | | [Donut](https://paxalabs.com/voices/donut) | Promos | Cheerful English voice for promos and demos. | | [Toast](https://paxalabs.com/voices/toast) | Tutorials | Plain-spoken everyday English voice for tutorials. | | [Tao Huay](https://paxalabs.com/voices/taohuay) | Drama lead | Tender, melancholy Mandarin voice for drama recaps, audio novels, and companion speech. | Five voices for short video: - [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/nomyen.m4a - [Moo Ping](https://paxalabs.com/voices/mooping), Street energy: https://paxalabs.com/audio/mooping.m4a - [Tom Yum](https://paxalabs.com/voices/tomyum), Best friend: https://paxalabs.com/audio/tomyum.m4a - [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/khanomchan.m4a - [Massaman](https://paxalabs.com/voices/massaman), Leading man: https://paxalabs.com/audio/massaman.m4a Pick one voice per channel and keep it. Viewers come to know a narrator the way they know a face, and a channel that changes voice every clip gives that up. [The guide to choosing a voice](https://paxalabs.com/guides/choosing-a-thai-ai-voice) goes through all 34 by job. ## Make the voiceover, step by step 1. Paste the script: Open [the Speech tool](https://paxalabs.com/app/speech) and paste the script. The cost appears under the text before you generate anything. 2. Pick and preview a voice: Open the voice picker, filter by language, and play the samples. Previews are free. 3. Set the pace: Move the speed control up a little for a fast promo, or down for an explainer. The price is the same at every speed. 4. Generate and listen: Listen to the whole take once, with your attention on names, numbers, and English words. Fix a misread word by respelling it in the script, then generate again. 5. Download the file: Choose MP3 for most editors, or WAV when the voice will be mixed with music. Then download the clip. 6. Cut the video to the voice: Import the audio into your editor and lay the footage over it. Cutting pictures to a finished voice track is faster than stretching a voice to fit finished pictures. The catalog's experimental Thai emotion voices read tags written into the script, such as `[happy]` before a line. [The emotional voices page](https://paxalabs.com/emotional-voices) plays each tag. ## Add captions from the same audio A viewer with the sound off gets only the captions. Upload the finished voice track to [the Transcribe tool](https://paxalabs.com/app/transcribe) and ask for subtitles. It returns an SRT or VTT file with the timing of every line, ready to import into a video editor. A Thai caption breaks only between words. That matters because Thai is written without spaces, and a careless break splits a word in two. Transcription costs 8.33 credits per minute of audio. ## What it costs Speech is priced by the character, at 10 credits per 1,000 characters. A one-minute clip costs about 9 credits, which is $0.009. A new account's 100 free credits voice about 11 one-minute clips. A channel that posts a one-minute clip every day uses about 269 credits a month. The smallest plan is $6 a month for 8,100 credits, and [the pricing page](https://paxalabs.com/pricing) lists the rest. The free credits are for personal use and evaluation. Buy any plan or credit pack and the account's audio is yours to use commercially, including clips made before the purchase. A monetized channel or a client job belongs on a paid account. ## What the platforms ask you to disclose Each platform publishes its own rule on AI-made content, and the rules change. YouTube's is a useful reference. [It asks for a disclosure](https://support.google.com/youtube/answer/14328491) in three cases. Realistic content makes a real person appear to say or do something they did not. Footage of a real event or place is altered. A realistic scene is shown that never happened. A catalog voice reading your own script over your own footage makes no real person say anything, which is the case the rule is written around. Read the rule against any clip that shows or imitates a real person. Paxa TTS Flash does not clone voices. Every voice in the catalog is a fixed cast member, and a new voice comes from [voice design](https://paxalabs.com/voice-design), built from a written description. Imitating a real person's voice without their consent is against [the rules every account accepts](https://paxalabs.com/responsible-ai). ### Can a channel with an AI voice be monetized? [YouTube's channel policies](https://support.google.com/youtube/answer/1311392) allow creative tools that help deliver an original narrative. They turn away mass-produced, templated videos and videos that only read out material someone else wrote. Both tests look at the script and the edit. A channel earns on a script that is its own and adds something to the footage. ## Mistakes that give an AI voiceover away - A number read the wrong way. Listen to every price, date, and phone number, and spell out the ones that come back wrong. - A brand name respelled in Thai letters. Keep the name in the Latin letters its owner uses, and [a Thai voice reads it as English](https://paxalabs.com/thai-english) inside the sentence. - Sentences with no place to breathe. Break a long sentence in two, and in Thai put a space where the pause belongs. - A pace set too fast. A little faster holds attention. A lot faster sounds like the terms and conditions at the end of a radio ad. - Music louder than the voice. Lower the music under the narration until every word is clear on a phone speaker. ## Next steps - Hear a line in the demo voices on [the free text to speech page](https://paxalabs.com/tts), with no account. - Open [the Speech tool](https://paxalabs.com/app/speech) with the free credits and voice a first clip. - See what [creators](https://paxalabs.com/use-cases/creators) and [advertisers](https://paxalabs.com/use-cases/advertising) make with the same voices. - Read [what a Thai voiceover costs](https://paxalabs.com/guides/thai-voice-over-cost) for studio and freelance rates beside these. # How to transcribe a meeting, an interview, or a lecture into text Three ways to turn a recording into text, how to record so the transcript comes out clean, and what to check before anyone relies on it. Published 2026-10-11. Paxa Labs. A recording is easy to make and hard to use. Nobody can search it, skim it, or quote from it without playing it back. Transcription turns the recording into text, and the text is what gets read, filed, and searched. The work used to belong to a typist with headphones. Speech to text software now writes the first draft, and a person checks it. The job has three parts, which are recording well, choosing the right settings, and reviewing the names and numbers. ## Three ways to get a transcript | Way | What it takes | Suits | | --- | --- | --- | | Type it yourself | Several times the length of the recording, since you pause and replay as you type | A short clip, or a recording nobody else may hear | | Hire a transcriber | A quote based on the length of the recording, and a turnaround counted in days | Legal or sensitive work that needs a named person to stand behind the text | | Speech to text software | An upload and a review pass | Meetings, interviews, lectures, and anything recorded every week | The three combine well. Software writes the draft and a person who was in the room corrects it, which is far quicker than typing from a blank page. ## Record so the transcript comes out clean Most transcript errors start in the room. A recording that a person strains to follow is one that software will struggle with too. - Put the phone or recorder near the speakers. In a meeting room, the middle of the table beats the end by the projector. - Keep away from the air conditioner and the fan. A steady hum covers the quiet ends of words. - Ask people to speak one at a time. Two voices at once come out as one muddled line. - Say names and figures clearly, and repeat the ones that matter. A budget read out twice gets checked twice. - Save in a common format. MP3, WAV, FLAC, OGG, M4A, AAC, WEBM files all work. - Split a long session at the breaks. One file can run up to 60 minutes and 25 MB. ## From file to text, step by step 1. Upload the recording: Open [the Transcribe tool](https://paxalabs.com/app/transcribe) and drop the file in, or record straight from the microphone. The cost appears before you start. 2. Choose how numbers are written: Pick Written to get digits, units, and the repetition mark, which is what minutes and reports want. Pick Spoken to keep every number in words, exactly as it was said. 3. Choose the style: Verbatim keeps every filler and false start, which a research interview or a legal record needs. Clean drops them and reads more easily, and it follows the recording less exactly. 4. Add your vocabulary: List the names, products, and jargon the recording contains, up to 50 terms. They come back spelled the way you wrote them. 5. Turn on speakers for a short recording: On a recording under 9 minutes, the transcript can come back as numbered turns, one per speaker. Nobody is identified by name. 6. Transcribe and copy: Run it, read the result beside the audio, and copy the text or download a subtitle file. > Paxa STT Lite is a research preview. Its transcripts and its options may change between releases. ## What each kind of recording needs | Recording | Settings | Then | | --- | --- | --- | | Meeting | Written numbers, Clean style, and every attendee's name in the vocabulary | Draft the minutes from the text, with decisions and owners at the top. See [meetings](https://paxalabs.com/use-cases/meetings). | | Interview | Verbatim style, with word timestamps on | Quote exactly, and jump back to the second a quote was said. See [interviews](https://paxalabs.com/use-cases/interviews). | | Lecture or training | Clean style, with subtitles as SRT or VTT | Post the transcript as notes and the subtitle file with the video. See [lectures](https://paxalabs.com/use-cases/lectures) and [captions](https://paxalabs.com/use-cases/captions). | | Voice note | Written numbers | Paste the text where the note was heading. See [voice notes](https://paxalabs.com/use-cases/voice-notes). | ## What Thai recordings do differently Thai speech sets a transcriber three problems that English does not. - English arrives mid-sentence. A Thai speaker names a product or a technical term in English and carries on in Thai. A good transcript keeps each word in the script it was spoken in, and [the code-switching guide](https://paxalabs.com/guides/transcribing-thai-english-code-switching) shows what that does for search. - Numbers are said in words. Whether the transcript writes the words or the digits is a setting, and [the numbers guide](https://paxalabs.com/guides/thai-transcript-numbers-spoken-or-written) covers which reader wants which. - Words are written without spaces. A timestamp has to cover a whole word, and a subtitle line has to break between words. ## Check the transcript before anyone relies on it A transcript is a draft until someone has read it against the audio. A full replay is rarely needed. Check the places where one wrong word changes the meaning. - Names of people, companies, and products. - Every figure, including amounts, dates, percentages, and phone numbers. - Negations. One dropped word turns a refusal into an agreement. - Who said what, when the transcript has speaker turns. - Any passage where people talked over each other. Word timestamps make the check quick. Find the doubtful word, play that second of the recording, and correct it. To compare two services, run the same three recordings of your own through both. [The accuracy guide](https://paxalabs.com/guides/measuring-thai-transcription-accuracy) explains how to score the result. ## What it costs Transcription is priced by the length of the recording. Paxa STT Lite costs 8.33 credits per minute of audio. An hour is 500 credits, or $0.50. | Recording | Length | Credits | Cost | | --- | --- | --- | --- | | Voice note | 10 minutes | 83.34 | $0.08 | | Meeting | 45 minutes | 375 | $0.38 | | Lecture | 60 minutes | 500 | $0.50 | A new account's 100 free credits cover about 12 minutes of audio, with no card. [The pricing page](https://paxalabs.com/pricing) lists the plans. ## When the text has to appear live A recording is transcribed after the fact. Live captions for a talk, or a voice agent that has to answer a caller, need the text while the person is still speaking. Paxa STT Lite Realtime does that over one connection, at 12.5 credits per minute of audio received. [The live transcription page](https://paxalabs.com/use-cases/live-transcription) covers it. ## Common questions ### Can I transcribe a video file? The Transcribe tool takes audio. Export the sound from the video as an M4A or MP3 file in your editor and upload that. The subtitle file that comes back lines up with the original video, because the timing is the same. ### Does it work for languages other than Thai? It is built for Thai first, and other languages are transcribed as they are spoken. A Thai sentence that switches into English keeps both scripts in one line. ## Before you upload a recording Tell the people in the room that the meeting is being recorded and that a service will transcribe it. Before you upload anything confidential, read how the service handles what you send. Ours is set out in [the Privacy Policy](https://paxalabs.com/privacy) and [the Terms](https://paxalabs.com/terms). ## Next steps - Upload a recording to [the Transcribe tool](https://paxalabs.com/app/transcribe) with the free credits. - See a Thai recording transcribed word by word on [the transcription page](https://paxalabs.com/transcribe). - Read [how to choose a Thai speech to text engine](https://paxalabs.com/guides/choosing-a-thai-speech-to-text-engine) before you commit to one. # What is OCR? Turning photos, scans, and PDFs into text you can use OCR reads the text in a photo, a scan, or a PDF and returns text a computer can search and edit. How it works, where businesses use it, and what to check. Published 2026-10-11. Paxa Labs. OCR, short for optical character recognition, is software that reads the text in an image. A photo of a receipt, a scanned contract, and a PDF made on a scanner are all pictures of words. A computer cannot search a picture or copy from it. OCR looks at the picture and writes the words out as text. The idea is decades old, and for a long time it worked only on clean, typed English. What changed is the reader. A modern OCR model reads a whole page the way a person does, with its columns, tables, stamps, and mixed languages. That is why OCR now sits inside accounting, onboarding, and search tools. ## How OCR works 1. Clean up the image: The page is straightened, and shadows, noise, and uneven lighting are reduced. A crooked phone photo becomes something closer to a flat scan. 2. Find the layout: The reader works out what is on the page, such as a heading, two columns, a table, and a stamp over a signature. It also decides the order a person would read them in. 3. Read the characters: Each line is turned into letters, digits, and marks. In Thai this includes the vowels and tone marks stacked above and below the consonants. 4. Return the text: The result comes back as plain text, as Markdown that keeps the headings and tables, or as structured blocks a program can process. Classic OCR did the third step one character at a time by matching shapes, which is why it failed on unusual fonts and poor scans. A modern model reads the line and the page together. It uses the context, the way a person reads a smudged word from the words around it. ## OCR, AI OCR, and document extraction Three terms describe three depths of the same job. | Term | What you get | Example | | --- | --- | --- | | OCR | The text on the page | A scanned letter becomes text you can search and copy | | AI OCR, or document OCR | The text with its structure, meaning headings, paragraphs, tables, and reading order | A financial report becomes Markdown with its tables intact | | Document extraction | The named fields you asked for, as data | A tax invoice becomes an invoice number, a date, a seller tax ID, and a total | Extraction is the step that removes the typing. You list the fields you need, and each one comes back as a value read from the page, or empty with the reason it could not be read. [Paxa Document Extraction](https://paxalabs.com/document-extraction) does this for Thai documents. ## What businesses use OCR for | Job | Documents | Read more | | --- | --- | --- | | Bookkeeping | Invoices, tax invoices, receipts, and withholding certificates | [Invoices](https://paxalabs.com/use-cases/invoices), [tax invoice fields](https://paxalabs.com/ocr/tax-invoice) | | Customer onboarding | ID documents, house registrations, bank statements, and application forms | [Identity documents](https://paxalabs.com/use-cases/identity) | | Legal and compliance | Contracts, company certificates, and title deeds | [Contracts](https://paxalabs.com/use-cases/contracts) | | Archives | Old books, newspapers, and paper files | [Archives](https://paxalabs.com/use-cases/archives) | | Search and AI assistants | Reports, manuals, and policies fed to a search index or a language model | [Knowledge bases](https://paxalabs.com/use-cases/knowledge-bases) | | Accessibility | Any printed page, read aloud | [Read a document aloud](https://paxalabs.com/read-aloud) | The pattern is the same in every row. A person is retyping what is already printed, and the retyping is slow and adds errors. OCR makes the page itself the source of the data. ## Why Thai is harder to read than English English OCR matured first because English is forgiving. It puts one letter after another on one line, with a space between words. Thai breaks each of those assumptions. - Marks stack. A Thai syllable can carry a vowel above or below the consonant and a tone mark above that. Lose one small mark and the word changes. - Words have no spaces. A line of Thai is an unbroken string, and a reader that guesses the boundaries wrongly returns words that do not exist. - There are two sets of digits. Official documents print Thai digits beside Arabic ones, sometimes in the same line. - Years run on the Buddhist calendar. A document dated 2569 is from 2026, and a system that stores the year as written is off by 543 years. - Modern fonts drop the loops. In a loopless typeface several Thai letters resemble Latin ones, such as ร and S or พ and W, and a weak reader swaps them. - Thai and English share a line. Company names, units, and product codes sit in Latin letters inside Thai text. A tool that passes on an English page can fail on any of these. [How to extract text from Thai PDFs and scans](https://paxalabs.com/guides/thai-pdf-text-extraction) shows what the failures look like. ## How to photograph a page for OCR - Lay the page flat and hold the phone directly above it. - Use even light, and keep the shadow of your hand and your phone off the page. - Fill the frame with the page, with all four corners visible. - Take one page per photo. - Send the original PDF when you have it. A scan of a printout of a PDF is two steps worse than the file. ## What to check before choosing an OCR tool Run your own worst page through it. A clean sample page tells you little, and a crumpled thermal receipt tells you a lot. - Marks and digits. Compare a line with stacked tone marks and a line with Thai digits against the page. - Tables. A table should come back as a table, with each number still in its column. - Reading order. On a two-column page, the text should run down one column and then the other. - Files. Check which formats one upload accepts, and how many pages. - Output. Markdown suits people and language models. Structured blocks suit a program. - Price unit. Most services charge per page, and the page count should be known before you pay. ## What OCR costs OCR is priced by the page. Paxa OCR Lite is our own model, and it costs 6.5 credits per page, whatever is printed on it. A photo counts as one page. | Job | Credits | Cost | | --- | --- | --- | | One page | 6.5 | $0.0065 | | A 50-page report | 325 | $0.33 | | 1,000 pages | 6,500 | $6.50 | A new account's 100 free credits read 15 pages. One upload takes a file of up to 50 pages and 10 MB, and the formats are PDF, PNG, JPEG, WEBP. Paxa Document Extraction costs 13 credits per page for a field list of up to 50 leaf fields, and 19.5 credits per page for a longer one. Reading the page is included. ## Common questions ### Is OCR the same as scanning? A scanner or a phone camera makes a picture of the page, and OCR reads the picture. A scanned PDF with no OCR is a stack of images, which is why its text cannot be selected. ### Can OCR read handwriting? Print reads far more reliably than handwriting in any OCR tool, and neat block letters read better than a hurried note. Try a few handwritten pages of your own before a workflow depends on them. ### Why does copying Thai text from a PDF come out broken? Some PDFs are scans with no text inside. Others carry a text layer whose Thai marks come out scrambled when copied. Running OCR on the page image fixes both, and [the PDF guide](https://paxalabs.com/guides/thai-pdf-text-extraction) explains the cause. ## Try it on your own page 1. Drop in a file: Open [the Documents tool](https://paxalabs.com/app/documents) and drop in a PDF or a photo. The page count and the cost appear before anything is read. 2. Pick the output: Choose Markdown to read the result yourself, or Structured to pass it to a program. 3. Read and compare: Run it, then compare the text against the page, starting with the numbers. 4. Pull out the fields: For a tax invoice or a receipt, open [the Extract tool](https://paxalabs.com/app/extract) and start from a ready-made field list. ## Next steps - See a Thai tax invoice read field by field on [the OCR page](https://paxalabs.com/ocr). - Read [how to feed Thai documents to an LLM](https://paxalabs.com/guides/thai-documents-for-llms) if the text is going to a language model. - Read [how to turn Thai PDFs into a search index](https://paxalabs.com/guides/thai-pdf-to-search-index) if it is going to search. # Localization vs translation: Thai examples of the difference Translation changes the language. Localization changes everything a local reader notices. See the difference on a Thai launch, from dates and money to names. Published 2026-10-11. Paxa Labs. Translation turns text from one language into another. Localization adapts a whole product, page, or campaign for the people of one market, and translation is one part of that work. A translated page can be correct in every sentence and still read as foreign. The gap is easiest to see on a real market. Thailand makes a good example, because Thai differs from English in script, grammar, calendar, and manners all at once. ## The difference in one table | English source | Translated | Localized for Thailand | | --- | --- | --- | | Hi Somchai, | สวัสดี สมชาย | สวัสดีค่ะ คุณสมชาย | | March 4, 2026 | 4 มีนาคม 2026 | 4 มีนาคม 2569 | | $19.99 | $19.99 | ฿590 | | Buy now | ซื้อตอนนี้ | สั่งซื้อเลย | | 0.4 acres | 0.4 เอเคอร์ | 1 ไร่ | | Email us | ส่งอีเมลหาเรา | แอดไลน์หรือโทรหาเรา | Every cell in the middle column is a correct translation. The right-hand column is what a Thai company would have written in the first place. The greeting gains a title and a polite particle, the year moves to the Buddhist Era, and the price is one somebody set in baht. The button uses the verb Thai shops use. Land is measured in rai, and the customer is sent to LINE or the phone. ## What localization covers - Language and register. The words, and how polite, formal, or playful they are. - Formats. Dates, numbers, currency, addresses, phone numbers, and units of measure. - Names and titles. How a person is addressed in a greeting, on a form, and on an invoice. - Layout. Line length, line breaks, fonts, and the room a script needs. - Examples and images. The people, places, holidays, and references a reader recognizes. - Payment and contact. The ways people in the market pay, and the channels they use to ask a question. - Legal text. Terms, privacy notices, and disclosures that a local lawyer has read. Translation covers the first item. The rest are decisions somebody has to make before or after the words change. ## Six places where translation stops, in Thai ### Politeness is part of the grammar English can write a neutral sentence, and Thai cannot. Every Thai sentence picks a level of formality through its pronouns and its closing particles, and the particles differ for a male and a female speaker. A brand has to decide how it speaks, and to whom, before the first line is translated. [Translating into Thai at the right level of politeness](https://paxalabs.com/guides/thai-politeness-in-translation) goes through the choices. ### The year is 543 years ahead Thailand counts years in the Buddhist Era, and 2026 is 2569. Receipts, contracts, and government forms print the Thai year, while software often stores the international one. A localized product shows the year its reader expects, and it says which calendar it means wherever the two could be confused. ### Borrowed words have more than one spelling Thai borrows English words freely, and many of them have an official spelling, a newspaper spelling, and a chat spelling. A brand name can stay in Latin letters or be written in Thai script. Either choice works, and mixing the two across one product is what readers notice. [The checking guide](https://paxalabs.com/guides/checking-machine-translation-into-thai) has a table of common cases. ### Lines break between words that have no spaces Thai is written without spaces between words. A browser or an app has to know where the words are before it can wrap a line, and a system without Thai support breaks a word in half. Thai also stacks vowels and tone marks above and below the line, and a tight line height clips them. Test every screen with real Thai text. [The word segmentation guide](https://paxalabs.com/guides/thai-word-segmentation) explains the mechanics. ### People are addressed by given name, with a title Thais use given names in daily and business life, with the title คุณ in front. A greeting built around an English surname produces a line no Thai would write. A form needs the given name first, and a salutation needs the title. ### Idioms and jokes stay behind An easy task is a piece of cake in English. The Thai phrase for the same idea is ปอกกล้วยเข้าปาก, peeling a banana into your mouth. A translator who knows both swaps one for the other. A literal rendering leaves the reader wondering where the cake came from. ## Transcreation, and where machine translation is enough Slogans, ad headlines, and campaign names are written again from the brief by a copywriter in the market. The trade calls this transcreation, and it is priced as creative work. Most of a product is a different kind of text. Help articles, product listings, interface text, and support replies carry facts, and they change every week. Machine translation with a clear brief and a human check does this work well, and waiting days for each update is hard to justify. Legal documents sit apart. A contract or an official filing may need a certified human translation, and [the translation cost guide](https://paxalabs.com/guides/thai-translation-cost) covers what that costs. ## Write a brief the translator can use A human translator and a translation model fail the same way when nobody tells them who the reader is. A one-page brief fixes most of it. 1. Name the reader and the register: State who reads the text and how the brand speaks to them. In the Translate tool this is the Register setting, with Formal and Casual. 2. List your terms: Give the Thai rendering you want for each product term. The Glossary holds the pairs and applies them every time. 3. Protect what must stay as written: Brand names, product codes, and legal names go under Keep as written and come back untouched. 4. Decide on borrowed words: Choose whether English loanwords are written in Thai script or kept in the original, and hold that choice across the product. 5. Give the context: Say where the text appears, such as a checkout button, a push notification, or a help article. Add a character limit where space is tight. 6. Show an approved example: Two or three lines of Thai copy the brand has already signed off are worth more than a paragraph of instructions. [The Translate tool](https://paxalabs.com/app/translate) takes each part of that brief as a setting. Paxa Translate Lite translates into Thai from 14 source languages you can name, or detects the language itself. It also marks a result that deserves a second look, such as a glossary term that went missing. ## What translating into Thai costs Paxa Translate Lite is priced by the character, and a request is billed at two rates. | What is counted | Rate | | --- | --- | | Text that is translated | 25 credits per 1,000 characters | | Reference material the model reads, such as context, a glossary, and instructions | 8 credits per 1,000 characters | A standard page of 1,800 characters costs 45 credits to translate, which is $0.05. A new account's 100 free credits translate about 2 pages, and [the pricing page](https://paxalabs.com/pricing) lists the plans. ## Common questions ### What do l10n and i18n mean? Both are abbreviations. L10n is localization, with ten letters between the l and the n. I18n is internationalization, the engineering work that lets a product show any language, date format, and currency without a rewrite. Internationalization comes first, and localization follows for each market. ### Can the Translate tool translate out of Thai? Paxa Translate Lite translates into Thai only. Text going from Thai into another language needs a different service. ## A checklist before a Thai launch - The register is chosen, with the brand's pronouns and particles written down. - A glossary of product terms is agreed, with one spelling for each borrowed word. - Dates show the Buddhist Era wherever a Thai reader expects it. - Prices are set in baht. - Greetings and forms use given names with a title. - Every screen has been checked with real Thai text for broken lines and clipped marks. - A Thai reader has read the main pages from start to finish. - A Thai lawyer has reviewed the terms and the privacy text. ## Next steps - Paste a paragraph into [the Translate tool](https://paxalabs.com/app/translate) and set the register. - Read [what machine translation into Thai gets wrong](https://paxalabs.com/guides/checking-machine-translation-into-thai) for a review a team without a Thai reader can run. - See how [e-commerce](https://paxalabs.com/use-cases/ecommerce), [support](https://paxalabs.com/use-cases/support), [travel](https://paxalabs.com/use-cases/travel), and [games and apps](https://paxalabs.com/use-cases/games-apps) teams translate into Thai. # Paxa TTS Flash now costs a third less From October 1, Paxa TTS Flash costs 10 credits per 1,000 characters, after a September that brought transcription, Document Extraction, Mandarin Chinese, and emotion tags. Published 2026-10-01. Paxa Labs. On August 3 we opened our first public API, one speech model that read Thai and English. Eight weeks later Paxa Labs runs 4 product lines on 7 models, more than 800 accounts have signed up, and more than 20 updates have shipped across the platform and the models. ## Speech costs a third less From October 1, Paxa TTS Flash costs 10 credits per 1,000 characters, which is $10 per million characters. Until September 30 the rate was 15 credits, or $15 per million. The new rate applies to every speech request, whichever way it arrives: `POST /v1/tts`, the OpenAI-compatible `POST /v1/audio/speech`, the [live socket](https://paxalabs.com/docs/live-streaming), and the Speech tool in the console. Requests with timestamps and live streaming sessions cost a third less as well. The model id, the voices, and the request format stay the same, and no code needs to change. Credits already in a balance or a plan keep their value and now voice 50 percent more text. A $10 refill pack voiced about 18 hours of Thai speech at the old rate. It voices about 27 hours now. | Credits | Characters from October 1 | Characters until September 30 | | --- | --- | --- | | 100 free credits on a new account | 10,000 | 6,666 | | $10 refill pack, 10,000 credits | 1,000,000 | 666,000 | | $6 monthly plan, 8,100 credits | 810,000 | 540,000 | | $21 monthly plan, 25,200 credits | 2,520,000 | 1,680,000 | | $99 monthly plan, 110,000 credits | 11,000,000 | 7,333,000 | Most of September went into optimizing speech. The new price is the result, and it applies to every account from the first request. ## What shipped in September Paxa TTS Flash was the only model when the API opened. Since then 6 more have shipped, most of them in September. ### Transcription, recorded and realtime Paxa STT Lite opened on September 9 as a research preview. It transcribes Thai and English recordings through `POST /v1/stt`, word for word in the language spoken, with timestamps that cover whole Thai words. Speakers are numbered, and a vocabulary of up to 50 terms keeps names and product terms spelled the way you write them. It costs 8.33 credits per minute of audio. A day later the same endpoint began returning subtitles as SRT or VTT files. Cues break at a change of speaker, at the end of a sentence, and at a pause, and a Thai line breaks only between words. On September 23, [Paxa STT Lite Realtime](https://paxalabs.com/blog/realtime-transcription-for-thai-voice-agents) added transcription while the speaker is still talking. It takes audio over a WebSocket and detects the end of each turn itself, which tells a voice agent when to answer. It costs 12.5 credits per minute of audio received. ### Paxa Document Extraction [Paxa Document Extraction](https://paxalabs.com/document-extraction) shipped on September 12. You send a PDF or an image with a JSON schema of the fields you need, such as an invoice number, a date, and the line items. The schema comes back filled in. Each value is the text printed on the page, or null with the reason it is missing. A page costs 13 credits for a schema of up to 50 leaf fields and 19.5 for a larger one, with the reading of the page included. ### Mandarin Chinese, emotion tags, and a language setting On September 14, Paxa TTS Flash [began reading Mandarin Chinese](https://paxalabs.com/blog/mandarin-chinese-and-six-new-voices) from Simplified script beside Thai and English. Six voices joined the roster that day, two for each language. On September 25, [two experimental voices](https://paxalabs.com/blog/emotion-tags-for-thai-speech), Khanom Chan and Kaprao, began reading emotion tags written into the text. There are 26 tags, such as `[happy]` and `[furious]`, and a weight such as `[sad:0.5]` sets how strongly a tag lands. The roster now holds 34 voices. On October 1, every speech request gained a language setting. Set it to the language the text is written in, and numbers, symbols, and codes such as AB2039 are read the way that language reads them. Auto, the default, works the language out from the text. Earlier in September, the live socket gained a cancel message that stops a reply mid-sentence. An assistant can now stop talking the moment its user talks over it. ### Tools for developers and agents The [MCP server](https://paxalabs.com/docs/mcp), `@paxalabs/mcp`, gives Claude Code, Cursor, and other MCP clients the API as tools. They can speak through the computer's speakers, save speech to a file, translate into Thai, and read documents with OCR. [Feedback](https://paxalabs.com/docs/api/feedback) now goes through the API as well. `POST /v1/feedback` reports a wrong output or an API problem against the request ids it names, and `GET /v1/feedback` lists each report with its status and a note from our team. ## Time to first audio The time a request takes to return its first audio is the delay a listener notices first in a voice agent or a live reply. [A measured run of the public API](https://paxalabs.com/blog/tts-latency-singapore-bangkok) from Singapore and Bangkok records it, with its date, client locations, and request shapes. ## Independent benchmarks [Speko's multilingual text to speech benchmark](https://benchmarks.speko.ai/tts-multilingual?lang=th) includes Paxa TTS Flash on its Thai board. Speko runs the board and publishes its method beside the results. ## Try it at the new rate The new rate is live on every account. The 100 free credits on a new account now voice 10,000 characters, about 16 minutes of Thai speech, in the Speech tool or through the API. [Pricing](https://paxalabs.com/pricing) lists every rate and plan, and the [changelog](https://paxalabs.com/docs/changelog) dates each release. # Paxa TTS Flash now reads emotion tags, on two new voices Khanom Chan and Kaprao join Paxa TTS Flash as experimental voices that read 26 emotion tags written into the text, with a weight for how strongly each one lands. Published 2026-09-25, updated 2026-10-01. Paxa Labs. A Thai reply carries its warmth in the words. Read in the level tone of a delivery update, ดีใจด้วยนะ sounds like someone reading a script. From September 25, two voices in Paxa TTS Flash take the delivery from the text as well. Write [happy] before the line, and the line is spoken happy. The two voices, with no tags: - [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/khanomchan.m4a - [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/kaprao.m4a ## What changed - [Khanom Chan](https://paxalabs.com/voices/khanomchan) (`khanomchan`) and [Kaprao](https://paxalabs.com/voices/kaprao) (`kaprao`) join the roster as experimental voices, for 34 in all. - Both read 26 emotion tags written into `text`, such as `[happy]`, `[sad]`, and `[furious]`. The other voices skip them. - A weight after a colon, as in `[sad:0.5]`, sets how strongly a tag lands. - `GET /v1/voices` adds `experimental` and `emotion_tags` to every voice. - The model id stays `paxa-tts-flash-v1`, and the speech rate does not change. ## How a tag reads A tag sets the delivery from where it stands until the next tag. Text before the first tag keeps the voice's own delivery, and one request changes delivery as often as its text needs. one request, two deliveries: ``` POST /v1/tts { "model": "paxa-tts-flash-v1", "voice": "khanomchan", "text": "[happy] สอบผ่านแล้ว [sad:0.6] แต่คงคิดถึงที่นี่" } ``` The request above, in Khanom Chan: - [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/request.m4a ## The tags The tags are everyday words for how a line should feel. Five name the broad emotions: `[neutral]`, `[happy]`, `[sad]`, `[angry]`, and `[afraid]`. The rest name a closer shade of one, such as `[annoyed]` or `[crying]`. The five broad emotions on one line, in Khanom Chan: - [neutral], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-neutral.m4a - [happy], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-happy.m4a - [sad], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-sad.m4a - [angry], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-angry.m4a - [afraid], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-afraid.m4a The same line and tags in Kaprao: - [neutral], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-neutral.m4a - [happy], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-happy.m4a - [sad], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-sad.m4a - [angry], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-angry.m4a - [afraid], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-afraid.m4a Inside a group, the other words set how far the delivery goes. `[annoyed]` is a lighter angry, and `[shouting]` is the loudest. `[disappointed]` is a lighter sad, and `[crying]` sounds close to tears. The angry group from lightest to loudest, in Kaprao: - [annoyed], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-annoyed.m4a - [angry], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-angry.m4a - [furious], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-furious.m4a - [shouting], [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/kaprao-shouting.m4a The sad group, in Khanom Chan: - [disappointed], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-disappointed.m4a - [sad], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-sad.m4a - [sorrowful], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-sorrowful.m4a - [crying], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/khanomchan-crying.m4a | Group | Tags | | --- | --- | | Neutral | `[neutral]`, `[calm]`, `[normal]`, `[plain]` | | Happy | `[happy]`, `[excited]`, `[cheerful]`, `[joyful]`, `[amazed]`, `[laughing]` | | Sad | `[sad]`, `[crying]`, `[sorrowful]`, `[gloomy]`, `[disappointed]` | | Angry | `[angry]`, `[shouting]`, `[furious]`, `[annoyed]`, `[frustrated]` | | Afraid | `[afraid]`, `[scared]`, `[fearful]`, `[nervous]`, `[terrified]`, `[anxious]` | The [emotional voices page](https://paxalabs.com/emotional-voices) plays all 26 tags in both voices, and the [emotion tags guide](https://paxalabs.com/docs/emotion-tags) covers the grammar. ## How strongly a tag lands A weight after a colon sets how strongly the tag applies, from 0 to 1. `[sad:0.5]` and `[sad:50%]` read the same. A weight past either end reads as that end, and a tag at 0 does not apply. [sad] from 0 to 1, in Khanom Chan: - [sad:0], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/dial-w0.m4a - [sad:0.25], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/dial-w25.m4a - [sad:0.5], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/dial-w50.m4a - [sad:0.75], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/dial-w75.m4a - [sad], [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/dial-w100.m4a ## Tags in real lines Each line below is the `text` of one request, sent as written. The clips are the audio those requests returned. A companion's reply turns from congratulations to missing someone, then settles on a plan. The [AI companion guide](https://paxalabs.com/ai-companion) covers the register the words already carry. text: ``` [happy] ได้ข่าวว่าสอบผ่านแล้ว ดีใจด้วยจริง ๆ นะ [sad:0.6] แต่ก็ใจหายเหมือนกัน ต่อไปคงไม่ค่อยได้เจอกันแล้ว [calm] ไม่เป็นไร เดี๋ยวเรานัดกินข้าวกันนะ ``` A companion's reply: - [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/companion.m4a A guard in a text adventure snaps about an open gate, then worries about the captain. [Interactive fiction](https://paxalabs.com/use-cases/interactive-fiction) covers voicing a cast of characters. text: ``` [furious] ใครเปิดประตูค่ายทิ้งไว้ [anxious:0.7] ถ้าหัวหน้ารู้เข้า พวกเราโดนหนักแน่ ``` A guard in a game: - [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/game.m4a An audio novel keeps the narration calm until the back door opens. [Audio novels](https://paxalabs.com/use-cases/audio-novels) covers long chapters read in one voice. text: ``` [calm] คืนนั้นฝนตกทั้งคืน บ้านทั้งหลังเงียบสนิท [terrified] จนกระทั่งประตูหลังบ้านค่อย ๆ เปิดออกเอง ``` A line of narration: - [Kaprao](https://paxalabs.com/voices/kaprao), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/story.m4a A support agent apologizes for a late parcel, then gives the new delivery date. [Voice agents](https://paxalabs.com/use-cases/voice-agents) covers speech for phone and chat assistants. text: ``` [disappointed:0.4] ขออภัยค่ะ พัสดุของคุณส่งช้ากว่ากำหนด [cheerful] ตอนนี้ออกจากคลังแล้ว คาดว่าจะถึงพรุ่งนี้ค่ะ ``` A support reply: - [Khanom Chan](https://paxalabs.com/voices/khanomchan), Emotion tags: https://paxalabs.com/audio/blog/emotion-tags-for-thai-speech/support.m4a ## Where tags work - `POST /v1/tts`, buffered or streamed. With timestamps on, the spans cover the spoken text, and a tag takes none. - `POST /v1/audio/speech`, the [OpenAI-compatible route](https://paxalabs.com/docs/openai-compatibility), reads tags in `input`. - The [live socket](https://paxalabs.com/docs/live-streaming) reads a tag split across two frames whole. Each leg opens in the delivery the last one ended in, until `cancel` resets it. - The Speech tool in the console has an Emotion panel with every tag and an intensity slider, and draws each tag in the voice's color as you type. ## What the API refuses On the two voices, a bracketed word outside the list, such as `[laughs]`, or a weight that is not a number, such as `[sad:half]`, answers 400 `tag_invalid` before any charge. A word outside the list followed by a colon, such as `[Fig:2]`, is ordinary text and is spoken. Text made only of tags answers 422 `unspeakable_text` before any charge. The other voices refuse no tag. They drop the tags on the list and read a bracketed word outside it aloud. ## What tags cost Tags are text, and their characters bill at the [one speech rate](https://paxalabs.com/pricing), 10 credits per 1,000 characters. The request above is 47 characters with its tags, and it costs 0.47 credits. ## What experimental means The voice ids and the tag list stay as they are. How each tag sounds may change while we tune the two voices, and a line generated later may sound different from the same line today. Every other voice keeps its delivery. ## Try it - Open the Speech tool, pick Khanom Chan or Kaprao, and open the Emotion panel. - Read the [emotion tags guide](https://paxalabs.com/docs/emotion-tags) for the grammar, the weights, and the live socket. # Paxa TTS Flash starts speaking in 104 milliseconds We measured time to first audio on Paxa TTS Flash from Google Cloud in Singapore and Bangkok: 104 ms and 131 ms at the median, with no failed requests. Published 2026-09-25. Paxa Labs. In conversation, the most common gap between one person finishing and the next person starting is under 200 milliseconds. A [study of ten languages](https://www.pnas.org/doi/10.1073/pnas.0903616106) found that pattern in every one of them. A voice agent answers inside the same gap, and the speech API spends part of it before any sound plays. On September 24 we measured that part from two cities. Virtual machines in the Singapore and Bangkok regions of Google Cloud called the production API the way a customer's server does. - Median time to first audio was 104 ms from Singapore and 131 ms from Bangkok. - 98.3% of requests from Singapore and 91.2% from Bangkok returned first audio in under 200 ms. - A 35.1-second Thai passage arrived complete within 750 ms. - All 1,676 speech requests succeeded. ## Time to first audio | Client location | Requests | p50 | p90 | p99 | Under 200 ms | | --- | --- | --- | --- | --- | --- | | Singapore | 299 | 104 ms | 157 ms | 324 ms | 98.3% | | Bangkok | 284 | 131 ms | 195 ms | 304 ms | 91.2% | Each request streamed a 93-character Thai line in the Nom Yen voice as MP3 from [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech), which is 6.9 seconds of audio. The clock starts when the client sends the request and stops at the first byte of audio. That span covers the network trip, authentication, and synthesis of the opening words. The p99 column is the slowest request in a hundred. At that point first audio still arrived within 324 ms from Singapore and 304 ms from Bangkok. ## Where the time goes The gap between the two cities is network distance. A bare round trip to the API's health endpoint took 16 ms at the median from Singapore and 42 ms from Bangkok. Subtract it from time to first audio and the two cities leave 88 and 89 ms. That remainder is the time the API spends between receiving a request and sending its first audio, and it is the same from either city. ## Long text starts as fast as short text A streamed response starts from the opening of the text while the rest is still being synthesized. A 420-character Thai passage, 35.1 seconds of audio, returned its first audio in 76 ms from Singapore and 102 ms from Bangkok at the median. The whole passage finished arriving within 750 ms, about 47 times faster than it plays. ## English, Opus, and the OpenAI endpoint The same run measured an English line, Opus audio, the OpenAI-compatible endpoint, and the longer Thai passage. Each figure below is time to first audio. | Request | Singapore p50 | Singapore p90 | Bangkok p50 | Bangkok p90 | | --- | --- | --- | --- | --- | | Thai, MP3, 93 characters | 104 ms | 157 ms | 131 ms | 195 ms | | English, MP3, 97 characters | 94 ms | 131 ms | 119 ms | 162 ms | | Thai, Opus, 93 characters | 110 ms | 158 ms | 131 ms | 197 ms | | Thai, OpenAI-compatible endpoint | 107 ms | 161 ms | 126 ms | 185 ms | | Thai, MP3, 420 characters | 76 ms | 112 ms | 102 ms | 132 ms | The English line used the Cookie voice and runs 5.3 seconds. The OpenAI-compatible endpoint, [POST /v1/audio/speech](https://paxalabs.com/docs/api/speech), landed within 5 ms of POST /v1/tts at the median from both cities. Code written for OpenAI's speech API gets the same first audio with a changed base URL. ## Complete audio A voice-over rendered into a video needs the whole file before anything plays. Without streaming, the 6.9-second Thai line arrived as a complete file in 241 ms at the median from Singapore and 280 ms from Bangkok. | Audio | Singapore p50 | Singapore p90 | Bangkok p50 | Bangkok p90 | | --- | --- | --- | --- | --- | | 6.9 s Thai line, one file | 241 ms | 362 ms | 280 ms | 396 ms | | 35.1 s Thai passage, streamed to the end | 750 ms | 1,021 ms | 747 ms | 981 ms | ## How we measured - The clients were two Google Cloud n4-standard-2 virtual machines, one in zone asia-southeast1-b (Singapore) and one in zone asia-southeast3-a (Bangkok). - Both ran Node.js 26.3 and called api.paxalabs.com over HTTPS with an API key that had no credit limit. Each sent one request at a time on one connection kept open between requests. - The two clients ran at the same time, from 10:31 to 10:38 UTC on September 24, 2026, with the tests interleaved. - Time to first audio is measured at the client, from sending the request to receiving the first byte of the response body. - Each row counts between 46 and 299 requests. - Thai requests used Nom Yen and English requests used Cookie. The two short lines are the published sample lines of those voices. ## Hear it yourself The [text to speech page](https://paxalabs.com/tts) speaks a line in any of its demo voices without an account. To call the API, [Optimize latency](https://paxalabs.com/docs/latency) lists the steps that run before the first byte and which of them a client controls. The two voices in the test, on their published sample lines: - [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/nomyen.m4a - [Cookie](https://paxalabs.com/voices/cookie), Support agent: https://paxalabs.com/audio/cookie.m4a # Paxa STT Lite Realtime transcribes Thai turn by turn, as it is spoken Paxa STT Lite Realtime transcribes Thai over one open WebSocket, finds the end of each turn on its own, and bills the audio it receives. Published 2026-09-24. Paxa Labs. A Thai voice agent cannot answer until it knows the caller has finished. Thai gives it very little to go on. The writing carries no full stop, the spaces fall between phrases, and a speaker's pause lands mid-sentence about as often as it lands at the end of one. Until September 23, a team building on our API transcribed finished recordings and wrote that judgment itself. Paxa STT Lite Realtime now transcribes speech as the audio arrives over one WebSocket, decides where each turn ended, and delivers one finished transcript for it. The model id is `paxa-stt-lite-realtime-v1-preview` and the endpoint is `WS /v1/stt/live`. ## What shipped - A realtime model, `paxa-stt-lite-realtime-v1-preview`, served over `WS /v1/stt/live`. One connection carries a whole conversation. - Turn detection inside the socket. `speech_started` opens a turn, `speech_ended` closes it, and one final `transcript` follows. - Audio in signed 16-bit PCM at 16 or 48 kHz, or in telephony mu-law at 8 kHz. - The language hint, word timestamps, the style, the convention, and the vocabulary of `POST /v1/stt`, all fixed for the connection at `start`. - Billing on the audio received, silence included, at 12.5 credits a minute. A `charged` receipt lands as the conversation runs. - A live mode in the [Transcribe tool](https://paxalabs.com/app/transcribe), which relays a browser microphone with no code at all. ## One session, unedited The ledger below is a capture from 2026-09-23. Three call-centre prompts, generated with Paxa TTS Flash, were played into one connection with a pause between them. That is 29.95 seconds of audio. The turn boundaries, the transcripts, and the charges are what the socket reported, reproduced without edits. | Turn | Seconds | Credits | Transcript | | --- | --- | --- | --- | | 1 | 0 to 2.7 | 0.57 | สวัสดีค่ะขอบคุณที่ติดต่อบริษัทของเรา | | 2 | 2.7 to 8 | 1.1 | กด 1 เพื่อติดต่อฝ่ายขายกด 2 ฝ่ายบริการลูกค้าหรือกด 0 เพื่อติดต่อเจ้าหน้าที่ค่ะ | | 3 | 9.11 to 18.05 | 2.1 | สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน 1,250 บาทครบกำหนดวันที่ 15 สิงหาคมนี้ชำระผ่านแอปได้ตลอด 24 ชั่วโมงค่ะ | | 4 | 19.16 to 22.9 | 1.01 | ขณะนี้อยู่นอกเวลาทำการเราเปิดให้บริการวันจันทร์ถึงศุกร์ | | 5 | 22.9 to 23.71 | 0.16 | เวลา | | 6 | 23.92 to 26.44 | 0.57 | 9 โมงเช้าถึง 6 โมงเย็น | | 7 | 26.44 to 28.55 | 0.44 | ฝากข้อความหลังเสียงสัญญาณได้เลยค่ะ | Read turns 4 through 6 together and they are one sentence about opening hours. The speaker paused before the word for time and again before the numbers, and the socket closed a turn at each pause. That is the Thai boundary problem showing up in a real capture. An agent holding its reply until the caller has been quiet for a moment reads all three and answers once. The turns charged 5.95 credits between them. The connection kept listening for another 1.4 seconds after the last turn closed, which added 0.29. The total the socket reported was 6.24 credits. ## What travels on the socket Three frames go up and seven events come down. Audio is binary and everything else is JSON, on the one connection. turn 1 on the wire: ``` client -> {"type":"start","model":"paxa-stt-lite-realtime-v1-preview", "audio":{"encoding":"pcm_s16le","sample_rate":16000}, "language":"th","convention":"written"} server <- {"type":"started","connection":"7d9c2b7e","model":"paxa-stt-lite-realtime-v1-preview"} client -> server <- {"type":"speech_started","turn":1,"time":0.000} server <- {"type":"transcript","turn":1,"is_final":false,"text":"สวัสดีค่ะ"} server <- {"type":"speech_ended","turn":1,"time":2.700} server <- {"type":"charged","seq":1,"seconds":2.700,"credits":0.57} server <- {"type":"transcript","turn":1,"is_final":true,"text":"สวัสดีค่ะขอบคุณที่ติดต่อบริษัทของเรา"} client -> {"type":"end"} server <- {"type":"done","turns":7,"total_credits":6.24} ``` - `start` names the model and the audio encoding and carries every option for the session. It is the first frame, exactly one per connection. - `playback` tells the socket when your agent's own voice is playing into the room the microphone is in. - `end` closes the turn in progress, delivers its transcript, and asks for `done`. The events coming down are the other half of the contract. - `started` confirms the session and names the connection id. Quote that id in a support request. - `speech_started` opens a turn and carries where the speech began on the audio clock. - `transcript` with `is_final` false is an interim draft of the turn so far. It is replaced by the next one. - `speech_ended` closes the turn. - `charged` is a billing receipt for one period, with the seconds it covered and the running total. - `transcript` with `is_final` true is the turn. This is the text a program acts on. - `error` names a failed turn, or reports a fatal failure before the close. - `done` carries the connection's totals and precedes a normal close. ## Turn detection, and the interruption signal `speech_started` arrives once the socket has heard enough to be sure. The time it reports is therefore earlier than the moment it lands on your side. Treat the event as the interruption signal. An agent that is mid-sentence stops its own audio and cancels the reply it was generating. `speech_ended` says the turn is closed and its final transcript is coming. A voice agent can show that it is thinking from here. The final follows once that turn's billing period has landed, which is what keeps a transcript from arriving ahead of its receipt. > The `playback` frame is what keeps an agent from transcribing itself. Send `{"type":"playback","speaking":true}` when your agent's audio starts and `false` when it stops. While it is true, opening a turn takes more sustained speech from the caller, and a real interruption still opens one. Turns count from 1 per connection, and a higher number is always a later turn. An agent that already answered turn 3 drops turn 3 and answers turn 4. That single comparison is the whole of the staleness rule. ## The options carry over Everything `POST /v1/stt` takes as a request field, the socket takes once in `start` and holds for the life of the connection. None of it changes the charge. | Field | What it does | | --- | --- | | `language` | A BCP 47 hint. Left out, the model detects the language from the audio and writes each language in its own script. | | `timestamps` | `"word"` adds a span array to every final transcript, one entry per word on the connection's clock. Thai words are segmented for you. | | `style` | `verbatim` writes fillers and false starts as they were said. `clean` drops them and reads more easily. Verbatim is the default and follows the audio most closely. | | `convention` | `spoken` writes numbers as the caller said them. `written` puts them in digits with units and ๆ, which is the form a booking or a payment field reads. | | `vocabulary` | Up to 50 terms of 50 characters the conversation is likely to contain, spelled the way they should appear. | Speaker numbering and subtitle files stay on the recording endpoint. One pass over a whole recording is what numbers the speakers and cuts the cues, and a live connection has no whole recording to read. ## The audio the socket takes | Encoding | Sample rate | Where it comes from | | --- | --- | --- | | `pcm_s16le` | 16 or 48 kHz | Signed 16-bit little-endian PCM, mono. A browser microphone through an audio worklet, or a capture device at its native rate. | | `mulaw` | 8 kHz | G.711 mu-law, mono, as a telephony platform hands it over. No transcoding on your side. | - Binary frames carry the bytes in any chunking, up to 64 KiB a frame. Twenty milliseconds is a common size for a microphone. - Any cadence works. Sending faster than real time is fine, and the clock the events report is the audio clock either way. - The bytes are read exactly as `start` declared them. A wrong sample rate transcribes noise. > An API key never belongs in a browser. The upgrade is authenticated with the same `Authorization` header as every other endpoint, and a browser WebSocket cannot send one. Relay the microphone through your own server. ## What a session costs A connection is billed for the audio it receives, silence included, at 12.5 credits a minute. That is 750 credits an hour, about $0.75. In smaller units it is one hundredth of a credit for every 48 milliseconds on the wire. The charge is taken in periods. A period closes at the end of each turn, after a minute of audio with no turn in it, and at the close of the session. Each one arrives as a `charged` receipt. The 0.1-credit request floor applies once per connection, on the first receipt. A turn the model could not transcribe costs nothing. Its audio is held back from the running total and an `error` event names the turn and the credits it returned. A connection that delivered no turn at all is refunded in full. | Workload | Audio received | Credits | Cost | | --- | --- | --- | --- | | One 3-minute call | 3 minutes | 37.5 | $0.04 | | 200 calls in a day | 10 hours | 7,500 | $7.50 | | A 30-day month of them | 300 hours | 225,000 | $225 | The 29.95-second capture above cost 6.24 credits. A new account's 100 free credits cover about 8 minutes of a live connection. In [the request log](https://paxalabs.com/app/logs) the whole connection is one row, whatever it took in turns. The row carries the audio received, the credits charged, and the connection id the socket announced at `started`. ## When something fails An `error` carrying a `turn` leaves the connection open. That turn delivers no transcript, its audio is held back from the bill, and the next turn proceeds. An agent can treat it the way it treats a caller it did not hear and ask again. A fatal failure closes the socket with a code. Three of them are worth handling in code. - `1001` is a restart on our side. Reconnect and keep going. Everything the delivered periods charged stays charged. - `4429` means the account already holds too many live connections. Close one and reconnect. - `4503` means the backend could not be reached at `start`. Nothing was charged. Retry. The full close-code list, with every frame field, is in the [realtime reference](https://paxalabs.com/docs/api/stt-live). Every code an `error` event can carry is in the [error reference](https://paxalabs.com/docs/errors). ## Limits - A connection accepts 4 hours of audio, then closes with `done` the way an `end` frame would. - An open connection holds one of the plan's [concurrent transcription slots](https://paxalabs.com/docs/rate-limits) for as long as it stays connected. - A binary frame is 64 KiB at most. - A connection with no frame in and no event out for 300 seconds closes. ## Recording or realtime The two models answer different questions. A file that already exists belongs on `POST /v1/stt`, which reads the whole recording before it answers and costs less a minute for doing so. | | Paxa STT Lite | Paxa STT Lite Realtime | | --- | --- | --- | | Transport | `POST /v1/stt` | `WS /v1/stt/live` | | Input | A file up to 60 minutes and 25 MiB | Binary audio for up to 4 hours | | Answer | One transcript for the whole recording | One final transcript per turn | | Rate | 8.33 credits a minute | 12.5 credits a minute | | Speaker numbering | Yes, counted from 0 | Recording endpoint only | | Subtitle files | srt and vtt | Recording endpoint only | | Charged | Once, before transcription | In periods, as the conversation runs | Both models are a research preview. They are sold at the published rate while they are evaluated, and their output and options may change between releases. The ids stay stable for as long as they serve. ## Opening a session One start frame names the model and the audio encoding. Everything after it is binary audio going up and events coming down, on a connection that stays open for the conversation. TypeScript: ```typescript import { open } from "node:fs/promises"; // Runs on Bun as-is; on Node, `import WebSocket from "ws"` accepts // the same constructor options. Browsers cannot send this header, and // API keys never ship to browsers anyway: relay the audio through your // server. const socket = new WebSocket("wss://api.paxalabs.com/v1/stt/live", { headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}` }, }); socket.onopen = async () => { socket.send(JSON.stringify({ type: "start", model: "paxa-stt-lite-realtime-v1-preview", audio: { encoding: "pcm_s16le", sample_rate: 16000 }, language: "th", })); // 16 kHz signed 16-bit mono is 32,000 bytes a second. A microphone // delivers frames at this pace on its own; a file has to be paced. const file = await open("caller.pcm"); const frame = new Uint8Array(640); // 20 ms for (;;) { const { bytesRead } = await file.read(frame, 0, frame.length); if (bytesRead === 0) break; socket.send(frame.subarray(0, bytesRead)); await new Promise((resolve) => setTimeout(resolve, 20)); } socket.send(JSON.stringify({ type: "end" })); }; socket.onmessage = (event) => { const message = JSON.parse(String(event.data)); switch (message.type) { case "speech_started": // The user is talking: stop the agent's audio, cancel its reply. break; case "transcript": if (message.is_final) console.log(`turn ${message.turn}: ${message.text}`); break; case "done": socket.close(); } }; ``` Python: ```python import json import os import time from websockets.sync.client import connect # pip install websockets with connect( "wss://api.paxalabs.com/v1/stt/live", additional_headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, ) as socket: socket.send(json.dumps({ "type": "start", "model": "paxa-stt-lite-realtime-v1-preview", "audio": {"encoding": "pcm_s16le", "sample_rate": 16000}, "language": "th", })) # 16 kHz signed 16-bit mono is 32,000 bytes a second; a file has to # be paced the way a microphone paces itself. with open("caller.pcm", "rb") as audio: while frame := audio.read(640): # 20 ms socket.send(frame) time.sleep(0.02) socket.send(json.dumps({"type": "end"})) for message in socket: event = json.loads(message) if event["type"] == "transcript" and event["is_final"]: print(event["turn"], event["text"]) elif event["type"] == "done": break ``` Server events: ```json {"type":"started","connection":"7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a","model":"paxa-stt-lite-realtime-v1-preview"} {"type":"speech_started","turn":1,"time":0.3} {"type":"speech_ended","turn":1,"time":12.252} {"type":"charged","seq":1,"seconds":12.252,"credits":2.56,"total_credits":2.56} {"type":"transcript","turn":1,"is_final":true,"text":"สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ","start":0.3,"end":12.252} {"type":"done","total_seconds":12.252,"turns":1,"total_credits":2.56} ``` ## Before you write any code [Transcribe](https://paxalabs.com/app/transcribe) has a live mode. It asks for the microphone, relays the audio through our server, and shows each turn as it closes with the running charge beside it. Speak into it and watch where the boundaries fall on your own Thai. ## Before you ship - Act on finals. An interim transcript is a draft and the next one replaces it. - Send `playback` around your agent's own audio. Without it the agent hears itself and opens a turn. - Drop a turn you already answered. The turn number is the only ordering a caller needs. - Declare the encoding the bytes actually carry. A wrong sample rate reaches the model as noise. - Reconnect on `1001`. A deploy on our side should be invisible to the person on the line. - Set a `vocabulary` for the names your callers say. Product names, Thai place names, and staff names are what it is for. - Hold a reply until the caller has been quiet for a moment. Thai pauses at phrase breaks, and a turn can close mid-sentence. ## Next steps - Open [Transcribe](https://paxalabs.com/app/transcribe), switch to live mode, and speak a sentence your callers would say. - Read [where a turn ends](https://paxalabs.com/guides/realtime-thai-transcription-turns) for why Thai makes the boundary awkward, and what the interim text is good for. - Read the [live transcription segment](https://paxalabs.com/use-cases/live-transcription) for what a call-centre workload costs. - Read the [speech to text product page](https://paxalabs.com/speech-to-text) for this session beside the recording endpoint's own capture. The contract itself lives in the docs. - [Realtime transcription](https://paxalabs.com/docs/api/stt-live): Every frame field and close code - [Transcribe recordings](https://paxalabs.com/docs/speech-to-text): The options, and what each one does - [Rate limits and concurrency](https://paxalabs.com/docs/rate-limits): Connections a plan may hold at once - [Credits, charges, and refunds](https://paxalabs.com/docs/credits): Periods, receipts, and refunds # Realtime Thai transcription, and where a turn ends Thai writes no full stop, and a live transcriber has to decide where the speaker finished anyway. What a turn is, what interim text is for, and what a live session costs. Published 2026-09-24. Paxa Labs. Thai does not close a sentence with a mark. The writing puts spaces at phrase and clause breaks and runs the rest together, and speech carries no punctuation at all. A transcriber working on a finished recording can read the whole file before it commits to anything. A transcriber working live has to commit while the person is still talking. Paxa STT Lite Realtime settles that by working in turns. The socket watches the audio, decides where a stretch of speech ended, and sends one finished transcript for it. What follows covers the turn itself, why Thai makes the boundary awkward, what the interim text before it is good for, and what a session costs. ## What a turn is A turn is one continuous stretch of one speaker's audio, bounded by the moment they started and the moment they stopped. The socket announces both. Between them it sends interim transcripts, each a fuller draft of the same turn. After the end it sends one final transcript, and the final is the text a program acts on. A caller can always take words as they are recognized and decide for itself where a thought landed. That decision is the hard part of a live pipeline, and in Thai it is harder than the recognition. turn 2: ``` speech_started turn 2 transcript turn 2 interim ผม transcript turn 2 interim ผมโอนไปแล้ว transcript turn 2 interim ผมโอนไปแล้วเมื่อเช้า speech_ended turn 2 transcript turn 2 final ผมโอนไปแล้วเมื่อเช้านี้ครับ ``` The word that arrives last in that turn is ครับ, and it carries no information about the payment at all. It marks that the speaker is done. An agent acting on the third interim would have answered a sentence that was still being built. ## Where Thai makes the boundary awkward Three properties of Thai speech work against an endpoint rule written for English. - There is no full stop. A Thai sentence ends because it is finished, and the page marks that with a space at most. A rule waiting for a terminal mark waits forever. - A polite utterance closes on a word. ครับ and ค่ะ end a turn, and so do นะคะ and แล้วครับ. They arrive after the content, which means the informative half of the sentence is complete before the speaker is. - A pause lands at a phrase break. Thai puts its spaces between phrases, and a speaker stops at those same places in the middle of a sentence. The gap sits well short of the end of the thought. The third one is what breaks a boundary rule built on silence alone. The cut lands between a noun and its classifier, or between a verb and the amount it governs. Each half of the split reads as a fragment that means less than the sentence did, and a downstream model answers the fragment it was handed. Code-switching sharpens the same problem. Thai speakers switch into English inside a clause. Any gap that opens around the English word sits in the middle of a sentence that is still running. The transcript has to keep both scripts in one line and count that gap as part of the turn. ## What the interim text is for Interim transcripts are drafts. Each one replaces the one before it, and any of them can change as more audio arrives. Put them on a screen where a person is watching a caption move. Keep them out of anything that acts: a booking made from an interim transcript is a booking made from half a sentence. The final transcript is the commitment. It carries the turn number, the text, and where the turn sat in the audio. A voice agent answers on the final. A supervisor's keyword alert fires on the final. A live caption paints the interims and settles on the final. > A turn boundary is also the signal to stop talking. The moment the socket reports speech_started, a voice agent cancels the reply it is speaking, which is how a caller talks over an assistant and is heard. ## What a live session costs Billing follows the audio the socket receives, silence included, at 12.5 credits a minute. The connection is cut into periods at each turn and charged as it runs, which is what lets a long session report a running total. A turn the model fails to transcribe costs nothing, and the total reported already accounts for it. In the request log the whole connection is one line, whatever it took in turns. Periods are how the money is counted; the session is what the caller asked for. A recording that already exists belongs on the buffered endpoint. Paxa STT Lite takes a file up to 60 minutes, answers once with the whole transcript, and costs less a minute because nothing has to keep pace with a speaker. ## Opening the socket One start frame names the model and the audio encoding. Everything after it is binary audio going up and events coming down, on a connection that stays open for the conversation. TypeScript: ```typescript import { open } from "node:fs/promises"; // Runs on Bun as-is; on Node, `import WebSocket from "ws"` accepts // the same constructor options. Browsers cannot send this header, and // API keys never ship to browsers anyway: relay the audio through your // server. const socket = new WebSocket("wss://api.paxalabs.com/v1/stt/live", { headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}` }, }); socket.onopen = async () => { socket.send(JSON.stringify({ type: "start", model: "paxa-stt-lite-realtime-v1-preview", audio: { encoding: "pcm_s16le", sample_rate: 16000 }, language: "th", })); // 16 kHz signed 16-bit mono is 32,000 bytes a second. A microphone // delivers frames at this pace on its own; a file has to be paced. const file = await open("caller.pcm"); const frame = new Uint8Array(640); // 20 ms for (;;) { const { bytesRead } = await file.read(frame, 0, frame.length); if (bytesRead === 0) break; socket.send(frame.subarray(0, bytesRead)); await new Promise((resolve) => setTimeout(resolve, 20)); } socket.send(JSON.stringify({ type: "end" })); }; socket.onmessage = (event) => { const message = JSON.parse(String(event.data)); switch (message.type) { case "speech_started": // The user is talking: stop the agent's audio, cancel its reply. break; case "transcript": if (message.is_final) console.log(`turn ${message.turn}: ${message.text}`); break; case "done": socket.close(); } }; ``` Python: ```python import json import os import time from websockets.sync.client import connect # pip install websockets with connect( "wss://api.paxalabs.com/v1/stt/live", additional_headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}, ) as socket: socket.send(json.dumps({ "type": "start", "model": "paxa-stt-lite-realtime-v1-preview", "audio": {"encoding": "pcm_s16le", "sample_rate": 16000}, "language": "th", })) # 16 kHz signed 16-bit mono is 32,000 bytes a second; a file has to # be paced the way a microphone paces itself. with open("caller.pcm", "rb") as audio: while frame := audio.read(640): # 20 ms socket.send(frame) time.sleep(0.02) socket.send(json.dumps({"type": "end"})) for message in socket: event = json.loads(message) if event["type"] == "transcript" and event["is_final"]: print(event["turn"], event["text"]) elif event["type"] == "done": break ``` Server events: ```json {"type":"started","connection":"7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a","model":"paxa-stt-lite-realtime-v1-preview"} {"type":"speech_started","turn":1,"time":0.3} {"type":"speech_ended","turn":1,"time":12.252} {"type":"charged","seq":1,"seconds":12.252,"credits":2.56,"total_credits":2.56} {"type":"transcript","turn":1,"is_final":true,"text":"สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ","start":0.3,"end":12.252} {"type":"done","total_seconds":12.252,"turns":1,"total_credits":2.56} ``` ## Next steps - Read the [realtime reference](https://paxalabs.com/docs/api/stt-live) for the frames, the events, and the close codes. - Read the [transcription guide](https://paxalabs.com/docs/speech-to-text) for the options a start frame carries. - Read the [live transcription segment](https://paxalabs.com/use-cases/live-transcription) for what a session costs at a call-centre workload. - Open the [transcribe tool](https://paxalabs.com/app/transcribe) and switch to live mode. The browser relays its microphone and each turn lands as it closes. # Paxa TTS Flash now speaks Mandarin Chinese, with six new voices Paxa TTS Flash reads Mandarin Chinese from Simplified script beside Thai and English, and six new voices join the roster, two in each language. Published 2026-09-16, updated 2026-10-01. Paxa Labs. A Thai business with Chinese-speaking customers publishes Chinese copy beside its Thai copy. Until September 14, voicing the Chinese half took a second speech vendor or a recording session. It now goes through the same API request as the Thai copy, with a Mandarin voice. The six new voices: - [Tao Huay](https://paxalabs.com/voices/taohuay), Drama lead: https://paxalabs.com/audio/taohuay.m4a - [Oolong](https://paxalabs.com/voices/oolong), Audio fiction: https://paxalabs.com/audio/oolong.m4a - [Espresso](https://paxalabs.com/voices/espresso), Long-form reader: https://paxalabs.com/audio/espresso.m4a - [Mocha](https://paxalabs.com/voices/mocha), Wellness: https://paxalabs.com/audio/mocha.m4a - [Khao Niao](https://paxalabs.com/voices/khaoniao), Breathwork: https://paxalabs.com/audio/khaoniao.m4a - [Tom Yum](https://paxalabs.com/voices/tomyum), Best friend: https://paxalabs.com/audio/tomyum.m4a ## What changed - Paxa TTS Flash reads Mandarin Chinese beside Thai and English, from Simplified script (简体中文). - Six voices join the roster, for 32 in all. Two are Mandarin-first, two English-first, and two Thai-first. - `GET /v1/voices` returns `"zh"` in the `language` field for the Mandarin voices. - The model id stays `paxa-tts-flash-v1`, and existing voice ids and requests work as before. ## Where the new voices work Every speech surface resolves the new voice ids the way it resolves the existing ones. - `POST /v1/tts` returns buffered or streamed audio in mp3, opus, or wav. - `POST /v1/audio/speech`, the [OpenAI-compatible route](https://paxalabs.com/docs/openai-compatibility), takes `taohuay` or `oolong` as its `voice`. - The [live socket](https://paxalabs.com/docs/live-streaming) reads Mandarin while the text is still arriving. There, `。`, `!`, and `?` end a sentence the way a period does, and the buffered sentence is spoken. - `speed`, from 0.5 to 1.5, applies to the Mandarin voices as it does to every voice. ## The six voices, one by one ### Tao Huay, drama lead [Tao Huay](https://paxalabs.com/voices/taohuay) is tender and a little melancholy. It is cast for drama recaps, audio novels, and companion speech. Its published sample reads “如果时间可以倒流,我还是会在那个下雨的傍晚,安静地等你回来。” The line means “If time could run backward, I would still wait quietly for you to come back on that rainy evening.” A Thai companion app that also answers in Mandarin can pair it with [Sanae Chan](https://paxalabs.com/voices/sanaechan), the Thai voice the [AI companion page](https://paxalabs.com/ai-companion) opens on. ### Oolong, audio fiction [Oolong](https://paxalabs.com/voices/oolong) is a low, rich narrator, cast for audio novels, brand reads, and late-night storytelling. Its sample reads “夜深了,城市渐渐安静下来。今晚的故事,从一封迟到二十年的信开始。” The line means “It is late at night, and the city is slowly growing quiet. Tonight's story begins with a letter that arrived twenty years late.” The Thai voice for the same late hour is [Oliang](https://paxalabs.com/voices/oliang), cast for late-night radio. ### Espresso, long-form reader [Espresso](https://paxalabs.com/voices/espresso) is deep, dark, and slow, cast for audiobooks, documentaries, and trailers. Its sample reads “Some stories are never told. They are kept, and passed from hand to hand, until someone finally dares to open the book.” [Latte](https://paxalabs.com/voices/latte) remains the English voice cast for polished brand films. Espresso takes the long read and the trailer. ### Mocha, wellness [Mocha](https://paxalabs.com/voices/mocha) is close, gentle, and low in volume, cast for bedtime, wellness, and companion speech. Its sample reads “Take a slow breath. There is no rush tonight. Whatever happened today, you made it here, and that is enough.” A wellness app in Thai and English can pair it with [Lod Chong](https://paxalabs.com/voices/lodchong), cast for sleep and meditation. The [meditation page](https://paxalabs.com/meditation) plays the three calm Thai voices side by side. ### Khao Niao, breathwork [Khao Niao](https://paxalabs.com/voices/khaoniao) is low and unhurried, cast for guided breathing, wellbeing narration, and slow documentary. Its sample reads “หายใจเข้าช้า ๆ แล้วปล่อยออกไปครับ ไม่ต้องรีบ ทุกอย่างรอได้ ตอนนี้แค่อยู่ตรงนี้ก็พอแล้วครับ” The line means “Breathe in slowly, then let it out. There is no rush, and everything can wait. For now, being here is enough.” ครับ, the polite ending of a male speaker, appears twice. It joins [Sangkaya](https://paxalabs.com/voices/sangkaya), the quiet close-mic voice cast for ASMR and calm narration. ### Tom Yum, best friend [Tom Yum](https://paxalabs.com/voices/tomyum) is fast, hot, and chatty, a best-friend voice cast for drama recaps, entertainment news, and social clips. Its sample reads “แกรู้ยัง เมื่อคืนมีเรื่องใหญ่มากเลย ฉันเพิ่งรู้มาสด ๆ ร้อน ๆ เดี๋ยวเล่าให้ฟังนะจ๊ะ” The line means “Have you heard? Something big happened last night. I only just found out. Let me tell you.” The register lives in the words. แก is the second-person pronoun of close friends, and จ๊ะ is a playful, familiar ending. The same script written in polite Thai reads as polite in this voice too. For ads and shorts with a confident male read, [Moo Ping](https://paxalabs.com/voices/mooping) remains the voice with street energy. ## How a request reads Chinese The model reads each run of text by its script: Chinese characters as Mandarin, Thai script as Thai, and Latin words as English. A Thai announcement and its Mandarin version differ only in `voice` and `text`. The optional `language` field changes only how codes, digits, and symbols are read, and its default, `auto`, follows the language most of the text is written in. one endpoint, two languages: ``` POST /v1/tts { "model": "paxa-tts-flash-v1", "voice": "nomyen", "text": "ยินดีต้อนรับค่ะ กรุณาเตรียมเอกสารการจองไว้ที่จุดเช็กอินค่ะ" } { "model": "paxa-tts-flash-v1", "voice": "taohuay", "text": "欢迎光临,请在办理入住处准备好您的预订资料。" } ``` The two requests above: - [Nom Yen](https://paxalabs.com/voices/nomyen), Lead voice: https://paxalabs.com/audio/blog/mandarin-chinese-and-six-new-voices/arrival-th.m4a - [Tao Huay](https://paxalabs.com/voices/taohuay), Drama lead: https://paxalabs.com/audio/blog/mandarin-chinese-and-six-new-voices/arrival-zh.m4a The voice sets the accent. A Mandarin-first voice is designed for sentences written wholly in Chinese, and it reads Thai or English with a foreign accent. A Chinese name inside a Thai sentence takes the accent of the Thai voice reading it. ## Chinese family names in Thai copy Chinese characters are always read as Mandarin, which matters most for family names. [More than half of Thailand's Chinese community](https://en.wikipedia.org/wiki/Thai_Chinese) traces its ancestry to Chaoshan, where Teochew is spoken. The family names Thai speakers know carry those Teochew sounds, as [Sanook's list of common family names](https://www.sanook.com/campus/1431575/) shows beside the Mandarin readings. | Characters | Family name in Thai | Mandarin reading | | --- | --- | --- | | 陈 | แซ่ตั้ง | Chén | | 林 | แซ่ลิ้ม | Lín | | 黄 | แซ่อึ้ง | Huáng | | 李 | แซ่ลี้ | Lǐ | Sent as characters, 林 is read Lín. In Thai copy, write the family name in Thai script, as ลิ้ม, and a Thai voice says it the way the family does. In Mandarin copy written for a Mandarin speaker, the Mandarin reading is the one the listener expects. ## What Mandarin costs Mandarin bills at 10 credits per 1,000 characters, the [one speech rate](https://paxalabs.com/pricing) for every voice and language. A Chinese character carries a whole syllable, while Thai writes each vowel and tone mark as a character of its own. Every announcement on the [Thai and Mandarin announcements page](https://paxalabs.com/thai-chinese) takes fewer characters in Mandarin. | Announcement | Thai | Mandarin | | --- | --- | --- | | Arrival | 58 characters, 0.58 credits | 22 characters, 0.22 credits | | Directions | 36 characters, 0.36 credits | 12 characters, 0.12 credits | | Closing time | 55 characters, 0.55 credits | 21 characters, 0.21 credits | A new account's 100 free credits voice the Mandarin arrival announcement 454 times. ## One voice for each language A product that speaks three languages keeps one voice per language for each job. Each line-up takes its voices from their catalog casting, or from a page that already pairs them. | Job | Thai | English | Mandarin | | --- | --- | --- | --- | | Audiobooks and long reads | Foi Thong | Espresso | Oolong | | Companion speech | Sanae Chan | Mocha | Tao Huay | | Brand reads | Nom Yen | Latte | Oolong | | Front desk announcements | Nom Yen | Cookie | Tao Huay | The front desk line-up matches the [Thai and Mandarin announcements page](https://paxalabs.com/thai-chinese), where Nom Yen and Tao Huay read each announcement as one announcer in two languages. ## Before you publish - Convert Traditional script to Simplified before sending. Only Simplified script is verified. - Expect Mandarin for every Chinese line, including copy written for Cantonese speakers. - Have a Mandarin speaker write or check the copy. The voice reads the characters it receives, mistakes included. - Play a line that mixes Thai and Chinese in the Speech tool before customers hear it. ## Try Mandarin The [free text to speech page](https://paxalabs.com/tts) reads a line of up to 50 characters in Tao Huay or Oolong, without an account. Signed in, the [Speech tool](https://paxalabs.com/app/speech?voice=taohuay) takes up to 5,000 characters a request. The [Mandarin voices page](https://paxalabs.com/chinese-voices) plays both voices on one welcome line. The [Chinese listening page](https://paxalabs.com/chinese-learning) reads four lesson lines with their pinyin, and the [languages page](https://paxalabs.com/languages) lists every page by language. # How to choose a Thai speech to text engine Seven properties to test on your own Thai audio before an accuracy figure enters the conversation, from word spans to what a failure costs. Published 2026-09-10. Paxa Labs. Most speech-to-text comparisons open with an accuracy figure and close there. For Thai that is a weak place to start. The figure depends on a tokenizer the language does not supply, and the properties that decide whether a transcript is usable are mostly about something else. An engine can hear a Thai sentence correctly and still hand back something no product can build on. This post is a test plan. Record thirty minutes of the audio you actually have, run it through each candidate, and check seven things. Every one of them can be answered from a single response. No engine is named here, ours included, and no figure is quoted. ## 1. Word spans on whole words Ask for word timestamps and look at what came back. Some engines time Thai one character at a time, because the writing system offers no other boundary. Character spans are technically timestamps and practically unusable: a subtitle cue cut on them breaks mid-word, a read-along player highlights a consonant, and a search result opens on a fragment. the same phrase, timed two ways (illustrative seconds): ``` ประชุมวันพฤหัส per word ประชุม 0.42-0.86 วัน 0.86-1.02 พฤหัส 1.02-1.48 per character ป 0.42-0.47 ร 0.47-0.51 ะ 0.51-0.55 ช 0.55-0.61 ุ 0.61-0.64 ม 0.64-0.86 ``` The engine that returns the first shape has segmented the sentence. The engine that returns the second has passed the problem to you. Solving it afterwards means running a segmenter over the transcript and mapping its output back onto character times. ## 2. A stated number convention A speaker says a figure aloud. A transcript can write it the way it was said or the way a document prints it, and the two are different strings. A search index wants one, a subtitle file wants the other, and a claims system wants digits it can parse. Check whether the engine offers the choice, or whether it picks for you and leaves the conversion as your problem. Test it with a recording that carries a price, a date, a phone number, and a percentage in the same minute. That is one ordinary Thai sentence in a call centre, and it exercises every rule at once. ## 3. Thai that turns English Thai speakers name products, technical terms, and brands in English mid-sentence without pausing. Send a recording that does this and read the line back. Three failures are worth looking for. The English word can arrive transliterated into Thai script, the Thai around it can be dropped, and the word spans can lose their seconds across the switch. ## 4. Speaker turns, if you need them Two people on a call, or four in a meeting. Check that the turns are numbered consistently, that a speaker who returns after a gap keeps their number, and what happens to the word spans when both speakers and timestamps are on. Check the length ceiling too. Marking who spoke when reads a recording whole, and that work usually carries a shorter limit than plain transcription. ## 5. Your own names Every product has words no general model has heard: part numbers, place names, a brand spelled a particular way. Check whether the engine takes a term list, how many terms it takes, and whether the spelling holds across separate requests. A list of 50 terms covers most working vocabularies. A list that silently truncates covers none of them predictably. ## 6. The ceilings, before you build Length per request, bytes per file, and the containers accepted. These decide the shape of your pipeline more than accuracy does. An engine that takes an hour in one call and one that takes ten minutes are two different integrations, and finding that out after the code is written is expensive. Check also whether the format is read from the bytes or declared by you. Declared formats mean every upload path in your product has to sniff and label the file correctly before it can send it. ## 7. What a failure costs Send a corrupt file, a silent file, and a file past the length ceiling. A refusal before any charge is the behaviour you want, with a code that says which of the three happened. Then check what a failure after the charge does. An engine that bills for a request it could not complete turns every bad upload in your product into a small loss. ## Then measure accuracy With the seven answered, score the candidates that are left. Do it over characters, on your own references, with the normalization rules written down. The accuracy post covers how, and the short version is that a figure nobody can reproduce is not a figure. ## Where we stand Paxa STT Lite answers the seven this way. Word spans cover whole Thai words. A spoken or a written convention comes on request. Thai and English share one line. Speaker turns are available on shorter recordings, and a term list is accepted. Recordings run to 60 minutes with the container read from the bytes, and a request that fails is not charged. No accuracy figure is published until the method is published beside it. ## Next steps - Read the [speech-to-text guide](https://paxalabs.com/docs/speech-to-text) for the request fields the seven checks above use. - Read [why word error rate misleads on Thai transcripts](https://paxalabs.com/guides/measuring-thai-transcription-accuracy) before scoring anything. - Read [what ASR means for Thai](https://paxalabs.com/asr) for the properties of the language behind these checks. - Open the [transcribe tool](https://paxalabs.com/app/transcribe) and run your own thirty minutes. # Transcribing Thai that switches into English Thai speakers name products and actions in English without leaving the sentence. What that does to a transcript, its word timings, and a search index. Published 2026-09-10. Paxa Labs. Listen to any Thai office call and count the English. A support agent says cancel and order. An engineer says deploy and staging. A doctor, a lawyer, and a mechanic each keep a working vocabulary of English words inside otherwise ordinary Thai sentences. Code-switching between Thai and English is documented in Thai public speech, and it is the norm in the recordings a Thai product actually collects. This is a transcription problem before it is a language problem. The sentence has one meaning and two scripts, and an engine has to decide what to write down. This post is about that decision, and about the three shapes a transcript can take. ## Three shapes, one sentence Take one line a support agent might say. There are three ways it comes back. one line, three transcripts: ``` spoken ลูกค้า cancel order ไปแล้วตั้งแต่เมื่อวาน romanised luk kha cancel order pai laeo tang tae muea wan phonetic ลูกค้า แคนเซิล ออร์เดอร์ ไปแล้วตั้งแต่เมื่อวาน as spoken ลูกค้า cancel order ไปแล้วตั้งแต่เมื่อวาน ``` The romanised shape loses the Thai. The phonetic shape spells the English words in Thai script, which is what somebody typing quickly might do and what a model trained mostly on Thai will default to. Both are readable by a person and useless to a search index: a customer searching for cancel finds nothing, because the transcript spells it แคนเซิล. The third shape keeps each word in the script it was spoken in. It is the only one where a search for either language finds the line, and the only one where the English terms match the strings already in your database. ## What it does to word timings Word spans have a harder job across a switch. Thai carries no spaces. The engine has to find the boundary between the last Thai word and the first English one from the sound alone, with no orthographic hint on either side. An engine that segments Thai well and then fumbles the handover produces spans that drift by a word around every switch. Check it on a recording of your own. Ask for word timestamps, find a switch, and play the two seconds around it against the spans. A subtitle cut from drifting spans shows the English word one cue early, which is the sort of error a viewer notices immediately. ## Pinning your own terms Product names are the hard case, because they are English words the model has no reason to expect. A term list solves it. Send the names your recordings actually contain, up to 50 terms. The spelling then holds across every request, however clearly each speaker said it. Keep the list to names that are genuinely ambiguous by ear. A list of common words does nothing. A list that runs past the ceiling is refused outright, with no quiet trimming, which is the behaviour you want when a database generates the list. ## The convention still applies Numbers inside a switched sentence follow the convention you asked for, in whichever language they were said. An order number read out in English digits and a price said in Thai land in the same transcript under one rule, which is what makes the line parseable afterwards. ## What the model does Paxa STT Lite writes each word in the script it was spoken in, and times whole words on both sides of a switch. It publishes no accuracy figure until the evaluation method is published beside it. The way to settle this for your own recordings is to run a few of them. ## Next steps - Read the [speech-to-text guide](https://paxalabs.com/docs/speech-to-text) for the vocabulary and convention fields. - Read the [transcription API reference](https://paxalabs.com/docs/api/stt) for the request shape. - Read [what ASR means for Thai](https://paxalabs.com/asr) for the three properties of the language behind this one. - The source for the pattern is [code-switching between Thai and English in young Thai politicians' speeches](https://so02.tci-thaijo.org/index.php/hasss/article/view/260788), HASSS 23(2), 2023. # How a Thai transcript writes numbers, spoken or written Thai says every number in words. A transcript can keep the words or turn them into digits, and each reader needs a different one. Which convention to ask for, and when. Published 2026-09-09. Paxa Labs. Thai speech carries numbers as words. A price is said as สองพันสามร้อยห้าสิบบาท, a date as สิบห้ามกราคม, and a time as เก้าโมงครึ่ง. A person writing the same line on a page turns each into digits without thinking about it. A transcription model has to be told which of the two it is writing, because both are correct and each serves a different reader. Paxa STT Lite calls the two forms conventions. The spoken convention writes what was said. The written convention writes what a page would show. This post shows the difference on five lines, then says which readers need which, and how to ask for each one. ## The same five lines, twice | Spoken | Written | | --- | --- | | ยอดรวม สองพันสามร้อยห้าสิบบาท | ยอดรวม 2,350 บาท | | นัดวันที่ สิบห้า มกราคม | นัดวันที่ 15 มกราคม | | ลดราคา ยี่สิบเปอร์เซ็นต์ | ลดราคา 20% | | ประชุมเวลา เก้าโมงครึ่ง | ประชุมเวลา 9:30 | | เด็ก ๆ ไปกันหมดแล้ว | เด็ก ๆ ไปกันหมดแล้ว | The first four rows change. Amounts become grouped digits with the unit kept as a word. A date keeps the month as a word and the day as a digit. A percentage becomes the sign. A clock time becomes hours and minutes with a colon. The fifth row is the same in both, and the reason is worth a paragraph. Thai repeats a word to mark plurality or emphasis, and the page writes the repetition with one mark, ๆ. Speech has no mark. It says the word twice. The written convention keeps the mark where a page would print it. The spoken convention keeps the word as said, and here the two forms happen to agree. ## Who needs which A subtitle needs the spoken form. The viewer hears the words and reads the words, and a digit on screen while the speaker says four syllables reads as a mismatch. A verbatim record for a newsroom or a court needs the spoken form for the same reason. The transcript is evidence of what was said. A language model reading the transcript usually wants the spoken form too. The model reads Thai number words as fluently as digits, and the spoken form keeps the sentence as one Thai sentence. A prompt that asks the model to extract the amount gets the same answer either way. A record that will be searched, filed, or matched by a rule needs the written form. A collections rule matches on 3,500. A meeting note that lists a deadline wants 15 มกราคม. A dictated letter wants to read as a letter. Anything that ends up in a table or a form wants digits. The two forms come from the same recording and the same request. Ask for both when the transcript has two readers. A call transcript can be filed under the written form and shown to a QA reviewer under the spoken one. ## Asking for each one The spoken convention is the default. Send `convention` as `"written"` for the other. Word timestamps are the same under both, because the timing belongs to the audio and the convention belongs to the text. curl: ```bash AUDIO=$(base64 < meeting.m4a | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"convention\": \"written\"}" ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน 1,250 บาทครบกำหนดวันที่ 15 สิงหาคมนี้ชำระผ่านแอปได้ตลอด 24 ชั่วโมงค่ะ", "usage": { "seconds": 11.952, "credits": 1.66 } } ``` One more detail matters for a written transcript. A figure that reaches digits is read from the words the model heard, and Thai says large numbers in a fixed order of thousands, hundreds, tens, and units. A number said in an unusual order stays as words. Check the written form against the audio at the second the figure was said, which word timestamps give you for nothing. ## Next steps - Read the [transcription guide](https://paxalabs.com/docs/speech-to-text#conventions) for the two conventions beside the model's own reading of one recording. - Read the [reference](https://paxalabs.com/docs/api/stt) for the `convention` field and the response shape. - Read the [subtitles post](https://paxalabs.com/guides/thai-subtitles-from-word-timestamps) for building cues from word timestamps under the spoken convention. - Open the [transcribe tool](https://paxalabs.com/app/transcribe) and run one recording under both conventions. The two transcripts sit side by side with the differing words marked. # Why word error rate misleads on Thai transcripts Word error rate needs word boundaries, and Thai writes none. How to score a Thai transcript so two models can be compared, what to normalize first, and how to build a test set from your own audio. Published 2026-09-09. Paxa Labs. Every speech-to-text vendor publishes a word error rate, and for Thai the number means less than it looks. Word error rate counts substituted, deleted, and inserted words between a reference transcript and the model's output, divided by the words in the reference. The count needs a word boundary. English puts a space at each one. Thai puts a space at the end of a phrase and nowhere else, and where one word ends inside a phrase is a decision a tokenizer makes. This post is about scoring a Thai transcript in a way two models can be compared on. It covers what the tokenizer does to the number, which metric to use instead, what to normalize before counting, and how to build a test set from the audio you already have. It gives no figure for any model, including ours. A figure without the method beside it is the problem the post is about. ## What the tokenizer does to the number Take one reference line and one hypothesis that differs by a single syllable. Score it under two common Thai tokenizers. One treats งบประมาณ as a word, the other splits it into งบ and ประมาณ. The same mistake produces a different word error rate under each, and neither is wrong. one error, three scores: ``` reference วันนี้ประชุมเรื่องงบประมาณปีหน้า hypothesis วันนี้ประชุมเรื่องงบประมานปีหน้า tokenizer A วันนี้ | ประชุม | เรื่อง | งบประมาณ | ปีหน้า 5 words tokenizer B วัน | นี้ | ประชุม | เรื่อง | งบ | ประมาณ | ปี | หน้า 8 words one wrong syllable in งบประมาณ WER under A 1 / 5 = 20% WER under B 1 / 8 = 12.5% CER 1 / 32 = 3.1% ``` A published word error rate for Thai therefore carries a hidden variable. Two vendors scored with two tokenizers cannot be compared, and a single vendor's number cannot be reproduced without the tokenizer named. A tokenizer that splits more finely lowers the number for everyone. ## Character error rate Character error rate counts the same three operations over characters. Thai characters have boundaries. The count is the same on every machine and needs no tokenizer. It is the metric most Thai speech research reports, and it is the one to ask a vendor for when a word error rate is quoted. A character error rate is smaller than the word figure for the same transcript, because an error touches one character in a word of several. The scale is different and the comparisons still hold, as long as both models are scored the same way on the same references. Character error rate has one blind spot. A tone mark or an upper vowel written in the wrong order is a character error even when the rendered word looks right. Normalize the Unicode order of both texts before counting, and the blind spot closes. ## Normalize before you count A transcript and a reference disagree in ways that are conventions, and a score that counts them measures the convention. Decide each one before scoring and apply the same rule to both sides. - Numbers. A reference written with digits and a hypothesis written with number words differ by every character of the number. Choose one form. The transcription API writes either on request, and the spoken form matches a reference typed from the audio. - Spaces. Thai spacing between phrases is inconsistent between two typists. Remove every space before counting, or count spaces separately as their own figure. - The repetition mark. ๆ and the word written twice are the same speech. Expand the mark on both sides. - Punctuation and case in English words. A Thai sentence that switches into English carries English conventions with it. Lowercase and strip punctuation on both sides. - Filler words. A verbatim reference keeps เอ่อ and a clean one drops it. Score verbatim output against a verbatim reference. A clean style is a readability choice and scores worse against a verbatim reference by construction. ## Build the test set from your own audio A vendor's test set sounds like the vendor's data. Yours sounds like your calls, your meetings, or your lectures, with your product names and your speakers. Fifty recordings of two to five minutes each, chosen to cover the speakers, rooms, and topics you have, is a test set a model can be judged on. Have the reference typed by a person listening to the audio, under a written rule for each item in the list above. Have a second person check a sample. A reference with a five percent error rate of its own cannot resolve a difference of two points between two models. Keep the references and the audio together and score every candidate model on the whole set with one script. Report the character error rate, the normalization rules, and the size of the set. A reader with all three can reproduce the number, and a number a reader can reproduce is one they can trust. ## What we publish Paxa STT Lite publishes no accuracy figure of its own. The evaluation method comes first, and a figure follows only beside the set and the rules that produced it. Until then, the recording on the product page and the transcribe tool are the way to judge the model on audio you choose. ## Next steps - Read the [transcription guide](https://paxalabs.com/docs/speech-to-text) for the conventions and the style option that the normalization rules above depend on. - Read the [numbers post](https://paxalabs.com/guides/thai-transcript-numbers-spoken-or-written) for how the spoken and written conventions differ, line by line. - Open the [transcribe tool](https://paxalabs.com/app/transcribe) and run a few of your own recordings before writing a single reference. # Speaker turns in a Thai call transcript A call transcript with two voices in one column is a puzzle. How diarization numbers the speakers, how a pipeline turns the numbers into roles, and what the numbers never claim. Published 2026-09-09. Paxa Labs. A recorded call has two voices and one audio track. A transcript of it in one column reads as a monologue, and every question a QA reviewer asks starts with who said that. Did the agent give the required notice, or did the customer read it back? Who mentioned the refund first? Diarization answers the first question, and the pipeline built on it answers the rest. This post covers what Paxa STT Lite returns when diarization is on, how to map its speaker numbers to the roles on a call, and where the numbers stop. It ends with the shape of a QA check that runs on every call, with no sampling. ## What comes back Send the recording with `diarization` set to `true`. The response carries the transcript as before, and a `segments` array beside it. Each segment is one turn with a speaker number, its text, and its start and end in seconds. Speakers are numbered from 0 in order of first appearance, and the numbers hold for the whole recording. curl: ```bash AUDIO=$(base64 < meeting.m4a | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"diarization\": true}" ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "segments": [ { "speaker": 0, "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "start": 0.12, "end": 11.66 } ], "usage": { "seconds": 11.952, "credits": 1.66 } } ``` Ask for word timestamps in the same request and every word carries its speaker number too. The turns give you the shape of the conversation, and the words give you the second inside a turn where a phrase was said. four turns of a two-party call: ``` speaker 0 0.0s สวัสดีค่ะ ฝ่ายบริการลูกค้า ยินดีให้บริการค่ะ speaker 1 3.2s สวัสดีครับ โทรมาเรื่องบิลเดือนนี้ครับ speaker 0 6.1s ขอทราบเลขที่บัญชีสี่ตัวท้ายด้วยค่ะ speaker 1 8.9s สี่ห้าหกเจ็ดครับ ``` ## From numbers to roles The model numbers voices. It attaches no name, no role, and no identity. On a two-party call the mapping from number to role is yours, and three rules cover most contact centres. - The first voice is the agent. An inbound call opens with the greeting the agent is scripted to say. Speaker 0 is the agent whenever the first turn matches the greeting. - The greeting names the role. When the first turn fails to match, search the first three turns for the scripted phrase and take that speaker as the agent. - Two channels make it certain. A telephony system that records the agent and the customer on separate channels can send each channel as its own file. The role is then the file, and diarization within each file catches a transfer or a supervisor joining. A call with a third voice, a supervisor or a transfer, arrives as speaker 2. A rule that expects two speakers should treat a third as a flag for review, since the conversation changed shape. ## A QA check on every call With roles assigned, a script check is a set of string matches on the agent's turns. The greeting, the identity check, the hold notice, and the closing line each become a phrase the agent's turns must contain, with the second it was said. A call that misses one is the call a reviewer listens to. Keyword alerts run on the customer's turns. The words for refund, cancel, and complaint, and the names of competitors, raise a call within minutes of it ending. A vocabulary of up to 50 product and plan names keeps their spelling the same across thousands of calls, which is what a string match needs. Talk ratio is the third figure the turns give for free. Sum the seconds of each speaker's turns. An agent who speaks for eighty percent of a complaint call is a coaching conversation, and the figure comes from the segments array with no listening at all. ## Where the numbers stop A diarized recording is read whole before the response returns, and a long call takes longer than a short one. Plan for the response to take tens of seconds on a long recording and send calls in parallel. The concurrency your plan allows is the number of calls in flight at once. That one pass is also where the length stops. A diarized request takes a call under 9 minutes, well below the 60 minutes a plain transcript accepts, and a longer one answers [400 diarization_audio_too_long](https://paxalabs.com/docs/errors#diarization_audio_too_long) before any charge. Split it at a silence and send the parts in parallel. Speaker numbers restart in each part. Map roles per part with the same rules. Diarization changes no charge. The recording bills by its length, and the turns ride along. ## Next steps - Read the [speakers section of the guide](https://paxalabs.com/docs/speech-to-text#speakers) for the response shape beside the model's own turns on one recording. - Read the [reference](https://paxalabs.com/docs/api/stt) for the `segments` and `words` fields. - Read the [call recordings use case](https://paxalabs.com/use-cases/call-recordings) for a QA pipeline as a wire log and what a month of calls costs. - Open the [transcribe tool](https://paxalabs.com/app/transcribe), turn on Speakers, and run one call to see the turns laid out. # Thai subtitles from word timestamps A Thai sentence has no spaces to cut a subtitle at. How word timestamps give a cue its cut points, the rules a Thai cue file needs, and the convention to ask for. Published 2026-09-09. Paxa Labs. A subtitle is a transcript cut into pieces that fit on a screen for the seconds they are spoken. In English the cutting is easy. A cue ends at a space, and a tool that knows nothing about the language produces readable lines. Thai writes a whole phrase as one run of characters. A tool that cuts at a character count cuts through a word, and the viewer reads half a word on one line and the other half two seconds later. This post shows how to build Thai cues from the word timestamps Paxa STT Lite returns. Each span is a whole Thai word with its start and end, which is exactly the set of places a cue may be cut. The rules for turning spans into cues fit on one page, and the result is a subtitle file any player reads. ## What a span is Ask for `timestamps` set to `"word"` and the response carries a `words` array beside the transcript. Each entry is one word with its start and end in seconds. For Thai the words are segmented on the way out. A span covers a whole word and never a character. The spans join back into the transcript with nothing lost. curl: ```bash AUDIO=$(base64 < meeting.m4a | tr -d '\n') curl -X POST https://api.paxalabs.com/v1/stt \ --max-time 300 \ -H "Authorization: Bearer $PAXA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"timestamps\": \"word\"}" ``` Response: ```json { "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ", "words": [ { "text": "สวัสดี", "start": 0.12, "end": 0.44 }, { "text": "ค่ะ", "start": 0.52, "end": 0.66 }, { "text": "คุณ", "start": 1.08, "end": 1.24 }, { "text": "สมชาย", "start": 1.32, "end": 1.88 }, { "text": "ยอด", "start": 2.36, "end": 2.56 }, { "text": "ชำระ", "start": 2.6, "end": 2.879 }, { "text": "ของ", "start": 3, "end": 3.14 }, { "text": "ท่าน", "start": 3.2, "end": 3.359 }, { "text": "จำนวน", "start": 3.48, "end": 3.879 }, { "text": "พันสองร้อยห้าสิบ", "start": 4.24, "end": 5.4 }, { "text": "บาท", "start": 5.56, "end": 5.759 }, { "text": "ครบ", "start": 6.32, "end": 6.52 }, { "text": "กำหนด", "start": 6.56, "end": 6.84 }, { "text": "วัน", "start": 6.92, "end": 7.039 }, { "text": "ที่", "start": 7.04, "end": 7.119 }, { "text": "สิบห้า", "start": 7.24, "end": 7.48 }, { "text": "สิงหาคม", "start": 7.68, "end": 8.26 }, { "text": "นี้", "start": 8.36, "end": 8.5 }, { "text": "ชำระ", "start": 9.08, "end": 9.36 }, { "text": "ผ่าน", "start": 9.48, "end": 9.68 }, { "text": "แอป", "start": 9.76, "end": 9.92 }, { "text": "ได้", "start": 10, "end": 10.14 }, { "text": "ตลอด", "start": 10.2, "end": 10.46 }, { "text": "ยี่สิบสี่", "start": 10.56, "end": 10.96 }, { "text": "ชั่วโมง", "start": 11.08, "end": 11.4 }, { "text": "ค่ะ", "start": 11.52, "end": 11.66 } ], "usage": { "seconds": 11.952, "credits": 1.66 } } ``` ## Cutting spans into cues Walk the spans in order and collect them into a cue until one of three limits is reached. The cue then ends at the last span's end, and the next cue starts at the next span's start. - Length. A Thai line reads comfortably at around thirty-five characters, and a cue holds at most two lines. Count the characters of the collected words and cut before the word that would push past the limit. - Duration. A cue on screen for longer than about six seconds reads as stuck. Cut when the next word would push the cue past the limit, even when the line has room. - Pause. A gap between one span's end and the next span's start longer than about half a second is a breath or a full stop. Cut there, and the cue ends when the speaker does. A cue that ends at a pause reads better than one that ends at a count. A small overrun of the length limit to reach a pause is worth allowing. Where a cue has two lines, break the line at the widest gap between spans inside the cue, which is usually a phrase boundary. one sentence, two cues: ``` words วันนี้ 0.00 0.38 เรา 0.38 0.55 จะ 0.55 0.70 คุย 0.70 0.98 เรื่อง 0.98 1.30 งบประมาณ 1.30 1.92 ปีหน้า 1.92 2.40 กัน 2.40 2.61 นะครับ 2.61 3.05 cues 1 00:00:00,000 --> 00:00:01,300 วันนี้เราจะคุยเรื่อง 2 00:00:01,300 --> 00:00:03,050 งบประมาณปีหน้ากันนะครับ ``` ## The convention to ask for Subtitles want the spoken convention, which is the default. The viewer hears สองพันสามร้อยห้าสิบบาท and reads the same words. A digit on screen while the speaker says seven syllables reads as a mismatch, and a cue built from the written form also loses the span boundaries inside a number, since the digits arrive as one token. Keep the verbatim style for the same reason. A viewer who hears a false start and reads a cleaned sentence notices. The clean style is for a transcript read on its own, away from the audio. ## Writing the file SRT numbers each cue, writes the start and end as hours, minutes, seconds, and milliseconds with a comma before the milliseconds, and follows with the text. WebVTT is the same with a period before the milliseconds and a header line. Both are plain text, and both carry Thai as UTF-8 with no further setting. Write the seconds from the spans with three decimals and the file is done. For a translated track, send the cues' text to the translation API one cue per segment with the ids echoed back. The timings stay with the ids. A Thai video gets an English track with the same cut points, and the two tracks stay aligned. ## Next steps - Read the [transcription guide](https://paxalabs.com/docs/speech-to-text#timestamps) for the word timestamps beside the model's own spans on one recording. - Read the [timestamps page](https://paxalabs.com/docs/timestamps) for the span shape shared with speech synthesis, where the same rules build read-along captions. - Read the [numbers post](https://paxalabs.com/guides/thai-transcript-numbers-spoken-or-written) for why a subtitle keeps the spoken form. - Read the [captions use case](https://paxalabs.com/use-cases/captions) for a caption pipeline end to end and what a season of video costs. # How to judge a Thai AI voice in ten sentences A ten-line listening test for any Thai text to speech voice, built on the sounds Thai carries and English does not, with a scoring sheet and the script to run. Published 2026-09-08, updated 2026-10-01. Paxa Labs. ข่าว, ขาว, and ข้าว are spelled with the same three letters and differ in tone alone. One is news, one is white, one is rice. A voice that gets one contour wrong turns a sentence about the morning news into a sentence about rice, and the listener hears the error before the sentence ends. Most demo scripts never put those three words in one line. This guide is a listening test. It names the five things a Thai syllable carries that an English one does not, gives a ten-line script that checks each of them, and explains how to score the result. The script works on any voice. The Paxa TTS Flash voices are on the [voices page](https://paxalabs.com/voices), and the test is the same whichever voice is in the player. ## Five things a Thai syllable carries Thai packs more into a syllable than English does. Each of the five properties below changes the meaning of a word on its own. Each is also a place where a voice can fail while sounding fluent to a listener who does not speak Thai. - Tone. Thai has five tones, and the tone is part of the word. ข่าว carries a low tone, ขาว a rising one, ข้าว a falling one. A wrong tone is a wrong word, and a flat reading that levels every tone is a reading with no words in it. - Vowel length. A short vowel and a long vowel are different vowels. ฝัน is a dream and ฝาน is to slice, and the only difference is how long the vowel lasts. A voice that clips long vowels merges pairs like these. - Final consonants. A syllable ending in ก, ด, or บ closes on a stop the speaker does not release. มาก ends with the mouth closed. A voice trained mostly on English tends to release the final, which adds a puff of air Thai listeners hear as foreign. - ร, ล, and clusters. Standard Thai keeps ร and ล apart and keeps clusters such as คร, ปล, and คล whole. Casual Bangkok speech drops the ร in ครับ and says คับ. A voice has to pick one register and hold it for the whole line, because a reading that alternates sounds like two speakers. - Borrowed words. A word borrowed from English has a fixed Thai reading, with its own tones, that the spelling only partly marks. คอมพิวเตอร์ ends on a falling tone nothing on the page records. A voice can read the Thai spelling with English sounds, or read it letter by letter, and both are wrong. Three more things sit above the syllable. Written Thai has several words whose letters allow two readings, and only the sentence decides which. Numbers, dates, and times read by rules a voice has to know. A Thai question is marked by a word at the end, and the pitch of the whole sentence does the opposite of what an English question does. ## The ten-line script Each line below tests one property, and the table after it says what to listen for. Every line is under 50 characters, which is the length the box on [the text to speech page](https://paxalabs.com/tts) speaks without an account. Copy them in order. the ten test lines: ``` 1. ข่าวเช้านี้บอกว่าข้าวขาวราคาขึ้น 2. เขาฝันว่าได้ฝานมะม่วงให้แม่ 3. หนังสือเล่มนี้หนักมาก อ่านจบแล้วครับ 4. ครูให้เราอ่านเรื่องปลาในคลองครับ 5. ส่งอีเมลจากคอมพิวเตอร์ที่ออฟฟิศ 6. เด็กตากลมนั่งตากลมริมทะเล 7. นัดวันที่ 15 กรกฎาคม 2569 เวลา 14:30 น. 8. โทร 02 123 4567 ต่อ 21 9. ทานข้าวหรือยังคะ ถ้ายังเดี๋ยวสั่งให้นะคะ 10. ทีม Marketing จะส่ง report ให้ CEO ก่อนเที่ยง ``` | Line | Checks | Listen for | | --- | --- | --- | | 1 | Tone | ข่าว low, ขาว rising, ข้าว falling, three distinct contours in one sentence | | 2 | Vowel length | ฝัน short and ฝาน long, and the long one audibly longer | | 3 | Final consonants | หนัก, มาก, จบ, and ครับ each close on an unreleased stop, with no puff of air after the final | | 4 | ร, ล, and clusters | ครู, เรื่อง, ปลา, and คลอง keep their ร and their clusters, or the line drops them together in one casual register | | 5 | Borrowed words | อีเมล, คอมพิวเตอร์, and ออฟฟิศ read as Thai words with Thai tones, with the falling tone on the last syllable of คอมพิวเตอร์ | | 6 | Two readings of one string | The first ตากลม reads as ตา-กลม, round eyes, and the second as ตาก-ลม, sitting in the breeze | | 7 | Date and time | 15 reads as สิบห้า, the year reads in full, and 14:30 น. reads as a clock time in either the formal or the spoken form | | 8 | Phone number and 21 | The digits read one by one, with สอง or the telephone form โท for 2, and ต่อ 21 reads as ยี่สิบเอ็ด | | 9 | Particles and a question | คะ on a high tone and นะคะ on the right two tones, with the question carried by the particle and no rising pitch across the sentence | | 10 | English inside Thai | Marketing and report read as English words, CEO reads letter by letter, and the Thai around them stays Thai | Line 6 is the classic test for reading in context, and it has a cousin in เพลา, which reads as one syllable when it means an axle and as two when it means a time of day. Line 9 matters most for a female voice, because ค่ะ on a falling tone and คะ on a high tone are one letter apart in spelling and a whole register apart in sound. The [script guide](https://paxalabs.com/guides/writing-for-thai-text-to-speech) covers how to write numbers and English words when the script is yours to change. ## How to listen A fluent reading hides errors. The ear forgives a wrong tone when the sentence carries on at a natural pace, and a listener who is judging the overall feel will pass a voice that fails three lines. Score each line on its own check and nothing else. - Play each line twice. Listen to the whole line the first time, and listen for the one property the table names the second time. - Score each line 0, 1, or 2. Two means the check passes cleanly. One means a Thai listener would notice and understand. Zero means the word changed or the reading is wrong. - Read all ten lines in one voice before switching. Comparing line 3 in one voice against line 3 in another is the point, and mixing voices inside a run confuses the memory of what each one did. - Ask a native speaker to score lines 1, 2, 6, and 9 blind, without the table. Those four are where a non-native ear passes what a native ear fails. - Keep the audio. A file per line per voice is a small folder, and a decision made on it can be revisited when the script or the voice changes. > A line that fails on every voice you test is a script problem. Rewrite it in the form the script guide gives, and run it again before you rule a voice out. ## Where a natural reading breathes Thai writing puts a space between phrases and no mark between sentences. A speaker breathes at the space, holds a shorter beat at a comma, and runs straight through a phrase with no space in it. Lines 3 and 9 each carry one space, and the reading should pause there and nowhere else. A reading that pauses inside a phrase has cut a word in half or has guessed a boundary wrong. A reading that runs through the space has lost the phrase structure, and a long sentence read that way sounds like a machine even when every syllable is right. Score this under the line it happens in, and note it separately, because the fix is different from the fix for a wrong tone. Speed is the last thing to judge. Thai news reads fast and Thai bedtime stories read slow, and a voice cast for one job is at the wrong pace for the other. Judge the pace against the job the voice is for, and use `speed` on the [request](https://paxalabs.com/docs/api/text-to-speech) to move it after the sounds pass. ## Reading the score The maximum is 20 per voice. A voice at 18 or above reads Thai as a Thai speaker would on the lines that matter. The remaining points are usually a number or a borrowed word the script can fix. A voice between 14 and 17 is usable for a surface where the text is written for it, and the lines it lost tell you what the writer has to avoid. Below 14 the voice is the problem, and a better script will not carry it. One line overrides the total. A zero on line 1 means the voice does not hold tones, and every other line was passed by luck. Rule that voice out whatever the rest of the sheet says. ## Voices the script treats differently Line 10 flips for an English-first voice. The 6 English-first voices read a whole English sentence with no Thai accent and read Thai with a foreign one, and the Thai lines above score them low by design. Test those voices on an English line of the same shape, and score line 10 on whether the Thai around the English words stays intelligible. The [English voices page](https://paxalabs.com/english-voices) lists them and the jobs they are cast for. Line 10 flips the same way for the 2 Mandarin-first voices, which read a Mandarin sentence in their own language and Thai with a foreign accent. Test them on a Mandarin line of the same shape. The [Mandarin voices page](https://paxalabs.com/chinese-voices) lists them and the jobs they are cast for. The 4 regional voices read the same script in their own sound system. The regional varieties of Thai carry tone systems of their own, and a speaker of central Thai scoring line 1 on a regional voice is judging it against the wrong system. Have a speaker from that region score lines 1, 2, and 9, and read the [accents page](https://paxalabs.com/voices/accents) for what the register changes. ## Run the script - Paste the lines one at a time into the box on [the text to speech page](https://paxalabs.com/tts). It speaks 9 of the 50 Paxa TTS Flash v2 voices with no account, one line every few seconds. - Open the [speech tool](https://paxalabs.com/app/speech) for the full script in one run, any voice, and audio you can download. A new account starts with 100 free credits, which covers the whole ten-line script about 30 times. - Call [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech) with the same ten lines when the test is part of a build, and keep the files under version control beside the script. - Read each voice's page under [voices](https://paxalabs.com/voices) before you pick. The catalog role says what the voice was cast for, and a voice at the wrong pace for the job fails a test no line above can catch. ## Next steps - Read the [choosing guide](https://paxalabs.com/guides/choosing-a-thai-ai-voice) to pick the two or three voices worth testing for a job. - Read the [script guide](https://paxalabs.com/guides/writing-for-thai-text-to-speech) to fix the lines a voice lost on numbers, English, and particles. - Read the [voices reference](https://paxalabs.com/docs/api/voices) for the ids and the fields the catalog returns. # What machine translation into Thai gets wrong, and how to check it The five ways a Thai translation can be grammatical and still wrong, a review a team without a Thai reader can run, and where machine output is already enough. Published 2026-09-08. Paxa Labs. A Thai sentence can be grammatical, accurate, and wrong for the reader at the same time. Thai asks the writer to choose a level of politeness, a pronoun for the speaker and one for the reader, and a spelling for every borrowed word. English asks for none of those, and a translation engine that carries the meaning across still has to make each choice. The choices it makes are the errors a team without a Thai reader ships. This guide names the five failures that survive a grammar check and gives a review a non-reader can run on every batch. It ends with where machine output is already good enough to ship after that review. The examples use [Paxa Translate Lite](https://paxalabs.com/docs/translation) and its fields, and the checks apply to output from any engine. ## Five failures that survive a grammar check ### Register drift Every Thai sentence sits at one level of formality, and the level shows in the particle at the end, the pronouns, and the verbs. A product is translated in batches over months, and each batch arrives at the level the engine inferred from that batch's text. The support macros come back polite, the onboarding tour casual, the terms formal. Each batch is correct on its own, and the product speaks in three voices. one string, two batches: ``` source Your order has shipped. batch 1 ส่งของแล้วนะ batch 2 จัดส่งคำสั่งซื้อของคุณเรียบร้อยแล้วค่ะ ``` Both lines above are fine Thai. The first belongs in a chat app and the second in a bank, and a product that shows one on the order page and the other in the email has drifted. The [politeness post](https://paxalabs.com/guides/thai-politeness-in-translation) explains the three registers in depth. The fix for drift is one decision written down and sent with every request. ### The wrong pronoun English has one word for I and one for you. Thai has a dozen of each, and the pair a sentence uses states who is speaking to whom. A support macro written in the first person often comes back with ฉัน, the pronoun a person uses about themselves in casual speech. A company writes เรา, or its own name, and addresses the reader as คุณ. ท่าน sits a rank higher and belongs in a letter to a government office or a formal notice. a refund macro: ``` source I've refunded your order. You'll see it in 5 to 7 days. machine ฉันคืนเงินคำสั่งซื้อของคุณแล้ว คุณจะเห็นภายใน 5 ถึง 7 วัน better เราคืนเงินค่าสินค้าให้คุณเรียบร้อยแล้วค่ะ ยอดเงินจะเข้าภายใน 5 ถึง 7 วัน ``` The second line also changes the verb of receipt. A refund in Thai arrives, ยอดเงินจะเข้า, and the reader does not see it. That change is the kind a native reader makes in one pass and a checklist never catches, which is why the review below keeps a reader on the strings customers see most. ### Three spellings of one borrowed word Thai borrows English words constantly and spells many of them more than one way. The Office of the Royal Society publishes one spelling, newspapers and apps use another, and chat uses a third. An engine that has seen all three picks per sentence, and a product ends up with two spellings of email on one screen. | Word | Royal Society spelling | Common variants | | --- | --- | --- | | email | อีเมล | อีเมล์, เมล | | application | แอปพลิเคชัน | แอพพลิเคชั่น, แอพ | | digital | ดิจิทัล | ดิจิตอล | | internet | อินเทอร์เน็ต | อินเตอร์เน็ต | | website | เว็บไซต์ | เว็ปไซต์ | Pick one column per product and hold it. The Royal Society spelling is the safe default for anything formal, and the common variant reads as natural in a consumer app. Whichever it is, put the chosen spellings in the `glossary`, and the engine will use them. The [borrowed words section](https://paxalabs.com/docs/translation#borrowed-words) of the guide covers the `borrowed_words` field, which decides whether a word with no established Thai form is transliterated or kept in Latin script. ### Words that are correct and never said A calque translates each word and produces a phrase no Thai service uses. บำรุงรักษา is maintenance, and it is what a mechanic does to an engine. A website that goes down for maintenance ปิดปรับปรุง, closes to improve. Both phrases are in the dictionary, and only one has ever appeared on a Thai status page. a maintenance notice: ``` source Heads up: we're doing maintenance tonight. machine แจ้งเตือน: เราจะดำเนินการบำรุงรักษาในคืนนี้ better แจ้งเพื่อทราบ คืนนี้ระบบจะปิดปรับปรุงชั่วคราวค่ะ ``` Calques cluster in system messages, error text, and status pages, because those strings are short and give the engine no context. Sending the surface in `context`, a status page or a push notification, is what moves the engine from the dictionary phrase to the one in use. A glossary entry does the same for a phrase that recurs. The [instructions section](https://paxalabs.com/docs/translation#instructions) of the guide covers the fields. ### Formatting that changes at the border Several formatting rules change when the language does. Thai has no plural forms, and the CLDR plural rules give it one category. Written Thai ends a sentence with a space and no full stop. The baht sign is rare in Thai text and the word บาท follows the number. Dates print in the Buddhist Era, which is the Common Era year plus 543, and a receipt that prints 2026 has kept the source calendar. what changes: ``` ICU {count, plural, one {# message} other {# messages}} Thai {count, plural, other {# ข้อความ}} source Saved. Thai บันทึกแล้ว source ฿100 · 2026-07-15 Thai 100 บาท · 15 ก.ค. 2569 ``` An ICU message with one, few, and other branches collapses to other in Thai, and a file that keeps the English branches carries text no Thai user will see. A placeholder such as `{count}` has to survive with its braces intact, and a tag has to close where it opened. Send rich strings with `format` set to `"html"` or `"markdown"` and the engine preserves the structure. The [markup section](https://paxalabs.com/docs/translation#markup) of the guide describes what is preserved. ## A review a non-reader can run The engine runs the first pass itself. Every delivered segment is checked against what the request asked for, and a segment that fails a check carries a `review` member naming the reason. The [review section](https://paxalabs.com/docs/translation#review) lists the reasons. A flag never refuses a request, and the translation is delivered either way. Read the flags before anything else. - Count the placeholders and tags. Every `{name}`, `%s`, and tag in the source appears once in the output, and the `markup_changed` and `preserved_span_missing` reasons name the strings where one moved or vanished. - Compare lengths. Thai runs at a fairly steady ratio to English across a batch, and a string far outside that ratio dropped a clause or picked up one. Set `max_length` on strings bound to a button or a title, and read the `too_long` reason. - Search for the glossary. Each term's chosen Thai form appears wherever the source term did, and the `glossary_term_missing` reason names the segments where it did not. - Read the `untranslated` reason with care. A product name that came back unchanged is right. A sentence that came back unchanged is a failure the engine reported on itself. - Check the particles by eye. Every customer-facing statement ends on the same particle, ค่ะ or ครับ, across the batch, and no string ends on นะ in a surface that handles money. The particles are visible to anyone, whatever they read. - Check the numerals, the dates, and the currency. Arabic digits throughout, บาท after the amount with a space before it, and the year in the era the surface uses everywhere else. - Give a reader the delta. The ten strings customers see most, the top of every surface, and every string about money or terms get a native read on every batch. The rest gets a sampled read on the first batch and the flagged strings after that. ## Measure the register without reading it Three counts over a batch show its register to someone who reads none of it. Each is a one-line script over the output file, and each catches drift before a reader is asked for an hour. - Count the last word of every statement. A batch at one register ends most statements on one particle, ค่ะ or ครับ, and a batch that splits between ค่ะ, นะคะ, นะ, and no particle at all is three batches in one file. - Count the pronouns. ฉัน, ผม, ดิฉัน, เรา, คุณ, and ท่าน each have a place, and a customer-facing batch should contain เรา and คุณ and little else. A single ฉัน in a support batch is worth opening. - Count each spelling of the borrowed words in the glossary. The chosen spelling should be the only one present, and a variant that appears once is a string the glossary did not reach. three counts over one batch: ``` particle ค่ะ 412 นะคะ 9 นะ 3 none 61 pronoun เรา 388 คุณ 402 ฉัน 2 ท่าน 0 spelling อีเมล 37 อีเมล์ 1 เมล 0 ``` The second and third lines above each name a string to open. The first says the batch holds together, apart from the 61 statements with no particle, which are most likely labels and belong in a separate count. Run the same counts on the approved strings from the last batch, and a new batch is compared against a number. ## Brief before you edit A wrong register on one string is an edit. A wrong register on a batch is a brief that was never written. The fields below fix each decision once, and a batch sent with them comes back needing the reader's pass and little else. | Decision | Field | What to send | | --- | --- | --- | | Level of formality | `formality` | `formal` for money, legal text, errors, and email; `casual` only for an audience that speaks that way | | Speaker and reader | `instructions` | The particle the product speaks with and the pronoun pair, for example a company voice with ค่ะ, เรา for us, and คุณ for the reader | | Surface and audience | `context` | Where the string appears and who reads it. A status page and a contract translate differently | | Fixed terms and spellings | `glossary` | Every term with one Thai form, borrowed-word spellings included | | Strings that stay | `do_not_translate` | Product names, feature names, placeholders, and codes | | Accepted output | `examples` | Source and approved Thai pairs from the last batch, which carry the voice better than a description of it | | Space on screen | `max_length` | The width a control allows, per segment, on buttons, titles, and notifications | Some strings are cheaper to write than to brief. A headline, a tagline, and anything that turns on wordplay are a copywriter's job in Thai as in English. A batch of under ten such strings costs less to hand to a writer than to describe to an engine. Send the engine the volume and the writer the voice. ## Where machine output is enough, and where it is not | Surface | After the review above | Native read | | --- | --- | --- | | Support macros and system email | Ships | First batch in full, then the flagged strings | | Product descriptions at volume | Ships | A sample per batch | | Interface strings | Ships with `max_length` and ids | The top of every surface | | Help center and documentation | Ships | The pages with the most traffic | | Terms, policies, invoices | A draft | Every string, every batch | | Headlines, taglines, campaign copy | A draft | A rewrite by a writer | The line between the rows is the cost of a wrong sentence. A support macro with a stiff particle costs a raised eyebrow. A clause in the terms with the wrong verb costs a dispute. The review is the same on every row, and the reader's share of it grows with what a mistake costs. ## A workflow that holds across batches - Batch by surface. One request carries up to 200 segments with shared context, and a batch drawn from one surface gets one register. Send each string's key as its `id`, and the result comes back keyed to your file with no dependence on order. The [batching section](https://paxalabs.com/docs/translation#batching) covers the shape. - Keep one glossary per product, in version control, and send the whole of it with every batch. Only the terms that appear in the text are sent to the engine and billed, and the pruning is automatic. - Read the review flags first, the reader's delta second, and change the brief when the same flag repeats. A flag that appears on every batch is a missing glossary term or a missing instruction. - Feed accepted strings back as `examples` on the next batch. Ten approved pairs from the last batch hold the voice better than any paragraph of instruction. - Translate only what changed. Hash each source string, and send a batch of the strings whose hash moved. The register holds because the brief has not changed, and the reader's delta stays small. ## Next steps - Read the [translation guide](https://paxalabs.com/docs/translation) for every field, and the [reference](https://paxalabs.com/docs/api/translate) for the request and response shapes. - Read the [politeness post](https://paxalabs.com/guides/thai-politeness-in-translation) to choose the register before the first batch. - Read the [cost post](https://paxalabs.com/guides/thai-translation-cost) for what a batch costs against an agency or a freelancer. - Open the [translate tool](https://paxalabs.com/app/translate) with ten real strings from one surface and read the flags it returns. # Feeding Thai documents to an LLM: OCR comes first A PDF is a drawing of text, and a Thai one fails a language model twice. How to read the pages first, what to hand the model, and how to pay for the reading once. Published 2026-09-08. Paxa Labs. A PDF is a drawing of text. It records where each glyph sits on the page, and whether the characters behind those glyphs are recoverable depends on how the file was made. A language model reasons over text it receives, and for a Thai document the text it receives is usually wrong in one of two ways. A text layer, when there is one, carries Thai in an order no reader wrote. A scan carries no text at all, and a model asked to read the image is guessing at marks a few pixels tall. This guide is about the step between the file and the prompt. It covers what a model receives from a Thai PDF and why a reader built for Thai pages goes first. It then covers what shape to hand the model, how to handle tables and page boundaries, and how to pay for the reading once. The examples use [Paxa OCR Lite](https://paxalabs.com/docs/ocr) for the reading, and the shape of the pipeline holds for any reader that returns structure. ## What a model receives from a PDF Sending a PDF to a model API does one of two things on your behalf, and hides which. The service extracts the text layer and puts it in the prompt, or it renders the pages to images and puts those in the prompt. Both paths work well enough on an English contract. Both fail on a Thai one for reasons that are specific to the script. A Thai text layer is built from the glyphs the producing software placed. Thai fonts keep repositioned copies of tone marks and upper vowels as private-use glyphs, shifted to clear a tall consonant such as ป or ฝ, and a text layer that names those glyphs extracts as code points no text tool recognizes. Other producers emit a mark before the letter it sits on. The [extraction post](https://paxalabs.com/guides/thai-pdf-text-extraction) catalogs the failures. The line below is one printed line and two things a text layer makes of it. one line, two text layers: ``` printed ปุ๋ยอินทรีย์ ถุงละ ๒๕๐ บาท text layer A ปยอินทรีย ถงละ ๒๕๐ บาท text layer B ป๋ ุยอินทรีย ์ ถ ุงละ ๒๕๐ บาท ``` The first layer dropped every mark the font stored as a private glyph. The second kept the marks and stored two of them out of order. A model reading either receives a string that spells nothing. It is not reading nonsense, because it can often reconstruct the word from the letters around it, and that is worse. The model fills the gap with the most likely word, and the most likely word on an invoice is a plausible wrong number. ## Why a Thai scan needs a reader before the model The image path avoids the text layer and meets the script directly. A Thai line uses four vertical levels. A vowel can hang below the consonant, a vowel or ั can sit above it, and a tone mark sits above that. One column stacks up to three glyphs, and the marks that carry the meaning are the smallest things on the page. The four tone marks, ่ ้ ๊ ๋, and the upper vowels ิ ี ึ ื are told apart by a stroke or a dot a few pixels wide at a normal scan resolution. A general model resizes an image to a fixed budget of pixels before it reads it, and a full page of Thai at that budget leaves a tone mark with too little to go on. The model reads the consonants, which are large, and infers the marks, which are small. The result reads as fluent Thai and disagrees with the page on the words that differ by one mark. ข่าว and ข้าว are one such pair, and an amount column has a dozen more. A reader trained on Thai pages reads at the page's resolution and outputs characters once. Text is what a model is good at. Reading a page once and handing the model the text puts each system on the task it does well. It also makes the reading a stored artifact a person can open and check. ## Markdown as the interchange format Ask the reader for Markdown. [POST /v1/ocr](https://paxalabs.com/docs/api/ocr) returns each page as GitHub-flavored Markdown in reading order by default. Headings arrive as heading marks, lists as list items, and tables as pipe tables. The structure is carried in plain characters that survive a prompt unchanged, and a model has read more Markdown than any other structured text. The page below is one page of a quotation as the reader returns it. one page as Markdown: ``` # ใบเสนอราคา เลขที่ QT-2569-0142 วันที่ 15 กรกฎาคม 2569 | รายการ | จำนวน | ราคาต่อหน่วย | รวม | | --- | --- | --- | --- | | ปุ๋ยอินทรีย์ 25 กก. | 40 ถุง | 250 | 10,000 | | ค่าขนส่ง | 1 | 800 | 800 | ยืนราคา 30 วัน นับจากวันที่ในใบเสนอราคา ``` Everything the model needs is on the page and nothing else is. There is no geometry, no font name, no bounding box, and no confidence score to explain away in the prompt. The table is a table, and a question about the total finds the total in a cell under a column heading. Join pages with a blank line to rebuild the document, and the document is a prompt. Two things are worth adding before the join. Mark each page boundary with a comment such as ``, which a Markdown renderer hides and a model reads as a marker, and the model can cite the page in its answer. And keep the Thai digits as printed. A model reads ๒๕๐ correctly, and a citation that shows the reader's text should match the scan. The [search index post](https://paxalabs.com/guides/thai-pdf-to-search-index#normalize) covers the normalized copy a retrieval index needs beside the original. ## Tables, running headers, and page boundaries A table is one object, and it should reach the model whole. A pipe table split across two pages by the scan arrives as two tables with one header. The pipeline should join them when the second page opens on rows with no heading above them. A table wider than the prompt can bear is the one case where the Markdown shape fights the model, and the structured output below is the answer to it. Running headers and footers repeat on every page. A document id, a company name, a page number, and a confidentiality line arrive once per page and add nothing after the first. Detect lines that recur at the top or bottom of most pages and drop them, and keep the page number in the boundary marker where it does the work of a citation. A long document is read in parts. One request reads up to 50 pages and 10 MiB, and a 200-page manual is 4 requests. Number the pages continuously across the parts, because the page is what an answer cites, and a part that restarts at page 1 breaks every citation after the first part. ## Read once, and pay for the reading once A page costs 6.5 credits whatever it holds. The 200-page manual is 1,300 credits, which is $1.30, and that is the cost of every question ever asked of it if the reading is stored. A pipeline that reads the document inside each request pays that again per question. Key the cache on a hash of the file's bytes, and a document uploaded twice is read once. the read-once pipeline: ``` key = sha256(bytes) pages = cache.get(key) if pages is missing: pages = POST /v1/ocr { document: base64(bytes), model, output: "markdown" } cache.put(key, pages) text = join(pages, page => "\n" + page.markdown) answer = model(system, text, question) ``` Store the reader's output beside the source hash, and rebuild the prompt from stored text every time. The page count is read before the charge, and a document the reader cannot open is refused without one. A request that fails after the charge is refunded. Send an `Idempotency-Key` on the read and a retry after a timeout replays the stored result without a second charge. The [OCR guide](https://paxalabs.com/docs/ocr#billing) covers the billing rules. ## The prompt around the reading The reading is the document. The prompt around it sets what the model may do with it, and three rules matter more for a Thai document than for an English one. - Answer from the text alone, and say when the text does not contain the answer. A model that falls back on what it knows about Thai invoices in general will produce a plausible field the page never had. - Quote Thai verbatim. A model asked to summarize will normalize spellings and drop particles, and a quotation that differs from the page by one mark is a quotation a reviewer cannot find. - Keep the digits and the calendar as printed, and convert on a separate line. ๒๕๖๙, 2569, and 2026 name the same year in three notations, and a model that converts in place leaves the citation pointing at text that no longer matches the page. a system prompt for a read document: ``` You are answering questions about a Thai document. The document follows, one page per marker. Answer only from the document. When the answer is absent, say so. Quote Thai text exactly as written, and name the page marker it came from. Keep digits, amounts, and years as printed. Put any conversion on a separate line. ``` A photo of a document goes through the same path. An image counts as one page, the reader accepts PNG, JPEG, and WebP beside PDF, and a phone photo of a receipt costs 6.5 credits like any other page. Photograph the whole page, and the reader returns its text in the same Markdown a scan gets. ## Structured blocks for extraction Set `output` to `"structured"` and each page arrives as typed blocks in reading order. Headings carry a level, lists carry their items, and tables carry rows of cells. A pipeline that extracts fields can route each block by type. Tables go to code, which reads a cell by column name without a model. Paragraphs go to the model with the heading trail above them as context. Figures carry a caption and nothing else. The rule that follows is worth stating plainly. Leave every number that sits in a table block to the code. The code reads it exactly, and the model reads it probably. Ask the model for what the code cannot do, which is to say which of three paragraphs states the payment terms, or whether a clause obliges the buyer or the seller. Validate what comes back against a schema, and keep the block the answer came from as the citation. A wide table is the other reason to use blocks. Flatten each row into a line of column name and value pairs. A table that would have wrapped past recognition as pipes becomes a list, and the model reads it without losing which value belonged to which column. ## Where the model still earns its place The reader turns the page into text. The model does the rest. It summarizes a fifty-page regulation, answers a question across a folder of contracts, and classifies a document by its first page. It normalizes the four ways a Thai document writes a date into one field, and it drafts the reply to a letter. Every one of those tasks is a text task, and every one of them is only as good as the text it starts from. ## Next steps - Read the [OCR guide](https://paxalabs.com/docs/ocr) for the outputs and limits, and the [reference](https://paxalabs.com/docs/api/ocr) for the page and block fields. - Read the [search index post](https://paxalabs.com/guides/thai-pdf-to-search-index) for chunking the stored text when the folder outgrows one prompt. - Read the [extraction post](https://paxalabs.com/guides/thai-pdf-text-extraction) for what a Thai text layer gets wrong and how to check a reading. - Open the [documents tool](https://paxalabs.com/app/documents) to read one page and see the Markdown and the blocks it returns. # What Thai translation costs in 2026: agency, freelancer, API Thai translation rates from public listings, per page and per word, beside what the same letter, contract, and product catalogue cost as generated translation. Published 2026-09-07. Paxa Labs. Translating a page between English and Thai in 2026 costs from ฿250 on a freelance listing to ฿1,500 at a language institute for technical text. An agency quotes from ฿150 to ฿600 a page, or from ฿1 a word with a minimum order. Generated translation prices the same 300-word page at about 45 credits, which is $0.05. This guide lays the two side by side. The human figures come from public listings retrieved on 7 September 2026 and linked beside each number. The generated figures come from the published per-character rate, and they move when it moves. ## How human translation is priced Human translation is priced by the page, by the word, or by the document. A freelancer on a marketplace sells a package, which is a word or page ceiling, a turnaround, and a number of revision rounds. An agency prints a per-page range and marks every figure as a starting price that depends on the content. A certified translator prices by document type, with the certification and the legalization run as separate lines. The headline figure is rarely the whole invoice. Specialized content moves a page up a tier, and a rush turnaround has its own fee. A document bound for a government office needs a certification stamp and sometimes a trip to the consular office, each priced on its own. ## What a page is The page is the unit most Thai listings price in, and no two define it the same way. [an agency's rate page](https://www.bbktranslator.com/rate) defines it by typesetting, as an A4 sheet at Angsana New 16 with images and charts excluded. [an agency's rate page](https://www.edubranch.com/17721677/%E0%B8%AD%E0%B8%B1%E0%B8%95%E0%B8%A3%E0%B8%B2%E0%B8%84%E0%B9%88%E0%B8%B2%E0%B8%9A%E0%B8%A3%E0%B8%B4%E0%B8%81%E0%B8%B2%E0%B8%A3) counts 350 Thai words a page at the same font. [a certified translator's rate page](https://www.firstchoicetranslation.com/en/servicerateeng/) allows 250 to 300 words an A4. [a language institute's price page](https://www.sumaa.net/services/translation-services/) fixes 300 words whatever the font, and [a freelance listing](https://fastwork.co/user/lerluck/translation-50150270) on a marketplace counts 500. A page of 500 words at one price and a page of 250 at the same price are a factor of two apart. Ask what the page is before comparing two page rates, or ask for the per-word rate, which two of the agency pages below print beside the page rate. The comparison in this guide uses a 300-word page throughout. ## Thai translation rates from public listings The table holds every figure found on nine public pages on 7 September 2026. Each row is one page's own price, and a listing is the opening position in a negotiation. Read the spread as the range a buyer meets on the first day of looking. | Job | Unit | Figure | Source | | --- | --- | --- | --- | | English and Thai, any direction | Starting price | ฿250 | [a marketplace category](https://fastwork.co/translation/english) | | Thai and English, 500-word page, one day | Per page | ฿250 | [a freelance listing](https://fastwork.co/user/lerluck/translation-50150270) | | Package by turnaround, two revisions | Per package | ฿400 for 2,000 words in four to seven days, ฿480 for 1,000 in two to three, ฿800 for 700 in one | [a freelance listing](https://fastwork.co/user/chalisaph/translation-27590348) | | English and Thai, general documents | Per page, from | ฿150 to ฿600 | [an agency's rate page](https://www.bbktranslator.com/rate) | | Civil registry forms, certified | Per form | ฿150 to ฿400 | [an agency's rate page](https://www.bbktranslator.com/rate) | | English to Thai | Per word, from | ฿1 (minimum ฿300) | [an agency's rate page](https://www.edubranch.com/17721677/%E0%B8%AD%E0%B8%B1%E0%B8%95%E0%B8%A3%E0%B8%B2%E0%B8%84%E0%B9%88%E0%B8%B2%E0%B8%9A%E0%B8%A3%E0%B8%B4%E0%B8%81%E0%B8%B2%E0%B8%A3) | | Thai to English | Per word, from | 1.25 baht (minimum ฿350) | [an agency's rate page](https://www.edubranch.com/17721677/%E0%B8%AD%E0%B8%B1%E0%B8%95%E0%B8%A3%E0%B8%B2%E0%B8%84%E0%B9%88%E0%B8%B2%E0%B8%9A%E0%B8%A3%E0%B8%B4%E0%B8%81%E0%B8%B2%E0%B8%A3) | | General and specialized documents | Per page, from | ฿400 general, ฿500 specialized (1.85 and 2.5 baht a word) | [an agency's rate page](https://www.iamtranslation.com/%E0%B8%94%E0%B8%B9%E0%B8%AB%E0%B8%99%E0%B9%89%E0%B8%B2-14413-%E0%B8%AD%E0%B8%B1%E0%B8%95%E0%B8%A3%E0%B8%B2%E0%B8%84%E0%B9%88%E0%B8%B2%E0%B8%9A%E0%B8%A3%E0%B8%B4%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B9%81%E0%B8%9B%E0%B8%A5%E0%B9%80%E0%B8%AD%E0%B8%81%E0%B8%AA%E0%B8%B2%E0%B8%A3.html) | | Certified documents, Thai to English | Per document | ฿400 for an ID card, ฿450 for a birth certificate, ฿550 to ฿900 for a court judgement | [a certified translator's rate page](https://www.firstchoicetranslation.com/en/servicerateeng/) | | General and technical text | Per page | ฿800 general, ฿1,200 to ฿1,500 technical | [a language institute's price page](https://www.sumaa.net/services/translation-services/) | | Certified Thai, overseas agency | Per page or per word | $24.95 a 250-word page certified, $0.10 a word standard | [an overseas agency's Thai page](https://rushtranslate.com/languages/thai) | Two of the pages price the same 300 words six times apart. A general page is ฿250 on the freelance listing and ฿800 at the institute, and the technical tier on the same institute page reaches ฿1,500. The difference is the translator, the subject, and the review the fee includes, and each page states all three. ## Certified documents are a different product A birth certificate, a house registration, or a court judgement translated for a visa or a government filing is priced per document, and the translation is the smaller line. [an agency's rate page](https://www.bbktranslator.com/rate) lists the consular legalization run from ฿500 on top of the form, and [a certified translator's rate page](https://www.firstchoicetranslation.com/en/servicerateeng/) prints an express fee of ฿250 beside its per-document prices. The stamp, the signed statement, and the trip are what the buyer pays for. Generated translation does none of that. A certified document needs a named translator who signs for the text, and this guide leaves that column out of every comparison below. ## What generated translation costs per page Generated translation is priced per character of source text, at 25 credits per thousand characters, where 1,000 credits are one dollar. English runs about 6 characters a word with the space, which makes a 300-word page about 1,800 characters. One page therefore costs about 45 credits, which is $0.05. There is no per-page tier, no rush fee, and no minimum order. The one floor is 2 credits per request, which is what a single product name costs. Context, a glossary, and instructions bill at a lower reference rate on top, and [the credits guide](https://paxalabs.com/docs/credits) walks through how a request is charged and refunded. > The characters-per-word figure is an estimate for English prose. Thai source text has no spaces between words and a different character count per meaning, and a Thai-to-English job is best priced from its character count directly. Every response reports the exact count it billed. ## Three jobs, priced three ways | Job | Text | Freelance floor | Agency or institute | Generated | | --- | --- | --- | --- | --- | | One-page business letter | 300 words | ฿250 | ฿400 to ฿800 | 45 credits ($0.05) | | 40-page commercial contract | 12,000 words | ฿10,000 at ฿250 a page | ฿30,000 at 2.5 baht a word, ฿48,000 at ฿1,200 a page | 1,800 credits ($1.80) | | Catalogue of 2,000 products | 40,000 words | ฿40,000 at ฿1 a word | ฿74,000 at 1.85 baht a word | 6,000 credits ($6) | The freelance column applies one listing's page rate to the whole job, and a real contract quote is negotiated at the specialized tier. The generated column is the published rate times the characters, with no rounding beyond the per-request floor. The gap grows with length. On a letter the two sit a few hundred baht apart. On the contract the generated translation costs less than one page of the specialized rate. A contract of that length is sent in parts, because one request accepts 20,000 characters of text. The parts share a glossary, and a defined term reads the same in clause 3 and clause 41. ## A year of updates The letter is translated once. The catalogue is translated every month. Take the 2,000-product catalogue above, where a tenth of the descriptions change each month with a new variant, a price line, or a reworded claim. With a human translator each month's batch is 4,000 words, and 48,000 words a year. That is ฿48,000 at the per-word rate above, plus the review that keeps a term consistent with what was translated last month. A month with fewer changes still meets the minimum order, ฿3,600 a year at that page's minimum. With generated translation the changed descriptions are sent as one batch of up to 200 segments with the same glossary as last month, and the unchanged ones are never sent. That is 7,200 credits a year, or $7.20. The [translation guide](https://paxalabs.com/docs/translation#glossaries) shows how a glossary holds a product name across batches. ## What the free credits buy A new account starts with 100 credits. At the published rate that is about 4,000 characters of source text, about 666 English words, or 2 pages of the size used above. It covers a letter, a set of interface strings, or the first batch of a catalogue, which is enough to read the output against a human draft before deciding. The free grant is for personal and evaluation use, and any purchase unlocks commercial use for the account, including the free credits. ## Where a human translator still wins - Any document that needs a signature. Certified, sworn, and notarized translation is a named person's liability, and no rate above for generated text applies. - A campaign line, a slogan, or a brand name, where the job is to write a Thai line that lands and the source is a brief. - Literary text, where voice is the product and a second draft by the same translator is the point. - The final read of a contract that binds the reader. A generated draft plus a lawyer's review is a common split, and the lawyer's hour is the larger line. ## What generated translation changes The rate is one line. The turnaround is the request. A batch of 200 product descriptions returns in one call, and the register, the pronoun pair, and the fixed terms are set once in the request and hold across every segment. The [politeness post](https://paxalabs.com/guides/thai-politeness-in-translation) covers the register choices, and [Paxa Translate Lite](https://paxalabs.com/docs/api/translate) documents the fields that carry them. The cost the generated column hides is review. A translator reads their own draft back. A generated draft needs a reader who knows the product, and a glossary written by that reader is what stops the same review from repeating every month. ## Next steps - Read the [pricing page](https://paxalabs.com/pricing) for the plans and what a month of translation costs. - Open the [translate tool](https://paxalabs.com/app/translate) to run a page with a glossary and read the result. - Read the [translation guide](https://paxalabs.com/docs/translation) for batching, segments, and the two billing rates. - Compare the priced examples for [support](https://paxalabs.com/use-cases/support), [ecommerce](https://paxalabs.com/use-cases/ecommerce), and [contracts](https://paxalabs.com/use-cases/contracts). # Thai has no full stop: where streaming speech should break Thai ends a sentence with a space and no full stop, and a voice agent streaming text to a speech socket has to find that boundary before the voice can start. Published 2026-09-07. Paxa Labs. Thai writing ends a sentence with a space. The same space separates clauses inside the sentence, and the full stop is absent from ordinary prose. A voice agent that streams a reply to a speech socket as the language model writes it has to decide where one sentence ends and the next begins. The character it would wait for in English never arrives. This guide is about that decision. It covers what the [live socket](https://paxalabs.com/docs/live-streaming) does with a stream of Thai text, which boundaries a Thai reply carries, and where a break sounds wrong. It ends with a way to have the language model mark the breaks itself. ## A space does the work of a full stop Written Thai uses one character for two jobs. A space closes a sentence, and a space also closes a clause inside a sentence. The comma appears in numbers and lists, the question mark appears in chat more than in print, and the full stop appears after an abbreviation and almost nowhere else. A reader tells a sentence break from a clause break by the words on either side of the space. The spacing rules taught in school put the space after a complete sentence, before a conjunction such as แต่ or เพราะ, and around a number or a quotation. A list gets one between its items. A well-written paragraph is a chain of short spaced phrases, and the sentence boundary inside the chain carries no mark of its own. This becomes a problem the moment text is spoken as it arrives. English streaming code waits for a period. Thai streaming code has nothing to wait for. ## Why the boundary decides the latency A live voice agent sends text to the speech socket as the language model produces it, and the socket synthesizes in legs. A leg starts when the buffer holds something worth speaking. The listener hears the first leg after the model's first sentence plus one synthesis pass, and every boundary after that is a place where the voice takes a breath. Two things go wrong at a bad boundary. A leg cut too early speaks half a clause, pauses, and speaks the rest with the pitch reset, which sounds like a hesitation. A leg cut too late waits for text the model is still writing, and the silence before the first word grows with every character the model adds. The socket flushes on three triggers, which are a newline in the frame, sentence-ending punctuation at the end of the buffer, and a buffered-length threshold. English dialogue trips the second trigger at every full stop. Thai dialogue trips neither of the first two and rides the length threshold, which suits a paragraph and arrives late for a reply of one sentence. The [API reference](https://paxalabs.com/docs/api/live) documents the three triggers and the `flush` frame that forces a leg by hand. ## Where a Thai sentence ends in dialogue Spoken Thai marks the end of an utterance more clearly than written Thai does. A reply from an assistant ends on a final particle far more often than a paragraph of prose does. ครับ and ค่ะ close a polite statement, คะ and ครับ close a polite question, นะ and สิ and เถอะ soften or urge, and ล่ะ and ไหม and มั้ย ask. A particle followed by a space is the strongest boundary signal a Thai stream carries. - A final particle followed by a space: ค่ะ ครับ คะ นะคะ นะครับ สิ เถอะ ล่ะ ไหม มั้ย หรือเปล่า. In a polite reply this closes nearly every sentence. - A newline. A language model writes one when asked to, and the socket flushes on it without any client logic. - A space before a sentence-opening word such as แต่ แล้ว ส่วน ถ้า เพราะ ดังนั้น. This is a clause boundary at least, and a safe pause on a long reply. - A closing quotation mark or bracket followed by a space. Particles carry the register of the reply, and the [politeness post](https://paxalabs.com/guides/thai-politeness-in-translation) explains why a formal assistant ends nearly every sentence on one. That habit is what makes the particle rule reliable. An assistant instructed to write polite Thai produces its own boundaries. ## Where a break sounds wrong A space is a pause, and the [script guide](https://paxalabs.com/guides/writing-for-thai-text-to-speech) covers how a script uses one. A leg boundary is a longer pause with a pitch reset, and there are places a speaker would never put one. - Before a final particle. A flush that lands before ค่ะ leaves the particle as its own leg, spoken alone after a gap. - Inside a number, an amount, or a time. ๒,๕๐๐ บาท and 14:30 น. are one spoken unit. - Between a title and a name, as in คุณ สมชาย, or between a name and the particle that follows it. - After a conjunction. A leg that ends on แต่ hangs in the air until the next leg arrives. - Between a verb and its object across a clause space, where a reader runs straight on. A short reply is one leg. Splitting a twelve-word answer into three legs costs nothing extra in credits and gains nothing in latency, because the first leg already started the voice. ## Ask the model to write the breaks The cheapest boundary detector is the language model itself. Instruct it to end every sentence with a newline and to keep each line to one or two clauses. The socket flushes on the newline, each leg is one sentence, and the client carries no Thai-specific logic at all. The client still sends a `flush` frame at the end of the turn, because the last sentence can end without a newline when the model stops. Flushing an empty buffer is a safe no-op, and the `flushed` event confirms that the buffer drained. A model that ignores the instruction on a long answer still hits the length threshold, and the particle rule above is the fallback for a client that wants to flush sooner. Check the particle, then the space after it, and never the particle alone. ค่ะ in the middle of a quoted line is a particle inside speech, and the space is what says the sentence closed. ## What an interruption costs A speaker talks over the reply. The client sends `cancel`. The leg being spoken stops, and its charge stays, because that text was synthesized before the interruption arrived. The buffered text that had not started was never charged, and it is discarded. The connection stays open, and the next `text` frame begins a new leg. The [live guide](https://paxalabs.com/docs/live-streaming#interrupt) walks through the exchange. Sending one sentence per leg bounds the cost of an interruption to the sentence being spoken. Credits follow characters however many legs carry them, and the 0.1 credit minimum applies once per connection. Each leg also counts as one request against the plan's [rate limit](https://paxalabs.com/docs/rate-limits), and a client that flushes after every word spends that limit without gaining anything a listener can hear. ## A turn, frame by frame The trace below is one assistant turn. The first two sentences end on a newline the model wrote, and the socket flushes each one as it lands. The third sentence arrives without a newline, and the client's `flush` at the end of the turn closes it. one turn on the live socket: ``` client text ได้เลยค่ะ\n server charged leg 1 server audio ... client text ร้านเปิดเก้าโมงเช้า client text ถึงสองทุ่มค่ะ\n server charged leg 2 server audio ... client text วันอาทิตย์ปิดนะคะ client flush server charged leg 3 server audio ... server flushed ``` Each `charged` event is the receipt for one leg. The listener hears ได้เลยค่ะ while the model is still writing the opening hours, which is the whole point of streaming a reply. ## Next steps - Read the [live streaming guide](https://paxalabs.com/docs/live-streaming) for the session lifecycle and the [reference](https://paxalabs.com/docs/api/live) for every frame. - Read the [politeness post](https://paxalabs.com/guides/thai-politeness-in-translation) to set the register the particles come from. - Open the [companion page](https://paxalabs.com/ai-companion) to hear a Thai reply spoken in the demo voices. # Thai PDFs into a search index: OCR, chunking, and the missing spaces How to turn scanned Thai documents into chunks a search index or a language model can use, when the text has no spaces to cut on. Published 2026-09-07. Paxa Labs. Most Thai source documents a company wants to search arrive as scans. Government circulars, contracts, board minutes, and older annual reports are PDFs of paper, and the ones with a text layer often carry a broken one. Once the pages are read, the text has no spaces between words and no full stops between sentences, and the chunker written for English cuts it in the wrong places. A Thai document goes from a scan to a chunk in an index in five steps. Read the pages as structure, see why fixed windows fail on Thai, choose where to cut, decide what to carry with each chunk, and normalize before anything is indexed. The examples use [Paxa OCR Lite](https://paxalabs.com/docs/ocr) for the reading. The chunking applies to any reader that returns structure. ## Scans and broken text layers A Thai PDF is one of two things. A scan has no text at all, and every character on the page has to be read from pixels. A born-digital PDF has a text layer, and Thai text layers fail in the ways the [extraction guide](https://paxalabs.com/guides/thai-pdf-text-extraction) catalogs. Tone marks are stored out of order, legacy fonts map bytes to the wrong characters, and columns come back interleaved. Both cases end in the same place. The page has to be read as a page. A pipeline that tries the text layer first and falls back to OCR on failure needs a way to detect the failure. A corrupted Thai text layer looks like text to every check short of a Thai speaker reading it. Reading every page the same way costs a few credits per page and removes the branch. ## Read the document as blocks Ask for structure at the reading step. With `output` set to `"structured"`, [POST /v1/ocr](https://paxalabs.com/docs/api/ocr) returns each page as typed blocks in reading order. The blocks are headings with a level, paragraphs, lists with their items, tables as rows of cells, and figures with their captions. The lines below are the kind of blocks one page of a regulation returns. one page as structured blocks: ``` { "type": "heading", "level": 1, "text": "ระเบียบการเบิกค่าเดินทาง" } { "type": "heading", "level": 2, "text": "หมวด ๒ อัตราค่าพาหนะ" } { "type": "paragraph", "text": "ข้อ ๗ การเดินทางโดยรถไฟให้เบิกได้ตามที่จ่ายจริง ..." } { "type": "table", "rows": [["ประเภท", "อัตรา (บาท/กม.)"], ["รถยนต์ส่วนตัว", "๔"], ["รถจักรยานยนต์", "๒"]] } ``` One request reads at most 50 pages and 10 MiB, and a longer document is sent as parts. Keep the page numbers continuous across the parts, because the page is what a citation points at. The Markdown output carries the same reading with the structure as heading marks and pipe tables, and a pipeline that already parses Markdown can start there. The blocks save the parse. ## Why fixed windows fail on Thai The usual chunker takes a window of a few hundred characters and slides it forward with some overlap. It trusts that a boundary near a space or a period lands near a meaning boundary. English gives it a space every five characters and a period every twenty words. Thai gives it a space every clause and nothing between words. A window that ends on a character count ends inside a word most of the time, and inside a sentence almost every time. The overlap hides the damage without repairing it. A word cut in half appears whole in the next chunk, and the half in this one is a string no query will match. A sentence split across two chunks has its subject in one and its verdict in the other, and a retrieval that returns one of them returns a fragment that answers nothing. Chunkers that count tokens have the same problem one step removed. The token boundary is a property of the tokenizer, and it has no more idea where a Thai word ends than the character count does. ## Cut on the structure the reader returned The document already carries its boundaries. Headings open sections, paragraphs are units of meaning the author chose, and a table is one object. A chunker that walks the blocks needs three rules and no Thai dictionary. - A paragraph is the default chunk. Merge short consecutive paragraphs under one heading up to a size cap. A merge stops at a heading. - A table is one chunk, whole, with its header row kept. A row alone loses the column names, and a table split by size loses the rows that answer the question. - A list is one chunk when it fits. When it does not, split between items and keep each item whole. A paragraph that exceeds the cap on its own is the one place a cut inside prose is needed. Cut on the Thai space, which is a clause boundary at least and a sentence boundary often, and prefer the space nearest the cap from below. That keeps a clause together, and it never lands inside a word. The [segmentation post](https://paxalabs.com/guides/thai-word-segmentation) explains why a cut inside a word is worse in Thai than a cut inside a sentence. The size cap is set by the model that reads the chunks and the number of chunks returned per query. Set it in characters of Thai, measured on the corpus. A Thai character carries more of a word than a Latin one, and a cap tuned on English prose is loose on Thai. ## Carry the heading trail and the page A paragraph under หมวด ๒ อัตราค่าพาหนะ means something different from the same paragraph under หมวด ๓. Prefix each chunk's text with its heading trail when it goes to the embedding, and store the trail as a field for display. Store the page too. A user who asks where a rule came from wants the page, and a reviewer who checks a reading wants to open the scan at it. one chunk in the index: ``` { "doc": "travel-regulation-2569.pdf", "page": 3, "trail": ["ระเบียบการเบิกค่าเดินทาง", "หมวด ๒ อัตราค่าพาหนะ"], "text": "ข้อ ๗ การเดินทางโดยรถไฟให้เบิกได้ตามที่จ่ายจริง ...", "match": "ข้อ 7 การเดินทางโดยรถไฟให้เบิกได้ตามที่จ่ายจริง ..." } ``` Numbered clauses such as ข้อ ๗ are worth a field of their own in a regulation or a contract, because a query often names the clause. The reader returns the number as printed. The pipeline reads it off the start of the paragraph. ## Normalize a matching copy, keep the original A Thai document carries several spellings of the same value, and a query uses one of them. Build a second copy of each chunk for matching, and leave the original for display and citation. - Thai digits to Arabic. ๒๕๖๙ and 2569 are the same year, and a document mixes the two on one page. - Buddhist Era years carried as printed, with the Common Era year in a separate field. Subtracting 543 in the text itself breaks the citation. - The vowel อำ as one code point. Some documents store it as นิคหิต followed by สระอา, which Unicode normalization leaves alone, and the two forms never match. - Unicode NFC, which fixes the order of stacked marks a broken text layer produced. - Whitespace collapsed to one space, and zero-width characters removed. A reader returns none, and a text layer returns many. Run the same normalization on the query. A mismatch between the two sides is the most common reason a term that is visibly on the page is not found. ## Lexical, vector, or both A vector index embeds the chunk and needs no word boundaries. The embedding model reads the characters, and the chunk boundary above is the only Thai-specific decision. A lexical index stores terms, and a term is a word. A Thai lexical index therefore needs a segmenter on both the chunk and the query, the same one on both, and a rebuild when the segmenter changes. Exact strings are where the lexical side earns its place. Clause numbers, document ids, tax ids, amounts, and names are what a vector index blurs and a term index nails. A hybrid that scores both and merges the lists handles a regulation well, and the normalized copy is what the lexical side indexes. ## What a fifty-page report costs to read A page costs 6.5 credits whatever it holds, and the page count is known before the charge. A 50-page report is 325 credits, which is $0.33. Read a document once, and rebuild the chunks from the stored blocks whenever the chunking rules change. Re-read a page only when the page changes. Store the blocks as well as the chunks. The chunk cap, the trail format, and the normalization all change as the index is tuned, and each change is a rebuild from stored blocks at no cost. ## Next steps - Read the [OCR guide](https://paxalabs.com/docs/ocr) for the outputs and limits, and the [reference](https://paxalabs.com/docs/api/ocr) for the block fields. - Read the [extraction post](https://paxalabs.com/guides/thai-pdf-text-extraction) for what a Thai text layer gets wrong. - Open the [documents tool](https://paxalabs.com/app/documents) to read a page and see the blocks it returns. # What a Thai voiceover costs in 2026: studio, freelancer, AI Thai voice over rates from public listings, per spot and per minute, beside what the same 30-second spot, e-learning module, and audiobook cost as generated speech. Published 2026-09-02, updated 2026-10-01. Paxa Labs. A Thai voiceover in 2026 costs from ฿500 for a 30-second freelance read to ฿6,000 for an advertising spot package. A studio quotes from ฿1,000 for e-learning and from ฿3,000 for a spot. Generated speech prices the same 30 seconds at about 3 credits, which is $0.003. This guide lays the two side by side. The human figures come from public listings retrieved on 2 September 2026 and linked beside each number. The generated figures come from the published per-character rate and the speaking rate this site prices audio with, and both move together when either constant moves. ## How human voice work is priced Human voice work is priced by the unit the client can count. A commercial spot is priced per spot at a fixed length, usually 15, 30, or 60 seconds. Narration for e-learning, corporate video, and documentaries is priced per finished minute of delivered audio. A long project such as an audiobook is priced per project, per finished hour, or as a share of sales. The headline figure is rarely the whole invoice. A listing states how many revisions are included, and a script change after delivery is a new session. Usage rights are a separate line for broadcast and paid media, a rush turnaround carries its own surcharge, and a studio booking adds engineer time to the artist's fee. - Revisions included, stated as a count. One listing below includes two tone adjustments and one script change. - Usage rights for broadcast and paid placement, quoted per term and renewed. - Turnaround, one to two days on the listings below, with rush delivery at a surcharge. - Studio and engineer time whenever the artist records somewhere other than a home booth. - Pickups after the client hears the cut, which are a new booking on most rate cards. ## Thai voiceover rates from public listings The table holds every figure found on six public pages on 2 September 2026. Each row is one listing's own price, and a listing is the opening position in a negotiation. Read the spread as the range a buyer meets on the first day of looking. | Job | Unit | Figure | Source | | --- | --- | --- | --- | | Spot or short read | 30 seconds | ฿500 | [a freelance listing](https://www.thaifreelanceagency.com/freelances/%E0%B8%A3%E0%B8%B1%E0%B8%9A%E0%B8%9E%E0%B8%B2%E0%B8%81%E0%B8%A2%E0%B9%8C%E0%B9%80%E0%B8%AA%E0%B8%B5%E0%B8%A2%E0%B8%87-%E0%B9%82%E0%B8%86%E0%B8%A9%E0%B8%93%E0%B8%B2-%E0%B8%AA%E0%B8%9B%E0%B8%AD%E0%B8%95%E0%B9%82%E0%B8%86%E0%B8%A9%E0%B8%93%E0%B8%B2-%E0%B8%AD%E0%B8%B7%E0%B9%88%E0%B8%99%E0%B9%86/) | | Narration | Per minute | ฿1,000 (฿1,500 for 1:30, ฿2,000 for 2:00) | [a freelance listing](https://www.thaifreelanceagency.com/freelances/%E0%B8%A3%E0%B8%B1%E0%B8%9A%E0%B8%9E%E0%B8%B2%E0%B8%81%E0%B8%A2%E0%B9%8C%E0%B9%80%E0%B8%AA%E0%B8%B5%E0%B8%A2%E0%B8%87-%E0%B9%82%E0%B8%86%E0%B8%A9%E0%B8%93%E0%B8%B2-%E0%B8%AA%E0%B8%9B%E0%B8%AD%E0%B8%95%E0%B9%82%E0%B8%86%E0%B8%A9%E0%B8%93%E0%B8%B2-%E0%B8%AD%E0%B8%B7%E0%B9%88%E0%B8%99%E0%B9%86/) | | Any voice work | Starting price | ฿500 | [a marketplace category](https://fastwork.co/voice-over) | | Any media, WAV delivery, three revisions | Starting price | ฿700 | [a freelance listing](https://fastwork.co/user/o9i1l16/voice-over-15381223) | | Advertising spot, 15 or 30 seconds | Per project | ฿6,000 | [a freelance listing](https://fastwork.co/user/bellnice17/voice-over-42801369) | | Presentation, infographic, corporate video | Per minute | ฿3,000 | [a freelance listing](https://fastwork.co/user/bellnice17/voice-over-42801369) | | E-learning, VTR, company profile | From | ฿1,000 | [a studio's price page](https://mellowlystudio.com/) | | Translation with voiceover | From | ฿2,000 | [a studio's price page](https://mellowlystudio.com/) | | Advertising spot | From | ฿3,000 | [a studio's price page](https://mellowlystudio.com/) | | Audiobook narration | Share of sales | 20% after VAT and payment fees | [an audiobook platform's narrator terms](https://www.mebmarket.com/?action=find_narrator) | Two of the six pages price the same 30 seconds twelve times apart. A short read is ฿500 on one freelance listing and ฿6,000 as a spot package on another. The difference is the artist, the deliverable, and what the fee covers, and each listing states all three. ## How to compare two quotes Two quotes for the same script rarely price the same thing. Five questions put them on one footing, and every one of them moves the number. - What the rights cover. A read licensed for one platform for one year and a buyout for all media are different products at different prices. - What the minute is. A finished minute of delivered audio and an hour of studio time are billed on different clocks, and a slow read fills more minutes. - How many revisions are inside the fee, and what counts as a revision. A tone change and a rewritten line are usually different rows. - What arrives. A raw WAV, a cleaned and levelled file, and a mixed track with music are three deliverables, and the edit is often the largest hidden line. - When it arrives, and what a rush costs. Two of the listings above state one to two days as the base. Ask the same five of a generated read. The rights are the account's plan, and the minute is the text divided by the speaking rate. Revisions are re-reads at the same rate, the file is the format the request asked for, and the turnaround is the session. ## What an AI voice costs per minute Generated speech is priced per character of text, at 10 credits per thousand characters, where 1,000 credits are one dollar. A minute of Thai narration runs about 606 characters of text, a rate measured on the free grant and in line with published corpus work on Thai speech ([CCOST](https://zenodo.org/records/17366698), [JASA](https://pubs.aip.org/asa/jasa/article/158/2/1204/3359743/Effects-of-speaking-rate-on-f0-trajectories)). One minute of audio therefore costs about 6.1 credits, which is $0.0061. There is no session, no booking, and no per-project floor. The one floor is 0.1 credits per request, which is what a two-word line costs. A script that changes after the first listen costs another read at the same rate, and [the credits guide](https://paxalabs.com/docs/credits) walks through how a request is charged and refunded. > The characters-per-minute figure is an estimate for Thai narration at a natural pace. A slower read, a meditation script, or a line with long pauses spends fewer characters per minute, and a fast promo read spends more. The credit cost follows the text, and the minutes follow the read. ## Three jobs, priced three ways | Job | Text | Freelance floor | Package or studio | Generated | | --- | --- | --- | --- | --- | | 30-second spot | 303 characters | ฿500 | ฿3,000 to ฿6,000 | 3 credits ($0.003) | | 10-minute e-learning module | 6,060 characters | ฿10,000 at ฿1,000 per minute | ฿30,000 at ฿3,000 per minute | 61 credits ($0.06) | | 8-hour audiobook | 290,880 characters | ฿480,000 at ฿1,000 per minute | 20% of sales on a platform deal | 2,909 credits ($2.91) | The freelance column applies the listing's per-minute rate to the whole job. A real audiobook quote is negotiated per project or paid as a share of sales, and the platform terms above show one such deal. The generated column is the published rate times the text, with no rounding beyond the per-request floor. The gap grows with length. On a spot the two sit a few hundred baht apart at the low end. On an audiobook the generated read costs less than one minute of the human per-minute rate. ## A year of updates The spot is priced once. The catalogue is priced every quarter. Take 200 minutes of e-learning across twenty ten-minute modules, where a few lines in every module change each quarter with a price, a policy, or a product name. With a human artist each changed module is a pickup. At the per-minute rates above with a one-minute minimum per module, twenty modules a quarter is 80 pickup minutes a year. That is ฿80,000 at the freelance rate and ฿240,000 at the corporate rate. Studio time comes on top, and so does a second artist when the original is unavailable. With generated speech the whole catalogue is re-read every quarter, because a re-read costs the same as a pickup and keeps every module in one voice at one pace. That is 800 minutes of audio a year, 4,848 credits, or $4.85. The line that changed and the line that did not cost the same, and nothing waits for a booking. ## What the free credits buy A new account starts with 100 credits, a one-time grant. At the published rate that is about 10,000 characters of text, or roughly 16 minutes of narration at the speaking rate above. It covers about 33 reads of a 30-second spot, enough to hear the same script in most of the roster before choosing. The free grant is for personal use and evaluation. Any purchase unlocks commercial use for the account, including audio made on the free credits. ## Where a human voice still wins - A campaign built around a face. When the voice is the person in the film, the fee buys the person. - Live direction in the booth. A director who wants the third word landed harder gets it in one take, with the artist listening. - A licensed celebrity or character voice, where the contract is the product. - Singing, chanting, and anything carrying a melody. - A read the client will judge against a specific human performance they already have in mind. Those jobs are a small share of voice spending. The bulk is e-learning modules, product videos, IVR menus, listings, and updates to all of the above, and that is the work where the rate table is the whole decision. ## What generated speech changes A script change costs another read at the same per-character rate, with no session fee and no pickup schedule. A module that changes every quarter is re-read every quarter for the price of the changed lines. Casting happens before the meeting. All 34 voices on the roster read the same script, and the client hears every candidate on the same text, which a booking budget never allowed. [The voices page](https://paxalabs.com/voices) lists them with a sample line each. Thai and English sit in one line. Paxa TTS Flash reads a product name, an English phrase, and the Thai around them in one pass. A human session covers the same line by casting a bilingual artist or splicing two reads. Volume stops being a schedule problem. An eight-hour audiobook is minutes of generation and a day of listening, and the listening is where the effort moves. ## Hidden costs on both sides | Cost | Human session | Generated speech | | --- | --- | --- | | Revisions | Included up to a stated count, then a new session | Another read at the per-character rate | | Usage rights | A separate line for broadcast and paid placement, renewed by term | Commercial use for the account from the first purchase | | Turnaround | One to two days on the listings, rush at a surcharge | The same session, in the playground or through the API | | Studio and engineer | Booked by the hour when the artist records in a studio | None | | Review | The client hears one take per session | Someone listens to the whole cut, and that time is real | | Pickups | A new booking for a mispronounced name | A respelling in the script, then one more read | The cost the generated column hides is attention. A human artist catches a mispronounced name in the booth. A generated read needs a listener who knows the names, and [the guide to writing for a Thai AI voice](https://paxalabs.com/guides/writing-for-thai-text-to-speech) covers the fixes a script can carry. ## Next steps - Read [the pricing page](https://paxalabs.com/pricing) for the plans and what a month of narration costs. - Type a line into [the box on the text to speech page](https://paxalabs.com/tts) and hear it in the demo voices without an account. - Open [the playground](https://paxalabs.com/app/playground) to run a full script, pick from all 34 voices, and download the audio. - Compare the priced examples for [advertising](https://paxalabs.com/use-cases/advertising), [e-learning](https://paxalabs.com/use-cases/e-learning), and [audiobooks](https://paxalabs.com/use-cases/audiobooks). # Choosing a Thai AI voice for the job, across all 34 voices How to pick a Thai text to speech voice for ads, IVR, audiobooks, sleep, companions, games, regional campaigns, and English lines, from all 34 voices. Published 2026-09-02, updated 2026-09-25. Paxa Labs. The right Thai AI voice is the one cast for the job the line has to do. Paxa TTS Flash ships 34 voices, and each carries a catalog role that says what it was cast for, from a news bulletin to a bedtime story. The roster is grouped here by job. Every voice links to its own page, where its published sample plays, and the whole roster plays at the end of this page. The [voices index](https://paxalabs.com/voices) lists all 34 with their sample lines, and the API returns the same catalog from [the voices endpoint](https://paxalabs.com/docs/api/voices). ## How to choose, in order Decide in this order and most of the roster falls away before you play a single clip. - The job first. A promo, an IVR menu, an audiobook chapter, and a meditation script ask for different pacing, and the catalog roles were cast on pacing. - Then the register of your text. Thai carries politeness in its particles and pronouns. A line ending in ครับ or ค่ะ is polite in any voice, and a line ending in นะ is close in any voice. Fix the words before you pick the voice. - Then gender, when the surface has a fixed persona. The roster holds 18 female and 16 male voices, and every job below offers both. - Then language. 26 voices are Thai-first and read an English word inside a Thai sentence the way a Bangkok speaker does. The 6 English-first voices read a whole English line without a Thai accent, and read Thai with one. The 2 Mandarin Chinese voices do the same for Mandarin. - Then accent. 4 voices carry a regional register for a campaign that speaks to one region. ## Voices for ads and promos [Nom Yen](https://paxalabs.com/voices/nomyen) is the roster's female lead, cast for promos, social clips, and everyday product speech. It is the voice the landing page opens on, and the one to start with when a spot has to sound like the product itself. [Moo Ping](https://paxalabs.com/voices/mooping) brings confident street energy to ads and shorts, the read a 15-second vertical clip wants. [Tom Yum](https://paxalabs.com/voices/tomyum) is the fast, chatty best-friend voice for drama recaps, entertainment news, and social clips. [Massaman](https://paxalabs.com/voices/massaman) is the deep, steady leading-man voice for trailers and drama, cast for the spot that has to carry weight in one sentence. A short spot has room for one voice. Cast it on the register the product already speaks in, then hear the same script in the two alternatives before the client meeting. The [advertising use case](https://paxalabs.com/use-cases/advertising) walks a spot through the whole roster, and the [creators page](https://paxalabs.com/use-cases/creators) covers a channel's own voice. ## Voices for IVR and announcements [Thong Ek](https://paxalabs.com/voices/thongek) is the catalog's news anchor, a crisp, authoritative read for bulletins and corporate updates. An IVR menu is a corporate update read aloud, and a queue message written in polite Thai lands in this voice without sounding hurried. [Massaman](https://paxalabs.com/voices/massaman) is cast for announcements as well as trailers, and suits a public-address line in a station or a mall. [Khanom Krok](https://paxalabs.com/voices/khanomkrok) is the calm, unhurried male lead, the choice for a hold message that has to stay patient for the fourth repeat. For an English support line, see the English-first voices below. The [call centers use case](https://paxalabs.com/use-cases/call-centers) and the [announcements use case](https://paxalabs.com/use-cases/announcements) cover the surfaces themselves. ## Voices for audiobooks and long reads [Foi Thong](https://paxalabs.com/voices/foithong) is the captivating narrator for long reads, and the pick when a chapter has to hold attention for forty minutes. [Khanom Krok](https://paxalabs.com/voices/khanomkrok) reads documentary and heritage narration at an elder's pace. [Panang](https://paxalabs.com/voices/panang) is the polished, theatrical voice with a stage actor's gravitas, cast for serious storytelling and the audio novel that wants a performance. A long read is where pacing matters most. Play a whole paragraph in the [playground](https://paxalabs.com/app/playground) before you decide, because a voice that sounds right on one sentence can tire over a chapter. The [audiobooks use case](https://paxalabs.com/use-cases/audiobooks) and the [publishers page](https://paxalabs.com/use-cases/publishers) price a full book. ## Voices for sleep, meditation, and ASMR [Lod Chong](https://paxalabs.com/voices/lodchong) is the slow, close-mic voice cast for mindfulness and sleep. For wind-downs and bedtime, [Woon](https://paxalabs.com/voices/woon) reads in a soft near-whisper. The male voice of the three is [Sangkaya](https://paxalabs.com/voices/sangkaya), quiet and close-mic, cast for ASMR and calm narration. All three are written for a microphone a few centimeters from the mouth, and they read a slow script slowly. The [meditation page](https://paxalabs.com/meditation) plays them side by side and shows the speed setting a slower read uses. [Khao Niao](https://paxalabs.com/voices/khaoniao) is a low, unhurried male voice cast for guided breathing, wellbeing narration, and slow documentary. It suits the script that tells the listener what to do next. ## Voices for companions and late-night reads [Sanae Chan](https://paxalabs.com/voices/sanaechan) is the warm, magnetic voice with a late-night glow, and the voice the [AI companion page](https://paxalabs.com/ai-companion) opens on. For late-night reads, [Tub Tim Krob](https://paxalabs.com/voices/tubtimkrob) brings a low, intimate delivery. The male option is [Oliang](https://paxalabs.com/voices/oliang), the relaxed low drawl of late-night radio. A companion sounds close because its lines are written close, with the pronouns and particles Thai uses between friends. The voice carries that register; it does not invent it. The [companions use case](https://paxalabs.com/use-cases/companions) shows the four registers on one script. ## Voices for characters and games [Luk Chup](https://paxalabs.com/voices/lukchup) is the light, childlike character voice for cartoons and playful spots. [Bua Loi](https://paxalabs.com/voices/bualoi) is the sweet, youthful voice for characters and light stories. [Panang](https://paxalabs.com/voices/panang) returns here for the narrator or the antagonist who needs a stage voice. A cast needs contrast more than it needs range. Two characters on the same voice blur, and two characters on a light voice and a deep one read as two people at once. The [games and apps use case](https://paxalabs.com/use-cases/games-apps) and the [interactive fiction page](https://paxalabs.com/use-cases/interactive-fiction) cover casting a whole script. ## Voices for regional campaigns [Som Tam](https://paxalabs.com/voices/somtam) and [Larb](https://paxalabs.com/voices/larb) carry the [Isan register](https://paxalabs.com/voices/accents/isan), one female and one male. [Khao Soi](https://paxalabs.com/voices/khaosoi) carries the soft [Northern cadence](https://paxalabs.com/voices/accents/northern), and [Roti](https://paxalabs.com/voices/roti) the quick [Southern cadence](https://paxalabs.com/voices/accents/southern). Write the script in standard Thai. A regional voice reads standard text with its accent, and the [accents page](https://paxalabs.com/voices/accents) plays one Central Thai sentence in all four, with [Khanom Krok](https://paxalabs.com/voices/khanomkrok) as the control. A campaign that speaks to one region and a national campaign can share a script and differ only in the voice. ## Voices for English lines [Donut](https://paxalabs.com/voices/donut) is the cheerful English voice for promos and demos. [Cookie](https://paxalabs.com/voices/cookie) is the kind, attentive English voice for support and onboarding. [Toast](https://paxalabs.com/voices/toast) reads tutorials in plain-spoken everyday English, and [Latte](https://paxalabs.com/voices/latte) is the polished voice for brand films. The split is by how much English the line holds. A product name or an English phrase inside a Thai sentence belongs to a Thai-first voice, which reads it the way the sentence's speaker would. A whole English paragraph belongs to an English-first voice. The [English voices page](https://paxalabs.com/english-voices) plays all 6 on one line. ## Voices for events and podcasts [Pad Thai](https://paxalabs.com/voices/padthai) is the Thai-English code-switching MC for events and lifestyle content, cast for the script that changes language mid-sentence and has to sound like it meant to. [Yoyo](https://paxalabs.com/voices/yoyo) is a conversational podcast host with an easy live-mic feel. It is the real voice of one of the lab's co-founders, and the one entry in the roster that is a person. ## Voices that take emotion tags [Khanom Chan](https://paxalabs.com/voices/khanomchan) and [Kaprao](https://paxalabs.com/voices/kaprao) are experimental conversational voices that read emotion tags written into the text. A tag such as `[happy]` or `[sad:0.5]` changes the delivery from where it sits until the next tag, which suits companions and game characters whose lines already know their mood. Their delivery may change while we tune them. Pin a stable voice for audio that has to sound identical across releases. The [emotional voices](https://paxalabs.com/emotional-voices) page lists every tag and the delivery it sets. ## What a voice cannot change Register comes from the words. The model has no style control, and the same voice reads ขอบคุณครับ and ขอบใจนะ as differently as those two lines are. Emotion comes from the words too, except on the experimental voices above, which also read emotion tags. If a read sounds wrong, change the text before you change the voice: the particle at the end, the pronoun at the start, a space where a breath belongs. > A Thai space is a phrase break, and the voice pauses on it. A comma does the same job for a shorter beat. Neither changes the charge. The [script guide](https://paxalabs.com/guides/writing-for-thai-text-to-speech) covers numbers, abbreviations, and English words in a Thai line, all of which read differently from how they look. ## One voice per surface Hold one voice per brand surface. A product that greets in one voice, reads notifications in another, and narrates its tutorial in a third sounds like three products. Add a second voice for a second character, or for a second language, and for nothing else. Keep the voice's slug in configuration next to the brand color. A slug is a stable public identifier, and a voice keeps its slug for as long as it serves. ## Voice by job, on one table | Job | Voices | Gender | Language or accent | | --- | --- | --- | --- | | Ads and promos | Nom Yen, Moo Ping, Tom Yum, Massaman | Female, Male, Female, Male | Thai | | IVR and announcements | Thong Ek, Massaman, Khanom Krok | Male, Male, Male | Thai | | Audiobooks and long reads | Foi Thong, Khanom Krok, Panang | Female, Male, Male | Thai | | Sleep, meditation, and ASMR | Lod Chong, Woon, Sangkaya, Khao Niao | Female, Female, Male, Male | Thai | | Companions and late-night reads | Sanae Chan, Tub Tim Krob, Oliang | Female, Female, Male | Thai | | Characters and games | Luk Chup, Bua Loi, Panang | Female, Female, Male | Thai | | Regional campaigns | Som Tam, Larb, Khao Soi, Roti | Female, Male, Female, Male | Isan, Isan, Northern, Southern | | English lines | Donut, Cookie, Toast, Latte, Espresso, Mocha | Female, Female, Male, Male, Male, Female | English | | Mandarin lines | Tao Huay, Oolong | Female, Male | Mandarin Chinese | | Events and podcasts | Pad Thai, Yoyo | Female, Male | Thai and English | | Emotion tags (experimental) | Khanom Chan, Kaprao | Female, Male | Thai | Every voice, with its published sample line. Press play on a row to hear it. The roster is the same one [the voices endpoint](https://paxalabs.com/docs/api/voices) returns. | Voice | id | Gender | Language | Description | | --- | --- | --- | --- | --- | | Khanom Krok | khanomkrok | Male | Thai | Calm, unhurried elder voice and the roster's male lead: documentary, heritage storytelling, and narration. | | Nom Yen | nomyen | Female | Thai | Bright, energetic voice and the roster's female lead: promos, social clips, and everyday product speech. | | Foi Thong | foithong | Female | Thai | Captivating narrator for long reads and explainers. | | Massaman | massaman | Male | Thai | Deep, steady leading-man voice for trailers, drama, and announcements. | | Thong Ek | thongek | Male | Thai | Crisp, authoritative read for news bulletins and corporate updates. | | Panang | panang | Male | Thai | Polished, theatrical delivery with a stage actor's gravitas. | | Oliang | oliang | Male | Thai | Relaxed low drawl for chill content and late-night radio. | | Sanae Chan | sanaechan | Female | Thai | Warm, magnetic voice with a late-night glow. | | Tub Tim Krob | tubtimkrob | Female | Thai | Low, intimate delivery for late-night reads. | | Moo Ping | mooping | Male | Thai | Confident, streetwise voice for ads and shorts. | | Bua Loi | bualoi | Female | Thai | Sweet, youthful voice for characters and light stories. | | Luk Chup | lukchup | Female | Thai | Light, childlike voice for characters and playful spots. | | Lod Chong | lodchong | Female | Thai | Slow, close-mic delivery for mindfulness sessions and sleep audio. | | Woon | woon | Female | Thai | Hushed, breathy read for wind-downs and bedtime. | | Sangkaya | sangkaya | Male | Thai | Quiet male voice for sleep tracks and slow narration. | | Pad Thai | padthai | Female | Thai | Thai-English code-switching MC for events and lifestyle content. | | Khao Niao | khaoniao | Male | Thai | Low, unhurried male voice for guided breathing, wellbeing narration, and slow documentary. | | Tom Yum | tomyum | Female | Thai | Fast, hot, chatty best-friend voice for drama recaps, entertainment news, and social clips. | | Som Tam | somtam | Female | Isan Thai | Isan-accented voice with local warmth. | | Larb | larb | Male | Isan Thai | Isan-accented male voice for regional content. | | Khao Soi | khaosoi | Female | Northern Thai | Northern-accented voice with a soft Lanna cadence. | | Roti | roti | Male | Southern Thai | Southern-accented voice with a quick coastal cadence. | | Donut | donut | Female | English | Cheerful English voice for promos and demos. | | Cookie | cookie | Female | English | Kind, attentive English voice for support lines and onboarding. | | Toast | toast | Male | English | Plain-spoken everyday English voice for tutorials. | | Latte | latte | Male | English | Polished English voice for brand films. | | Espresso | espresso | Male | English | Deep, dark, unhurried English reader for long fiction, documentaries, and trailers. | | Mocha | mocha | Female | English | Close, gentle, low-volume English voice for bedtime, wellness, and companion speech. | | Tao Huay | taohuay | Female | Mandarin Chinese | Tender, melancholy Mandarin voice for drama recaps, audio novels, and companion speech. | | Oolong | oolong | Male | Mandarin Chinese | Low, rich Mandarin narrator for audio novels, brand reads, and late-night stories. | | Tako | tako | Female | Thai | Easygoing reader for audiobooks, recaps, and explainers. | Experimental: **Khanom Chan** (`khanomchan`, Female, Thai). Natural conversational voice that changes mood with emotion tags written into the text: happy, sad, angry, or afraid. Experimental: **Kaprao** (`kaprao`, Male, Thai). Easygoing male voice that changes mood with emotion tags written into the text: happy, sad, angry, or afraid. Special: **Yoyo** (`yoyo`, Male, Thai). One of Paxa Labs' co-founders: a conversational podcast host with an easy live-mic feel. ## Before you decide - Name the job before you name the voice, and start from the two or three voices cast for it. - Write the line in the register you want, particles included, and only then compare voices on it. - Type your own sentence into the box on [the text to speech page](https://paxalabs.com/tts). It speaks 9 of the voices with no account. - Open the full roster of 34 in the [playground](https://paxalabs.com/app/playground) for a longer script, a whole paragraph, and audio you can download. - Hold one voice per surface, and write its slug into configuration once. # How to write a script for a Thai AI voice Ten script rules for Thai text to speech cover where to put spaces and how to write numbers, dates, abbreviations, and English words the AI voice reads right. Published 2026-09-02. Paxa Labs. A Thai AI voice reads what the script says, and a Thai script says less than it looks. Written Thai marks no word boundaries, writes some vowels nowhere, and leaves the reading of a number or an abbreviation to the reader. Ten rules below settle those gaps before the model has to. Every example line is 50 characters or fewer, the limit of the box on [/tts](https://paxalabs.com/tts). Each one can be pasted there and heard. The rules apply to any Thai voice and were written for Paxa TTS Flash. The reading facts behind them are the ones published on the [product page](https://paxalabs.com/text-to-speech). ## A space is a pause Thai spends the space character on phrase breaks, the job a comma does in English. A voice pauses at a space and runs through everything between two spaces as one breath. That gives the writer direct control of phrasing, and it also means a space in the wrong place cuts a word in half. the space decides the phrase: ``` before วันนี้ฝนตกหนักมากรถติดทั้งเมือง after วันนี้ฝนตกหนักมาก รถติดทั้งเมือง one breath becomes two phrases wrong วันนี้ฝนตกหนักมาก รถติดทั้งเมื อง a space inside เมือง splits the word ``` A name inside a sentence reads cleanest with a space on each side. คุณ สมชาย stresses the name; คุณสมชาย runs it into the title. The same holds for a place or a product name that the listener needs to catch on the first hearing. Put a space where a reader would breathe: after a clause, before a conjunction such as แต่ or แล้ว, and around a name that carries its own stress. Leave a compound alone. ร้านอาหาร, โทรศัพท์มือถือ, and กรุงเทพมหานคร each read as one word. ## Full stops and question marks set the fall Thai prose usually ends a sentence with a space and nothing else. A voice needs more. A full stop tells it the pitch falls and the next sentence starts fresh; a question mark tells it to lift. A script for speech may punctuate where a printed page would not. punctuation the page omits, the voice needs: ``` before ร้านเปิดกี่โมง พรุ่งนี้หยุดไหม after ร้านเปิดกี่โมง? พรุ่งนี้หยุดไหม? before ขอบคุณที่โทรมา เราจะติดต่อกลับ after ขอบคุณที่โทรมา. เราจะติดต่อกลับ. ``` An exclamation mark raises the line and suits a promotion. Three dots hold the voice on the last syllable, which works once in a bedtime story and wears out in a menu. A comma inside a Thai sentence does nothing a space does not already do, and a stray one can read as a pause in the wrong place. Use the space for pauses and keep the comma for numbers. ## Numbers, dates, and prices A number has more than one reading, and the script decides which one the voice gets. A year reads as a quantity, a phone number reads digit by digit, and a quantity ending in one takes เอ็ด in place of หนึ่ง. Thai digits and Arabic digits read the same. Use whichever the audience expects, and keep to one kind within a script. | Written | How it reads | What to write | | --- | --- | --- | | พ.ศ. 2569 | พอ-สอ สองพันห้าร้อยหกสิบเก้า | Keep it. A year reads as a quantity. | | 1,250 บาท | หนึ่งพันสองร้อยห้าสิบบาท | Keep the comma; the unit follows the number. | | ๑๐๑ | ร้อยเอ็ด | A trailing one in a quantity takes เอ็ด. Nothing to add. | | 02 000 0000 | ศูนย์สอง ศูนย์ศูนย์ศูนย์ ศูนย์ศูนย์ศูนย์ศูนย์ | Space the groups; a phone number reads digit by digit. | | 10150 | หนึ่งศูนย์หนึ่งห้าศูนย์ | Leave a postal code unspaced; it reads digit by digit. | | ๑๖.๓๐ น. | สิบหกนาฬิกาสามสิบนาที | Keep the น. mark; it makes the point a clock time. | | 3/4 | สามส่วนสี่ or สาม ทับ สี่ | Write the reading out when it matters: สามในสี่ or บ้านเลขที่ 3 ทับ 4. | When two readings are both plausible, write the one you mean as words. A fraction, a house number with a slash, and a score all look alike as digits and read differently aloud. The written-out form costs a few characters and removes the guess. A date reads day, month, year, and the era mark tells the voice which century it is in. 15 กรกฎาคม 2569 reads the year in full. Add ค.ศ. in front of a Western year, because 2026 read bare sounds like a Buddhist year from the past. A percentage reads เปอร์เซ็นต์ and a decimal reads จุด, so 7% and 3.5 need no rewriting. ## Abbreviations, and when to expand them Thai abbreviates with a full stop, ฯ, or ฯลฯ, and each mark carries a reading rule. กรุงเทพฯ restores six syllables. ฯลฯ reads และอื่น ๆ at the end of a list and ละถึง between two items. ก.พ. has two accepted readings, the full month กุมภาพันธ์ and the letter names กอ-พอ, and a listener accepts either. Expand an abbreviation when only one reading is right for your audience. A clinic reminder wants กุมภาพันธ์ in full. A news line can leave กทม. as it is, because กรุงเทพมหานคร is the only reading it has. expand where one reading is required: ``` before นัดครั้งถัดไป 14 ก.พ. ที่ รพ. เดิม after นัดครั้งถัดไป 14 กุมภาพันธ์ ที่โรงพยาบาลเดิม ``` ## English words inside Thai Thai business text carries English inside it: product names, job titles, and acronyms. Keep them in Latin script. A voice reads a Latin word as English and a Thai respelling as Thai, and the respelling often reads worse. An acronym reads letter by letter. `AI` and `HR` need no help. keep English in its own script: ``` before ส่งอีเมลหาเอชอาร์ก่อนประชุม after ส่ง email หา HR ก่อน meeting before เปิดแอปเวอร์ชันใหม่ได้แล้ว after เปิด app เวอร์ชันใหม่ได้แล้ว ``` Words that Thai has fully absorbed stay Thai. แท็กซี่, คอมพิวเตอร์, and เวอร์ชัน read well in Thai script because that is how Thai speakers say them. A brand name follows the brand's own spelling. Some acronyms are spoken as words. NASA reads as two syllables and UNESCO as three, and a letter-by-letter reading sounds foreign to a Thai listener. Write those in Thai script, นาซา and ยูเนสโก, and the voice reads the word. A mixed name keeps each half in its own script: บริษัท Paxa Labs จำกัด reads the Thai frame in Thai and the name in English. ## ๆ and what it repeats The repetition mark ๆ repeats a word, two syllables, or a whole phrase, and meaning sets the span. เด็กเล็ก ๆ repeats one word. ในวันหนึ่ง ๆ repeats two syllables. แต่ละวัน ๆ repeats the phrase. Write the mark with a space before it, as the standard orthography does, and write the repetition out when the span is in doubt. write the span out when it could be misread: ``` before ค่อย ๆ เดิน ช้า ๆ นะ after ค่อยค่อยเดิน ช้าช้านะ both read the same here; the second leaves the voice nothing to decide ``` ## Particles and pronouns set the register and the gender A Thai sentence carries its politeness in its last word and its speaker's gender in its pronoun. ค่ะ and ดิฉัน belong to a female speaker; ครับ and ผม to a male one. A script written with ครับ and read by a female voice sounds wrong before the first sentence ends. Match the line to the voice you pick on the [voices page](https://paxalabs.com/voices). | Register | Line | Voice | | --- | --- | --- | | Formal, female | ยินดีต้อนรับค่ะ ดิฉันขอแนะนำบริการของเรา | Nom Yen | | Formal, male | สวัสดีครับ ผมขอแจ้งข่าวประจำวันนี้ | Thong Ek | | Casual, male | เดี๋ยวผมส่งไฟล์ให้นะ | Khanom Krok | The particle does the work a style setting would do elsewhere. Write นะคะ and the read softens. Write ครับผม and it firms up. Drop the particle and the line reads as narration with no listener in the room. Spell the female particle by its tone. ค่ะ closes a statement and carries a falling tone. คะ closes a question and follows นะ, with a high tone. ขอบคุณค่ะ and ใช่ไหมคะ are both right; the reverse spellings read with the wrong tone and a Thai listener hears the slip at once. ## How long a line should be Short lines suit ads and prompts, because each one lands with its own stress. Narration reads better as full paragraphs of three to five sentences, where the voice can settle into a pace. One request takes up to 5,000 characters. A chapter longer than that splits at a heading or a paragraph, and a split inside a sentence is audible as a break in the read. - An ad line is one clause, one idea, and a particle at the end. - A menu prompt names the option first and the instruction second. - Narration is written in paragraphs, with a space at every phrase and a full stop at every sentence. - A list read aloud puts one item per line, with the item count written before the list. ## English lines belong to the English-first voices A Thai voice reads a whole English sentence with a Thai accent, the way a Thai speaker would. That is right for one English word inside a Thai line and wrong for a support script written entirely in English. The roster carries [English-first voices](https://paxalabs.com/english-voices) for that work; Cookie and Toast read a support greeting in the register a native speaker expects. > A single line that switches language halfway is a Thai line. Give it a Thai voice, and keep the English words in Latin script. ## Speed and timing Pace is a request option. The script never has to fake it with punctuation. The `speed` field runs from 0.5 to 1.5 on the voice's own rate, and it changes nothing in the charge. A meditation read sits below 1; a news bulletin sits above it. Word timestamps mark where each word starts and ends, which is what a [subtitle track](https://paxalabs.com/subtitles) needs in a language that writes no spaces. The [timestamps guide](https://paxalabs.com/docs/timestamps) shows the three granularities. Word timing suits subtitles and karaoke-style highlighting. Sentence timing suits a player that jumps between lines. Utterance timing marks the whole request and costs the least attention. Pick the coarsest one that does the job. ## Common mistakes at a glance | Written | What you hear | Fix | | --- | --- | --- | | รถติดทั้งเมื อง | Two fragments where one word belongs. | Delete the space inside the word. | | โทร 020000000 | A quantity in the tens of millions. | Space the digits into groups. | | ประชุม 14 ก.พ. | The letter names, when the month was meant. | Write กุมภาพันธ์. | | ส่งเมล์หาเอชอาร์ | Thai respellings that sound like neither language. | Write email and HR in Latin script. | | สวัสดีครับ read by a female voice | A male particle in a female voice. | Write ค่ะ, or pick a male voice. | | A 2,000-character block with no full stop | One long fall with no fresh starts. | End each sentence with a full stop. | ## Before you press play - Read the line aloud once. Where you breathed, put a space. - End every sentence with a full stop or a question mark. - Write ambiguous numbers as words, and space phone numbers into groups. - Expand an abbreviation when only one reading fits. - Keep English words in Latin script. - Match ครับ or ค่ะ to the voice you chose. - Try each line in the box on [/tts](https://paxalabs.com/tts), then move longer scripts to the [playground](https://paxalabs.com/app/playground), which takes 5,000 characters and returns audio you can download. - Developers integrating the read find the request fields in the [text to speech guide](https://paxalabs.com/docs/text-to-speech) and the [API reference](https://paxalabs.com/docs/api/text-to-speech). # How to extract text from Thai PDFs and scans Why copying text out of a Thai PDF returns broken marks, junk letters, and unspaced phrases, and how Thai OCR turns the page into text, headings, and tables. Published 2026-09-02. Paxa Labs. A Thai PDF gives up its text in one of two ways. A file with a text layer can be copied directly. A scanned file has to be read by OCR, which looks at the page as an image and writes the text back. The catch is that the text layer of a Thai PDF often carries broken text, even when the page prints perfectly. Tone marks land in the wrong order, older fonts extract as Latin junk, phrases arrive as one unbroken token, and Thai and Arabic digits sit on the same line. Each failure below appears with the string it produces, followed by when OCR is the right tool and what document OCR returns for a real Thai invoice. ## The two kinds of Thai PDF A PDF exported from a word processor, an accounting system, or a browser carries a text layer. The glyphs on the page are drawn from fonts, and the characters behind them are stored in the file. Select all and copy, and something comes out. A PDF from a scanner, a phone camera, or a fax gateway is a container for images. The page is pixels. Select all finds nothing, and search finds nothing, until a reader has turned the pixels into characters. Two quick checks tell them apart. Search the file for a word you can see on the page, and if the search fails on a word that is clearly there, the page is an image. Drag to select a line, and if the selection highlights the whole page as one rectangle, or nothing at all, the page is an image. Many real Thai documents are mixed, with a typed form as a text layer and a stamped, signed, or annotated section as an image on top of it. ## Why the text layer of a Thai PDF breaks English extraction has one common failure, the missing space between columns. Thai extraction has four, and each one produces text that looks plausible until a Thai reader or a database sees it. ### Tone and vowel marks come out of order A Thai syllable stacks marks above and below its consonant. The word `กุ้ง` (shrimp) is four code points in Unicode order, and a correct text layer stores them in that order. A PDF producer that lays out glyphs one at a time can store the tone mark before the vowel, because the mark was drawn first. Some producers do not store the marks as characters at all and paint them as positioned artwork. one word, four code points, three storage orders: ``` กุ้ง as written U+0E01 ก base consonant U+0E38 ุ vowel below U+0E49 ้ tone mark above U+0E07 ง final consonant ก ้ ุ ง the same four, as one PDF stores them ก ง the same word, marks placed as art ``` Both cases extract as a string that renders almost right and matches nothing. A search for `กุ้ง` fails, a database that stores the string fails to join on it, and a downstream translation of it reads a different word. The page itself looks correct throughout. ### Legacy fonts extract as Latin junk Thai documents from before Unicode was universal were set in TIS-620 and in fonts with their own private glyph maps. Those files still circulate, and government forms, contracts, and title deeds are reprinted from them. A PDF made from one has a text layer, and the bytes in it are Thai only to a reader that knows the code page. A generic extractor reads them as Latin-1 and produces the characters below. a legacy font on the page and in the file: ``` กรุงเทพ on the page A1 C3 D8 A7 E0 B7 BE in the file (TIS-620) ¡Ãا෾ after extraction ``` The junk is deterministic, and a table can map it back for the TIS-620 case. Font-private maps have no table. The visible page is the only reliable copy of the text, and that is the definition of a scan. ### There are no spaces between words Thai writes words together and spends the space character on phrase and clause breaks. A correct text layer of an invoice returns `ใบแจ้งหนี้เลขที่` as one token. Any tool that splits on spaces, matches a word boundary with `\b`, or wraps at a fixed width is working on phrases it cannot see inside. The text is right and the tooling around it is wrong. ### Two numeral systems share a line Thai documents use Thai digits (`๑ ๒ ๓`) for dates, quantities, and addresses and Arabic digits for money, in the same table, sometimes in the same cell. A date reads `๑๕ กรกฎาคม ๒๕๖๙`, in the Buddhist Era. A quantity of `๔` sits beside an amount of `3,000.00`. Latin runs are mixed in as well, from invoice numbers like `INV-2569-0107` to product names. Extraction that normalizes digits, or that assumes one script per field, silently changes values. ## When OCR is the right tool for a Thai PDF | The file | What to do | | --- | --- | | Text layer, extracted text reads correctly | Use the text layer and keep the structure the producer stored | | Text layer, marks out of order or missing | Treat the page as an image and OCR it | | Text layer, Latin junk from a legacy font | Treat the page as an image and OCR it | | Scanned or photographed page | OCR | | Typed form with a stamped or handwritten layer | OCR the whole page, then compare with the text layer | OCR reads the page the way a person does, from the pixels, and is indifferent to how the file stored its characters. That makes it the right tool for every row above except the first. It is also the only tool that returns the stamped, signed, and handwritten parts of a mixed page. OCR is the wrong tool for a clean text layer, where it costs a vision pass to recover text the file already holds. Run the two checks first. ## What document OCR returns for a Thai page Character recognition returns a string. Document OCR returns the page. Paxa OCR Lite reads the whole page in reading order and returns it as Markdown, with headings and tables kept, or as typed blocks (heading, paragraph, list, table, figure) for code to consume. The lines below are the first lines of the committed reading of a sample Thai invoice, unedited. the reading of a Thai invoice, first lines: ``` # บริษัท ศตวรรษการค้า จำกัด (สำนักงานใหญ่) เลขประจำตัวผู้เสียภาษีอากร 0 1055 64078 12 7 # ใบแจ้งหนี้ เลขที่ INV-2569-0107 วันที่ ๑๕ กรกฎาคม ๒๕๖๙ | ลำดับ | รายละเอียด | จำนวน | หน่วยละ | จำนวนเงิน | | ๑ | ค่าบริการดูแลระบบรายเดือน กรกฎาคม ๒๕๖๙ | ๑ | 18,000.00 | 18,000.00 | | ๒ | ค่าพื้นที่จัดเก็บข้อมูลส่วนเกิน | ๔ | 750.00 | 3,000.00 | ( สามหมื่นหกพันสามร้อยแปดสิบบาทถ้วน ) ``` Three things in that excerpt are the ones that break text-layer extraction. The line items carry Thai digits for the running number and the quantity beside Arabic digits for the money, and both return as the digits printed. The date is in the Buddhist Era, and it returns as printed for the application to convert. The total in words, `สามหมื่นหกพันสามร้อยแปดสิบบาทถ้วน`, is one unspaced Thai string, and it returns as one line. With `output` set to `structured`, the same page returns as typed blocks. The company name is a level one heading block, each label line is a paragraph block, and the line items are one table block whose rows are arrays of cell strings. Code reads the invoice number as the text after its label and the line items as the rows of that block, and no template per supplier is needed. The table returns as a table. A reader that walks the page in pixel strips would flatten those five columns into one line per row and lose which number was the unit price. The full reading, and the sample page it was made from, are on the [invoice page](https://paxalabs.com/ocr/invoice). ## Checking a Thai OCR result A reading is verbatim, and a document can be wrong on its own terms. These checks catch most problems in a Thai business document before the values reach a system. - A Thai tax identification number is 13 digits. Strip the spaces and count. `0 1055 64078 12 7` passes. - Line amounts equal quantity times unit price, and the subtotal, VAT at 7%, and grand total add up. A reading that disagrees with itself has misread a digit. - A year above 2500 is Buddhist Era. Subtract 543 before storing it as a date. `๒๕๖๙` is 2026. - The number of table rows matches the count of line items on the page. A merged row is the most common table error on a dense invoice. - Thai digits and Arabic digits both parse as numbers. Convert them with a table. Hand conversion introduces errors. - The amount in words matches the grand total. It is the one field a person wrote to catch exactly this class of error. > A tax invoice is a legal document with fields the Revenue Code prescribes, and a plain invoice is a commercial one. The [tax invoice page](https://paxalabs.com/ocr/tax-invoice) lists the prescribed fields, which are the ones an audit will read back. ## What Thai OCR costs and what it accepts The charge is per page. A page costs 6.5 credits, which is $6.50 per thousand pages, and the amount of text on the page does not change it. A new account starts with 100 free credits, which reads 15 pages. One request takes a PDF, PNG, JPEG, or WebP of up to 10 MB and up to 50 pages. An image is one page. The page count is read from the file before the charge, and a PDF that wants a password is refused before any credit moves. Accounts-payable teams that run every supplier invoice through this flow are described on the [invoice processing page](https://paxalabs.com/use-cases/invoices). ## How to extract text from a Thai PDF today - Run the two checks. If the text layer reads correctly, keep it. - Drop a broken or scanned page into the [playground](https://paxalabs.com/app/playground) and read the Markdown back. No code is needed for the first page. - Choose `markdown` when a person or a language model reads the result next, and `structured` when code does. - Read the [OCR guide](https://paxalabs.com/docs/ocr) for the request shape and the [API reference](https://paxalabs.com/docs/api/ocr) for every field and error. - Run the checklist above on the first hundred documents, and keep the checks that fired. - The [Thai OCR page](https://paxalabs.com/ocr) holds the witnessed reading of a full document and the twelve document types with sample readings. # Translating into Thai at the right level of politeness Thai politeness is written into the sentence through particles, pronouns, and vocabulary. How to choose a register before you translate, with examples. Published 2026-09-02. Paxa Labs. A Thai translation has to choose a level of politeness before it can write a single sentence. English carries most of its politeness in tone and word choice, and a neutral sentence stays neutral in print. Thai marks the relationship between speaker and listener inside the grammar: the pronouns, the sentence-final particles, and the verb itself all change with the register. This means a translation into Thai cannot be neutral. Every rendered sentence sits at some level of formality, implies a speaker with a gender, and addresses a listener with a rank. A translation that is technically accurate and set at the wrong register reads as a bank talking like a teenager, or a game talking like a ministry. The register is a decision the translator has to make. ## Politeness in Thai is written into the sentence The same action has several Thai verbs, each with its own level. To eat is `กิน` between friends, `ทาน` to a customer or a guest, and `รับประทาน` in an announcement or a formal notice. To die is `ตาย` in speech and `เสียชีวิต` in the news. To tell someone is `บอก` at home and `แจ้ง` in a letter from a company. A translator picks the tier for the whole document, and a reader hears the tier in the first line. | Meaning | Casual | Polite | Formal | | --- | --- | --- | --- | | to eat | กิน | ทาน | รับประทาน | | to die | ตาย | เสียชีวิต | ถึงแก่กรรม | | to tell, to inform | บอก | แจ้ง | เรียนให้ทราบ | | to give | ให้ | มอบ | มอบให้ | | to know | รู้ | ทราบ | ทราบ | | money | ตังค์ | เงิน | เงิน | The verb tier is the least visible of the three signals because it hides inside ordinary sentences. It is also the one a non-Thai reviewer misses most often, since a bilingual dictionary lists all three verbs under the same English headword. The particles and pronouns are louder, and they are where most translation complaints begin. ## The particle ladder from ครับ to จ้า A Thai sentence usually ends with a particle that has no dictionary meaning and carries most of the politeness. The particle also states the speaker's gender. `ครับ` marks a male speaker, `ค่ะ` a female speaker, and both are the default in customer-facing text. Dropping the particle turns a polite sentence into a flat one. Choosing the wrong one turns a hotel into a stranger. | Particle | Speaker | Reads as | Typical use | | --- | --- | --- | --- | | ครับ | Male | Polite, neutral | Every polite statement or answer | | ค่ะ | Female | Polite, neutral | Every polite statement or answer | | คะ | Female | Polite, rising | Questions only, paired with ค่ะ | | นะครับ | Male | Polite and soft | Reassurance, gentle instructions | | นะคะ | Female | Polite and soft | Reassurance, gentle instructions | | นะ | Either | Friendly, informal | Chat between peers, casual brands | | จ้ะ | Either | Warm, familiar | Speaking to a child or a junior | | จ้า | Either | Playful, informal | Social replies, casual retail chat | | ฮะ | Male | Casual polite | Younger male speaker, relaxed | Two details trip up most machine output. First, `คะ` with a high tone is the question form and `ค่ะ` with a falling tone is the statement form. Swapping them is the most common spelling error in Thai customer service text. Second, `นะ` softens a request, and a customer service line that omits it sounds like an order. A translation that ends `กรุณารอสักครู่ค่ะ` is polite. The same line ending `รอสักครู่` is a command. ## Which Thai pronoun a translation should use English has one you and one I. Thai has a ladder for each, and the rung fixes the relationship. `ท่าน` is reserved for officials, executives, and letters that want distance. `คุณ` is the safe second person for a customer. `เธอ` and `นาย` belong to friends and fiction. On the first-person side `ดิฉัน` is a formal woman, `ผม` a polite man, `ฉัน` a neutral woman in writing, and `เรา` a company speaking as a company. | Pronoun | Person | Gender | Register | Where it fits | | --- | --- | --- | --- | --- | | ท่าน | You | Any | Formal, deferential | Government, banking, legal notices | | คุณ | You | Any | Polite, standard | Retail, support, product interfaces | | เธอ | You | Any | Familiar | Friends, fiction, song lyrics | | นาย | You | Male | Familiar, blunt | Close friends, games between players | | ผม | I | Male | Polite | Any male speaker addressing a customer | | ดิฉัน | I | Female | Formal | Presentations, official replies | | ฉัน | I | Female (mostly) | Neutral in writing | Narration, apps with a female persona | | เรา | I or we | Any | Neutral | A company or team speaking as one | | หนู | I | Female, junior | Deferential | A young speaker to an elder | Most product text does well with `คุณ` for the reader and `เรา` for the company, which avoids picking a gender for the brand in the pronoun. Interfaces often drop the pronoun altogether: `บันทึกแล้ว` says saved without naming who saved it. A translation that inserts `คุณ` into every sentence reads as padded, because Thai omits a subject whenever context supplies it. ## One sentence at three registers The English line "Your order will arrive tomorrow." has no register. Each Thai rendering below is a correct translation of it, and each belongs to a different business. The particle is written for a female speaker in the first two rows, which is the convention most Thai support teams follow for a brand voice. | Register | Thai | Who it sounds like | | --- | --- | --- | | Formal | สินค้าที่ท่านสั่งซื้อจะจัดส่งถึงในวันพรุ่งนี้ค่ะ | A bank, an airline, a government portal | | Polite | ออเดอร์ของคุณจะถึงพรุ่งนี้นะคะ | An e-commerce support chat, a delivery app | | Casual | ของถึงพรุ่งนี้นะ | A teen fashion brand, a game, a friend | Read the three rows again for what changes. The formal row uses `ท่าน`, the full verb `จัดส่ง`, and the noun `สินค้าที่ท่านสั่งซื้อ` for order. The polite row switches to `คุณ`, borrows `ออเดอร์` from English, and softens with `นะคะ`. The casual row drops the pronoun, shortens order to `ของ`, and keeps only `นะ`. Length falls from 48 characters to 16 while the meaning holds. ## Particles give the brand a gender Because `ครับ` and `ค่ะ` name the speaker's gender, every polite sentence a company publishes makes the company male or female for that sentence. Thai brands settle this once. Most consumer brands, banks, and airlines write as a female speaker with `ค่ะ`, following the convention of the call center and the front desk. Some brands with a male persona, such as a sports channel, write with `ครับ`. The failure mode is mixing. A help center that answers one article in `ค่ะ` and the next in `ครับ` reads as two different people, and a Thai reader notices at once. Decide the speaker before the first translation, write it into the brief, and check every batch against it. Short interface strings have a neutral option. Leave the particle off and use the plain verb, as native apps do for buttons and labels. ## Where formal reads cold and casual reads rude Formal Thai in the wrong place feels like a form letter. A push notification that says `ขอเรียนให้ท่านทราบว่าคำสั่งซื้อของท่านได้รับการยืนยันแล้ว` is correct and nobody wants to read it on a lock screen. The polite version `ยืนยันคำสั่งซื้อแล้วค่ะ` carries the same fact in a quarter of the characters, which also matters on a phone. Casual Thai in the wrong place feels like an insult. A payment failure that says `จ่ายไม่ผ่านนะ ลองใหม่` uses the friend register, and a customer who has just lost money reads it as flippant. The polite version `การชำระเงินไม่สำเร็จค่ะ กรุณาลองใหม่อีกครั้ง` names the problem and the next step at the register a bank would use. The rule of thumb is that money, health, legal, and error messages move one rung up, and marketing for a young audience moves one rung down. ## Borrowed words in Thai text Thai borrows English words constantly, and a translation has three ways to write each one. The established Thai spelling exists for common words: `โปรโมชัน` for promotion, `แอปพลิเคชัน` for application, `อีเมล` for email. A transliteration renders a newer word in Thai script even where no dictionary form exists, such as `แดชบอร์ด` for dashboard. Preserving keeps the Latin script, which is what Thai text does with product names, technical terms, and codes: `iPhone`, `API`, `OTP`. Register decides which option a reader expects. Formal text prefers the established Thai form and keeps Latin only for proper nouns. Polite product text mixes freely, and `กด Save` next to `บันทึก` is common in Thai apps. Casual text keeps more English because young Thai speakers code-switch in speech. Product names, feature names, and error codes should stay in the source script at every register. A transliterated product name is unsearchable and reads as a typo. ## Holding one register across a product Register drifts across a product because text is translated in batches over months by different people. The support macros arrive polite, the onboarding tour arrives casual, the terms arrive formal. A style sheet fixes the three decisions in writing, which are the speaker particle, the pronoun pair, and the borrowed-word policy. A glossary fixes the terms that must never vary, such as the Thai form of each feature name and the words that stay in Latin script. Thai has one further consistency trap. The word for you inside a sentence and the word in the greeting must agree in rank. A message that opens `เรียน คุณสมชาย` and continues with `ท่าน` has shifted register mid-paragraph. Keep one pronoun per document and let the glossary carry it. ## Royal and monastic vocabulary Two Thai registers sit above formal. Royal vocabulary, `ราชาศัพท์`, applies to the monarchy and uses separate nouns and verbs for the same actions. Monastic vocabulary applies to monks and has its own set. Both are governed by convention and by official guidance, and both need a human editor who works with them regularly. Machine translation should stop at formal, and a translation brief that involves either register should say so before any text is sent. ## Mapping each decision to an API option Each decision above maps to one field on [Paxa Translate Lite](https://paxalabs.com/docs/api/translate). The defaults are set for a caller who sends plain text and wants a sensible result; the fields exist so the register can be fixed once and held across every request. | Decision | Field | Values | | --- | --- | --- | | Overall register | `formality` | `auto` matches the source tone, `formal` writes polite written Thai with polite particles where natural and formal vocabulary, `casual` writes conversational Thai | | English words | `borrowed_words` | `auto` uses the established Thai form where one exists, `transliterate` writes borrowed words in Thai script, `preserve` keeps technical terms and proper nouns in the original script | | Speaker gender and pronoun pair | `instructions` | Free-form style direction, for example a female speaker with ค่ะ and คุณ for the reader | | Fixed terms | `glossary` | Source and target pairs, up to 5,000 per request | | Names and codes | `do_not_translate` | Strings that stay exactly as written | | Situation | `context` | Background the model reads and never translates, such as the audience and the channel | Reference fields bill at 8 credits per thousand characters against 25 for the text itself, which is the rate structure that makes a long style brief affordable to send on every request. The [translation guide](https://paxalabs.com/docs/translation) covers batching and how a glossary is matched. > State the speaker once in `instructions` and reuse the same string on every request. Register that lives in a field can be audited; register that lives in a translator's head drifts. ## A checklist before you translate - Pick the speaker particle, `ค่ะ` or `ครับ`, and write it into the brief. - Pick the pronoun pair for the reader and the company, usually `คุณ` and `เรา`. - Set `formality` once per surface: formal for money, legal, and errors, casual only for an audience that speaks that way. - List product names, feature names, and codes in `do_not_translate`. - Put every term with a fixed Thai form in the `glossary`, including the borrowed words you want spelled one way. - Send the audience and channel in `context` so a lock-screen notification and a contract are translated differently. - Test a batch of real strings in the [playground](https://paxalabs.com/app/playground); a new account starts with 100 free credits. The [translation](https://paxalabs.com/translation) page describes the model and its languages. The [e-commerce](https://paxalabs.com/use-cases/ecommerce) and [customer support](https://paxalabs.com/use-cases/support) pages show what a product listing and a support macro look like once the register is set. # What Thai word segmentation is, and why it decides translation, speech, and search Thai word segmentation finds word boundaries in text written without spaces. Why it decides translation, speech phrasing, timestamps, and search. Published 2026-09-02. Paxa Labs. Thai word segmentation is the step that finds where one word ends and the next begins in text that carries no spaces between words. Every system that handles Thai, whether it translates, speaks, indexes, or times the text, has to perform this step before it can do its own job. The step is invisible in English because the space already did the work. In Thai the space exists and means something else. This post explains what the space marks and shows the strings that can be cut two ways. It then follows one wrong cut into a translation, a spoken sentence, a subtitle file, and a search index. ## Why Thai has no spaces between words Thai writing runs its words together inside a phrase. `ผมกินข้าวที่ร้านอาหารไทย` reads as six words to a Thai speaker and as one token to any function that splits on whitespace. The space character is reserved for a larger unit. It marks the end of a phrase or a clause, the job a comma or a period does in English, and it appears far less often than an English reader expects. That convention is old and consistent. Numbers, names, and foreign words sometimes take a space on either side, and a sentence-final particle sometimes stands apart, but the words inside a Thai phrase never do. A reader learns to find the boundaries from vocabulary and context, the same way an English reader resolves which sense of `lead` a sentence uses. Words are short. On the BEST2010 corpus the average Thai word is 3.855 characters long ([COLING (2014)](https://aclanthology.org/W14-5513/)), and this site rounds that to 4 when it converts a word count into billable characters. A hundred-word paragraph is therefore a string of about four hundred characters with perhaps three spaces in it, and every one of the hundred boundaries has to be inferred. ## Strings that can be cut two ways Most Thai text segments one way once the vocabulary is known. The hard cases are strings where two different cuts both produce real words. A dictionary cannot settle them, because the dictionary contains every word on both sides. | String | One cut | Another cut | | --- | --- | --- | | ตากลม | ตา กลม (round eyes) | ตาก ลม (to air-dry) | | ไปหามเหสี | ไป หา มเหสี (go to see the queen) | ไป หาม เห สี (go carry, then two stray syllables) | | มากกว่า | มาก กว่า (more than) | มา กกว่า (come, then a non-word) | | ขนมอบ | ขนม อบ (baked sweets) | ขน มอบ (fur, to hand over) | The first two rows are the ones a reader meets in practice. `ตากลม` is a real ambiguity, and only the sentence around it decides which reading is meant. `ไปหามเหสี` looks ambiguous to a machine and never to a person, because the second cut leaves syllables that do not form words. The last two rows show the same shape: one cut is a phrase people say, the other is a sequence of dictionary entries that nobody would write. one string, two segmentations: ``` ตากลม ตา กลม round eyes ตาก ลม air-dry เด็กคนนี้ตากลม the child has round eyes เอาผ้าไปตากลม take the cloth to air-dry same five letters; the words before them decide ``` The figure is the whole problem in one line. Nothing inside `ตากลม` chooses a reading. The choice comes from `เด็กคนนี้` (this child) or `เอาผ้าไป` (take the cloth), which sit outside the string a per-word matcher is looking at. ## What a wrong cut does to a translation A translation system that segments first and translates second inherits every wrong cut. `เอาผ้าไปตากลม` cut as `ตา กลม` produces a sentence about a cloth and round eyes. The output is fluent, grammatical, and wrong, and a reviewer who does not read Thai has no signal that anything happened. Names are the common case. A brand written in Thai script, `ไลน์` for a chat app or `แกร็บ` for a ride service, is a string of ordinary syllables. Cut the wrong way, `ไลน์` becomes a word for a line and the brand disappears from the translation. The [translation API](https://paxalabs.com/docs/api/translate) takes a glossary for exactly this reason. A term listed there in its published form is matched as a unit and rendered as the caller wrote it. Markup is the other place a cut goes wrong. An inline tag reinserted by character offset lands inside a word whenever the offset drifts by one, and in Thai that means between a consonant and the vowel that belongs to it. The `format` field exists so that the model receives the markup and keeps it on word boundaries it can see. ## What a wrong cut does to speech A speech model needs the word to know two things: where to breathe and how to read the letters. Phrasing follows words and clauses. A pause dropped inside a word sounds like a stutter, and a sentence read without any pause sounds like a machine. Neither the space nor the punctuation tells the model where the words are. Syllable reading depends on the word as well. The same letters cut into different syllables in different words, and only the word decides. `ปลา` reads as one syllable, `plaa`, with a consonant cluster. `ปลัด` opens on the same two letters and reads as two syllables, `pa-lat`. `จักรยาน` reads `jak-kra-yaan` and `จักรี` reads `jak-kri`, with a different cut after the shared `จักร`. The doubled `รร` reads `un` at the end of a word in `สรร` (`san`) and `um` before a final consonant in `กรรม` (`kam`). | Word | Reading | Why the word decides | | --- | --- | --- | | ปลา · ปลัด | plaa · pa-lat | Same opening letters, cluster in one, two syllables in the other | | จักรยาน · จักรี | jak-kra-yaan · jak-kri | Same first four letters, different syllable cut | | สรร · กรรม | san · kam | The doubled รร reads un at word end and um before a final | A model that has found the word reads each of these correctly. A model that reads letters in sequence gets each one wrong on the first syllable. The [text-to-speech page](https://paxalabs.com/text-to-speech) lists more of these reading rules with the reading a Thai speaker produces. ## What a wrong cut does to word timestamps A word timestamp is a span of audio attached to a word. In English the words are given and the alignment is the whole task. In Thai the model has to produce the words first, and the timestamps inherit whatever it produced. A wrong cut yields a span that covers one and a half words, and a subtitle built from those spans highlights the wrong syllables. The speech API returns [timestamps](https://paxalabs.com/docs/timestamps) at word, sentence, or utterance granularity, and the word spans follow the model's own segmentation of the text. The [subtitles page](https://paxalabs.com/subtitles) shows what that looks like as a highlighted read-along. Sentence granularity sidesteps the question entirely when a caption only needs to change once per line. ## What a wrong cut does to a search index A search index stores terms, and a term is a word. An index built by splitting Thai on spaces stores whole phrases as single terms, and a query for one word inside a phrase finds nothing. An index built with a segmenter finds the word, and finds it under whichever cut the segmenter chose. The query has to be cut the same way, by the same segmenter, or the two sides disagree. The practical consequence is that Thai search quality is capped by segmentation quality, and that changing the segmenter means rebuilding the index. Product catalogs feel this first, because product names are exactly the strings a segmenter has never seen. ## Dictionary matching and context models Two families of method exist. Dictionary methods walk the string and take the longest word the dictionary contains at each position, with a backtrack when the remainder cannot be segmented. They are fast, transparent, and correct on most text. They fail on the ambiguous strings above, because both cuts are in the dictionary and the method has no way to look at the surrounding sentence. They also fail on every word that is not in the dictionary, which includes most new names. Context models read the characters around each position and predict whether a boundary falls there. They learn that `เด็กคนนี้` is followed by a description and `เอาผ้าไป` by an action, and they cut `ตากลม` accordingly. They handle unknown words by shape, since a Thai word has a recognizable structure of consonant, vowel, and tone mark. Their cost is that they are models, with the training data and the failure cases that implies. > The two families are often combined. A dictionary supplies the easy boundaries and a context model decides the rest. Whichever method a pipeline uses, the segmentation it produces is the segmentation every later stage inherits. ## What to do when you call an API with Thai text - Send whole sentences or [segments](https://paxalabs.com/docs/translation). A fragment cut at an arbitrary character count can start or end inside a word, and the model then has to guess at both edges. - Put brand names, product names, and place names in the glossary in the exact form you publish. A listed term is matched as a unit and never split. - Use `do_not_translate` for strings that must survive untouched, and send `format` when the text carries markup. The model then preserves the tags on boundaries it can see. - Request `timestamps` at the granularity the product needs. Sentence spans are stable; word spans are the model's own cut of the text. - Segment queries with the same method that built the index. A mismatch between the two sides is the usual cause of a Thai search that misses obvious matches. - Test the ambiguous strings from the table above in the [playground](https://paxalabs.com/app/playground) with the sentence around them, and read the output as a Thai speaker would. Segmentation is the first decision any Thai pipeline makes, and the one every later output carries. The [translation](https://paxalabs.com/translation) and [text-to-speech](https://paxalabs.com/text-to-speech) models on this site make that decision inside the model, with the whole sentence in view. The fields above are how a caller keeps it from being made alone. # CPU speech synthesis under a serverless bill Kokoro-82M served on serverless CPUs, and the two changes to the serving path that take a bursty day's bill from $2.41 to $0.38. Technical report arXiv:2610.00063v1 (https://arxiv.org/abs/2610.00063v1), 2026-09-04, 6 pages, cs.DC, cs.AI, cs.CL. Authors: Pakorn Nathong, Kunat Pipatanakul. Affiliations: Paxa Labs. Figures below are the report's own, measured on its own benchmarks. They are not specifications of any Paxa Labs API. Wayu-Paxa-TTS-Edge, the open Thai voice from the lab's [first speech report](https://paxalabs.com/research/thai-tts-from-synthetic-speech), is an 82M model on the Kokoro backbone. A model that small runs on a CPU, and a serverless container that scales with traffic is a natural place to host it. This report measures what such a container bills for, with Kokoro-82M itself as the workload. Two changes to the serving path carry the result. Bounding the CPU each request may use doubles sustained efficiency on OpenVINO, from 1.35 to 2.71 audio-seconds per CPU-second. Releasing what an idle instance holds cuts its billed memory from 12.1 GB to 1.33 GB, and a request arriving afterwards reaches first audio in 2.2 seconds. Over one day of bursty traffic the bill falls from $2.41 on PyTorch defaults to $0.38. ## What a warm instance bills for An instance-billed platform meters a container for its whole lifetime. It charges CPU-seconds while the container computes and GB-seconds for as long as it holds memory, idle or busy. The meter includes the page cache, and model files the kernel keeps there stay billable until they are reclaimed. The report prices every configuration at $30 per vCPU-month and $8 per GB-month, round figures of the same order as public list prices. Its conclusions compare configurations and do not depend on the exact rates. At those rates a PyTorch server that holds 8.7 GB while idle costs about $69 a month before it serves a request. Scaling to zero removes the charge, and the next request then waits 7.7 seconds for first audio. ## Sizing each request The exported model synthesizes one utterance per call. Padding requests of different lengths without masking changes the audio, and the model has no token-by-token decoding loop for a server to batch across. Parallelism comes from running independent requests side by side. ONNX Runtime and OpenVINO size each request's thread pool to the machine's physical cores by default. Concurrent requests then compete for the same CPUs. The report gives each request a bounded share. Its server runs eight inference streams with two threads each on a 32-vCPU container. **Audio-seconds per CPU-second at four concurrent clients** (higher is better) | Configuration | Value | | --- | --- | | OpenVINO defaults | 1.35 | | + sized threads, four streams | 2.31 | | + compile cache | 2.33 | | + eight streams | 2.71 | | + idle release | 2.71 | Table 2 of the report, each row adding one change to the one above. The thread budget and the stream count carry the gain. The compile cache and the release leave efficiency unchanged. Sizing trades the latency of a lone request for throughput under load. A short utterance with the machine to itself reaches first audio in 134 ms on OpenVINO defaults and in 493 ms once sized. At four clients the comparison reverses, and median first audio falls from 1.26 to 0.69 seconds. Throughput plateaus near 38 times realtime at eight clients, and requests past that point queue. ## A third state between warm and cold Eight streams buy that efficiency with memory. The sized server holds 12.1 GB while warm, more than any baseline, and an instance-billed platform charges for it between bursts. Scaling to zero is the usual answer, and it costs a process cold start of 5.7 to 7.7 seconds on the next request. The report adds a released state between the two. After 45 quiet seconds the server frees the compiled model and its request pool, returns unused allocator memory, and evicts the compile-cache files from the page cache. The process and the compile cache on disk stay. The next request rebuilds inference state from that cache, which skips the restart and the recompile. **Memory an idle instance holds** (lower is better) | Configuration | Value | | --- | --- | | OpenVINO defaults | 5250 MB | | + sized threads, four streams | 8684 MB | | + compile cache | 7490 MB | | + eight streams | 12097 MB | | + idle release | 1331 MB | Table 2 of the report, read 600 seconds after the last response. The eight-stream row is read at 30 seconds, before any release. A request that arrives after release reaches first audio in 2.2 seconds, within 2.1 to 2.3 across ten cycles. A process cold start of the same configuration takes 6.2 seconds. ## One day of bursty traffic The report tests both changes on a synthetic day of two audio-hours, delivered in eight equal bursts across 24 hours. The container is never scaled to zero between bursts. **One day of bursty traffic, billed in US dollars** (lower is better) | Configuration | Value | | --- | --- | | PyTorch defaults | 2.41 | | Request sizing alone | 3.24 | | Request sizing with release | 0.38 | Figure 1 of the report, at $30 per vCPU-month and $8 per GB-month. Each row is one server left running through the same day. Request sizing alone makes the day more expensive. The sized server spends less CPU on each burst and then holds 12.1 GB until the next one, and the day costs $3.24 against $2.41 on PyTorch defaults. Idle memory was already 95% of the PyTorch bill. Release is what turns the efficiency gain into a lower bill, at $0.38. ## What the server scores **Kokoro-82M served six ways on one 32-vCPU host** | System | Efficiency ↑ | First audio ↓ | Idle memory ↓ | Cold start ↓ | Per audio-hour ↓ | | --- | --- | --- | --- | --- | --- | | PyTorch eager | 0.66 | 2.46 s | 8659 MB | 7.7 s | $0.0631 | | ONNX Runtime default | 0.89 | 0.94 s | 3969 MB | 5.8 s | $0.0468 | | ONNX Runtime sized | 1.36 | 2.36 s | 3958 MB | 5.7 s | $0.0306 | | OpenVINO default | 1.35 | 1.26 s | 5250 MB | 6.9 s | $0.0309 | | OpenVINO sized | 2.71 | 0.69 s | 12097 MB | 7.4 s | $0.0154 | | OpenVINO sized, with release | 2.71 | 0.94 s | 1331 MB | 6.2 s | $0.0153 | Table 1 of the report. Efficiency is audio-seconds per CPU-second and first audio is the median, both at four concurrent clients. Idle memory is read 600 seconds after the last response, and at 30 seconds for the sized row without release. Cold start runs from process start to first audio. Cost is the busy cost per audio-hour at the report's example rates. Busy cost per audio-hour falls to $0.0153, 4.1 times below PyTorch defaults and 3.1 times below ONNX Runtime defaults. The complete system holds the least memory once idle and ties for the highest efficiency. It gives ground elsewhere. Median first audio at four clients is 0.94 seconds against 0.69 for the sized server without release, and ONNX Runtime starts a fresh process faster. ## Where the method stops With both changes in place, the report looks for further gains and keeps none. Static-shape compilation shortens per-call time by 3.3%, inside the variation between repeated runs. Padding inputs to a fixed length fails the output check. A 36-token utterance padded to 128 tokens yields 3.1 times as much audio, because the model synthesizes the padding. The study covers one model family, one runtime for the approach, two CPU architectures, and a synthetic trace. Quantization, SMT, and core pinning were left untested. The Thai student built on the same backbone was outside the study. On an AMD Zen 5 host the same design reaches 3.26 audio-seconds per CPU-second and releases to 1.13 GB. A second model, the 100M pocket-tts on its own PyTorch server, holds 1234 MB through a ten-minute idle window and has no released state. ## If you serve speech on CPUs On the report's bursty day a small speech model costs far more idle than busy, and a faster server raises the bill when it holds more memory. The implementation is about 1,900 lines of Python on OpenVINO with no custom kernels, and it is open with the raw logs behind every figure. This is research. It is not a product specification. Paxa Labs serves speech through its own [text to speech API](https://paxalabs.com/text-to-speech), which is a different model with its own published limits. The dollar figures price cloud compute at example rates and say nothing about what the API costs. ## Released artifacts - paxalabs/cpu-tts-to-the-wall: https://github.com/paxalabs/cpu-tts-to-the-wall # A Thai voice from fifteen seconds of speech An 82M model small enough to run on the device, trained entirely on speech a larger model generated, and the benchmark that says what it still gets wrong. Technical report arXiv:2609.03502v1 (https://arxiv.org/abs/2609.03502v1), 2026-09-03, 18 pages, cs.CL, cs.AI. Authors: Kunat Pipatanakul, Potsawee Manakul, Warit Sirichotedumrong, Sittipong Sripaisarnmongkol, Pakorn Nathong, Phatrasek Jirabovonvisut. Affiliations: Wayu Research, Paxa Labs, Typhoon. Figures below are the report's own, measured on its own benchmarks. They are not specifications of any Paxa Labs API. A voice-cloning model can reproduce a speaker from a few seconds of audio, but it needs a large generative backbone and a GPU to do it. A compact fixed-voice system runs anywhere, and needs hours of studio recording from the speaker it imitates. This report takes a third route: use the cloning model as a data source, turn a fifteen-second reference into a synthetic corpus, and train an 82M student on that. The student never hears a real recording of the voice it speaks with. The result, Wayu-Paxa-TTS-Edge, reads Thai and English on the device. It scores 68.2% on a held-out set of 1,531 sentences built around expressions Thai speech models get wrong. It places phrase breaks more accurately than the 600M teacher it learned from, at 82M parameters. ## One teacher, filtered, one student Thai text is prepared and verbalized, a zero-shot teacher renders it against a frozen voice reference, a quality filter accepts or rejects each candidate, and what survives trains the student. The filter rejects 23.0% of what the teacher produces. Rejection is not the same as deletion. A rejected text is re-rendered, because dropping it would quietly remove the hardest sentences from the corpus. **Twelve voices, none of them recorded**: Each one began as a written description that the teacher turned into a speaker. These are the references the synthetic corpus was built from. - Teen girl, bright (female · teenager · high pitch): https://paxalabs.com/audio/research/voices/f_teen_bright.mp3 - Young woman, bright (female · young adult · high pitch): https://paxalabs.com/audio/research/voices/f_young_bright.mp3 - Young woman, clear (female · young adult · moderate pitch): https://paxalabs.com/audio/research/voices/f_young_clear.mp3 - Young woman, warm (female · young adult · low pitch): https://paxalabs.com/audio/research/voices/f_young_warm.mp3 - Middle-aged woman, clear (female · middle-aged · moderate pitch): https://paxalabs.com/audio/research/voices/f_mid_clear.mp3 - Middle-aged woman, warm (female · middle-aged · low pitch): https://paxalabs.com/audio/research/voices/f_mid_warm.mp3 - Elder woman, soft (female · elderly · moderate pitch): https://paxalabs.com/audio/research/voices/f_elderly_soft.mp3 - Elder woman, low (female · elderly · low pitch): https://paxalabs.com/audio/research/voices/f_elderly_low.mp3 - Teen boy, bright (male · teenager · high pitch): https://paxalabs.com/audio/research/voices/m_teen_bright.mp3 - Young man, clear (male · young adult · moderate pitch): https://paxalabs.com/audio/research/voices/m_young_clear.mp3 - Middle-aged man, warm (male · middle-aged · low pitch): https://paxalabs.com/audio/research/voices/m_mid_warm.mp3 - Elder man, deep (male · elderly · very low pitch): https://paxalabs.com/audio/research/voices/m_elderly_deep.mp3 ## What character error rate hides Word error rate needs word boundaries, and Thai does not mark them. Character error rate avoids that, and then hides the errors that matter: a 150-character sentence can mispronounce a brand name and still score around 1% CER, which reads as fine. A transcript can also be perfectly correct while the waveform pauses in the middle of a word. So the report measures four things: correctness on a challenge set of local expressions, pause placement, speaker similarity, and speaking rate. The challenge set holds 1,531 sentences, each containing one target expression, scored by whether the transcript contains it. | Challenge-set category | Items | | --- | --- | | Thai-English code-switching | 391 | | Names | 310 | | Rare words | 410 | | Informal spelling | 210 | | Long sentences | 210 | | Total | 1,531 | ## What the student scores **Wayu-Paxa-TTS-Edge against its teacher and a frontier API** | System | Params | Keyword ↑ | Thai CER ↓ | Pause prec. ↑ | PPER ↓ | | --- | --- | --- | --- | --- | --- | | Wayu-Paxa-TTS-Edge | 82M | 68.2% | 3.7% | 91.4% | 6.7% | | OmniVoice teacher | 600M | 72.8% | 4.6% | 89.9% | 17.6% | | Gemini 3.1 Flash TTS | ≥405B | 79.8% | 3.3% | 96.4% | 13.8% | Table 8 of the report. PPER is the pause-placement error rate. Keyword accuracy uses 1,531 items and Thai CER 500; pause metrics use 210 long sentences. The Gemini parameter count is a literature-derived lower bound. The vendor publishes no figure. Compression costs the student difficult lexical realization and almost nothing else. It trails both larger systems on the challenge set, improves on the teacher's Thai CER, and produces the fewest pause errors of the three. Its rate of pauses inside a word is the lowest at 1.4%. Adding English took its English CER from 4.4% to 1.1% while leaving speaker similarity and the intra-word pause rate unchanged. ## What each step of the recipe bought | Corpus construction | Hours | Keyword | CER | Pause prec. | PPER | | --- | --- | --- | --- | --- | --- | | Unfiltered baseline | 17.72 | 67.5% | 4.0% | 81.3% | 16.2% | | + pause filtering | 17.63 | 69.6% | 3.5% | 88.4% | 14.3% | | + bilingual frontend | 17.63 | 68.7% | 4.1% | 88.2% | 14.8% | | + filtered scale-up | 40.58 | 67.2% | 3.9% | 85.4% | 11.9% | | + resample rejected | 41.81 | 69.2% | 3.4% | 92.8% | 6.2% | Scaling the corpus from 17.63 to 40.58 hours under the same filter improved the pause metrics and cost keyword accuracy. Repeated filtering thins out exactly the hard examples the challenge set asks about. Resampling the rejected texts recovers that coverage and produces the best configuration on every measure but one. **Trained from scratch against pretrained initialization** (higher is better) | Configuration | Value | | --- | --- | | Keyword accuracy, from scratch | 51.7% | | Keyword accuracy, pretrained | 69.2% | | Code-switch accuracy, from scratch | 22.8% | | Code-switch accuracy, pretrained | 65.5% | Table 6 of the report, on the Thai-only model. Reusing the pretrained backbone is worth 17.5 points of keyword accuracy overall and 42.7 on code-switching alone: the student inherits far more from its initialization than its size suggests. ## A dialect with no corpus Most language varieties will never have a licensed corpus or a recording studio, which is why most of them have no speech technology at all. The same pipeline was pointed at Isan with one fifteen-second crop of Isan-accented speech: the teacher rendered about 1.5 hours from it, and the student was fine-tuned for four epochs. Nothing else was recorded. **Fifteen seconds in, a dialect voice out**: The reference clip, then the fine-tuned 82M model reading text from the Thai Dialect Isan Speech Corpus. - The reference (15 seconds of Isan-accented speech, from a public demo): https://paxalabs.com/audio/research/isan/ref_15s_donor.mp3 - The student, first sentence (82M model, four Isan-only epochs): https://paxalabs.com/audio/research/isan/00_student_ep12.mp3 - The student, second sentence (82M model, four Isan-only epochs): https://paxalabs.com/audio/research/isan/01_student_ep12.mp3 The adapted student sits at 0.842 cosine similarity to the reference against the teacher's 0.854, and reaches 5.5% Isan CER where the teacher reaches 6.6%. It pays for that in Central Thai, where CER rises from 3.4% to 5.1% and keyword accuracy falls from 69.2% to 65.8%. CER measures intelligibility and nothing more. Whether the result sounds natural to an Isan speaker is not something this evaluation can answer. ## Where the method stops The student cannot learn what the teacher cannot say. Sampling the teacher 118 times and keeping any correct rendering lifts exact accuracy from 72.8% to 87.9%. That puts 15.1 points of the gap within reach of better sampling alone. The rest tracks the teacher's own training data. At 118 samples, keywords the teacher never saw are recovered 66% of the time, against 98% for keywords it saw between 100 and 10,000 times. What binds is coverage. Both evaluation instruments lean on heuristics. Pause scoring composes Thai word segmentation with CTC forced alignment, and its accuracy is bounded by both; keyword scoring uses exact matching against accepted forms. The rates are most reliable as comparisons between systems measured with the same instrument. ## If you are building on Thai speech The practical finding is that the expensive part of a Thai voice is not the model. It is the coverage of the data behind it, and text normalization and pronunciation handling sit outside the acoustic model, where 1.1 points of challenge-set accuracy were recovered with the weights frozen. Anyone shipping Thai speech can measure the same things: the evaluation framework and both benchmarks are open. This is research. It is not a product specification. Paxa Labs serves Thai speech through its own [text to speech API](https://paxalabs.com/text-to-speech), which is a different model with a different roster and its own published limits. ## Released artifacts - wayu-ai/wayu-paxa-tts-edge: https://huggingface.co/wayu-ai/wayu-paxa-tts-edge - wayu-ai/thai-tts-keyword-bench: https://huggingface.co/datasets/wayu-ai/thai-tts-keyword-bench - wayu-ai/thai-tts-pause-bench: https://huggingface.co/datasets/wayu-ai/thai-tts-pause-bench - wayu-research/thai-tts-eval: https://github.com/wayu-research/thai-tts-eval - wayu-research/wayu-tts-inference: https://github.com/wayu-research/wayu-tts-inference - kunato/wayu-paxa-tts-demo: https://huggingface.co/spaces/kunato/wayu-paxa-tts-demo # Thai OCR trained without a single real label A 0.9B document model taught on 45,723 reconstructed pages, and the controlled experiments that say which part of a synthetic document actually transfers. Technical report arXiv:2609.03595v1 (https://arxiv.org/abs/2609.03595v1), 2026-09-03, 20 pages, cs.CL, cs.AI, cs.CV. Authors: Kunat Pipatanakul. Affiliations: Wayu Research, Paxa Labs. Figures below are the report's own, measured on its own benchmarks. They are not specifications of any Paxa Labs API. Synthetic training pages give you exact labels at any scale, which is the whole reason to use them for a language whose documents are plentiful and whose labels are not. What nobody had isolated is which part of a synthetic page does the work. Realism is not one property: it bundles the source domain, the page context, the typography, the spatial structure, and the shape of the glyphs. The usual advice is to add more of all of it. This report separates those factors with a controlled reconstruction pipeline, then uses the answer to build Wayu-Paxa-OCR-Zero, a Thai document model trained without one OCR label from a real Thai page. It reads a typical printed page at 1.24% median character error, down from 6.64% for the checkpoint it was adapted from. At 0.9B parameters it beats a 7B Thai OCR model on all five evaluation sets. ## Reconstruct a real page, one property at a time Every synthetic page starts as a real one. The pipeline takes a document with annotated text regions, keeps the region geometry and the reading order, and erases the original text pixels. It then fits the label back into the same region and renders it in either a sampled typeface or real handwriting glyphs. The label is exact by construction, because it is the text that was rendered. **One reconstructed page** - Out-of-domain synthetic: An annual-report page whose original text has been erased and replaced with Thai, keeping the original charts, rules, and column structure in place. (https://paxalabs.com/research/thai-ocr-from-synthetic-data/reconstructed-page.jpg) Rendered by docaug from a public source page, as published with the report. Because the geometry survives, each property can be varied on its own. Keep the source background or replace it with white. Keep the two-dimensional arrangement or stack the regions vertically. Sample the measured Thai typeface profile or collapse to a single face. Render glyphs from a font or redraw them from a bank of real ink. ## Which properties actually transfer **Source properties, page-level training** (lower is better) | Configuration | Value | | --- | --- | | Out-of-domain synthetic | 43.99% | | Without non-text page context | 43.16% | | Without typeface diversity | 49.86% | | Without two-dimensional layout | 58.4% | Table 2 of the report: median character error rate on the handwriting set, each row removing one more property. Removing the page's backgrounds, figures and rules changes nothing; removing typeface variety and then spatial structure degrades transfer step by step. Non-text context is the property everyone reaches for first, and it moves the median by at most 1.62 points across three evaluation sets and two training settings. Typeface diversity is the first thing whose removal consistently costs something, and it costs it on handwriting. Removing it takes 6.70 and 9.48 points of median CER under page-level training, against 0.56 and 2.15 on printed pages. Flattening the layout costs more again. Training granularity changes which dataset wins, and the reversal is sharp. A model that sees whole pages does best on in-domain reconstruction, at 1.82% median CER against 5.07% for out-of-domain. A model that sees individual cropped elements does the opposite, at 15.59% against 5.52%. The report does not explain the reversal, and says so. ## How close reconstruction gets to real supervision **Reconstruction against real supervision, page-level training** (lower is better) | Configuration | Value | | --- | --- | | Qwen3-VL-2B-Instruct, unadapted | 14.47% | | Out-of-domain synthetic | 5.07% | | In-domain synthetic | 1.82% | | Real Thai printed pages | 1.31% | Table 4 of the report: median character error rate on printed Thai. On a typical page, reconstruction lands 0.51 points off real supervision. The median is the flattering statistic. Under character-weighted mean CER the same comparison reads 16.20% against 9.79%. Real supervision is still removing a tail of severe failures that reconstruction does not reach. On handwriting, reconstructed Thai print and real Thai print land within 0.13 points of each other, and both stay far behind training that includes real handwriting at 26.05%. ## The gap was handwriting, and real ink closed it A handwriting typeface renders the same character the same way every time. Real handwriting does not, and that difference is visible before any number is quoted. The two plates below carry identical labels, layout, ink height and ink colour, and differ only in where the strokes came from. **The same page, two sources of ink** - Handwriting typefaces: A Thai page rendered in a handwriting font, where every occurrence of a character has an identical outline. (https://paxalabs.com/research/thai-ocr-from-synthetic-data/glyphs-typeface.jpg) - Real glyph instances: The same Thai page redrawn from a bank of real handwritten characters, where each occurrence of a character has a different shape. (https://paxalabs.com/research/thai-ocr-from-synthetic-data/glyphs-real.jpg) Table 5 of the report. Labels, layout, ink height and ink colour held fixed; only the source of the strokes differs. Handwriting typefaces alone take page-level median CER on the handwriting set from 43.99% to 38.65%. Redrawing the same pages from real ink takes it to 37.91%, and the easier handwriting set from 34.84% to 30.66%. Because the two variants differ in nothing else, the remaining gain belongs to stroke variation. Real handwriting supervision still wins at 26.05%, and the glyph bank holds only 5,953 instances. ## What the model scores The recipe that follows from all of this is 45,723 synthetic pages: a base set with both renderings of every handwriting page, a handwriting-focused set, and 2,190 reconstructed forms. Every page is generated from a public source document. No real Thai page is used as a source image and no label from one enters training. **Wayu-Paxa-OCR-Zero against its base checkpoint and open Thai OCR** | System | Heldout ↓ | Handwriting ↓ | ThaiOCRBench ↓ | SEA-DocBench ↓ | | --- | --- | --- | --- | --- | | PaddleOCR-VL-1.6 (0.9B) | 6.64% | 74.87% | 38.0% | 8.87% | | Wayu-Paxa-OCR-Zero (0.9B) | 1.24% | 20.55% | 15.3% | 4.86% | | Typhoon OCR (7B) | 2.54% | 43.60% | 30.6% | 9.22% | | Typhoon OCR 1.5 (2B) | 0.21% | 19.36% | 6.2% | 5.81% | | Gemini 3.7 Flash | 0.00% | 11.29% | 0.9% | 5.51% | Table 7 of the report: median page character error rate, lower is better. Heldout and Handwriting are the report's own sets. ThaiOCRBench averages five audited reading tasks, and SEA-DocBench uses its 1,148-page clean Thai subset. Scores include layout-detection and reading-order errors. Against the checkpoint it was adapted from, with the architecture held fixed, the recipe cuts median CER on every benchmark: 6.64% to 1.24% on printed pages and 74.87% to 20.55% on handwriting. Against independently trained systems the comparison is about competitiveness, and it is mixed on purpose. It beats the 7B Typhoon model everywhere, nearly matches the 2B model on handwriting, and wins SEA-DocBench. It stays behind on printed pages and ThaiOCRBench. Gemini 3.7 Flash is stronger on three of the four. ## Where the method stops Synthetic supervision still trails real supervision on the severe failures that dominate a character-weighted mean, and it trails it further on handwriting. The value of matching the source domain depends on the training unit, and the reason is unresolved. A model trained only on reconstructions is less robust to document types, typefaces, and handwriting styles it never saw, which is a real limit for anyone pointing it at an unfamiliar form. ## If you are working with Thai documents The useful conclusion is where to spend effort. More visual realism in the background of a synthetic page buys nothing measurable. More typefaces, real two-dimensional layout, and real handwritten strokes buy a great deal, and the training unit decides which corpus you want in the first place. The reconstruction pipeline and both model weights are open. The experiment can be repeated on another language with abundant documents and few labels. This is research. It is not a product specification. Paxa Labs serves Thai documents through its own [OCR API](https://paxalabs.com/ocr), which is a different model with its own published limits. ## Released artifacts - wayu-ai/wayu-paxa-ocr-zero: https://huggingface.co/wayu-ai/wayu-paxa-ocr-zero - wayu-ai/wayu-paxa-ocr-zero-gguf: https://huggingface.co/wayu-ai/wayu-paxa-ocr-zero-gguf - wayu-research/docaug: https://github.com/wayu-research/docaug - wayu-research/wayu-ocr-inference: https://github.com/wayu-research/wayu-ocr-inference - kunato/wayu-paxa-ocr-zero-demo: https://huggingface.co/spaces/kunato/wayu-paxa-ocr-zero-demo