Ref: [Main Index](https://docs.inworld.ai/llms.txt) # Inworld AI Documentation - Full Content Generated by scripts/build-llms-full.js from public MDX and shared snippets. Private, hidden and feature-gated content is excluded. Navigation orders the pages; additional public pages follow. For the curated index, see [llms.txt](https://docs.inworld.ai/llms.txt). ## Home ### Build with Inworld Source: https://docs.inworld.ai/introduction > Inworld AI developer docs for Voice and Inference. Quickstarts, guides, and API reference.

Choose a product to get started.

Products

Generate speech, transcribe audio, and build realtime voice agents with TTS, STT and the Realtime API. Run text, image, video and decision models with one API key.
To generate and speak an answer with any model in one API call, use [Voice responses](https://docs.inworld.ai/router/guides/llm-plus-tts.md). Ready to launch? See [Go to production](https://docs.inworld.ai/resources/production.md) for credentials, limits, and usage.
## Developer tools
### Build with AI Give your coding assistant the docs or connect it to live search. [Docs for AI](https://docs.inworld.ai/llms.txt) · [Connect the MCP server](https://docs.inworld.ai/developer-tools/vibe-coding.md)
### Use the CLI Use Inworld from your terminal. [Explore the CLI](https://docs.inworld.ai/developer-tools/inworld-cli.md). ```shell npm install -g @inworld/cli ```
--- ## Voice ### TTS #### TTS overview Source: https://docs.inworld.ai/tts/tts > Turn text into natural, expressive speech with Inworld TTS, and make your first request. Inworld TTS turns text into natural, expressive speech. Give your app, agent, or character a voice, narrate long content, or clone and design voices. The [Realtime TTS-2 models](https://docs.inworld.ai/tts/tts-models.md) speak 200+ languages, stream the first audio in tens of milliseconds, and let you steer emotion and delivery in natural language. Follow the steps to make your first API call. Try voices, models, and instant voice clones in the Portal. Find code examples for common use cases. **Using AI to code?** Give your assistant the docs index at `https://docs.inworld.ai/llms.txt`. For live search, [add the MCP server](https://docs.inworld.ai/developer-tools/vibe-coding). **Prefer the terminal?** Install the [Inworld CLI](https://docs.inworld.ai/developer-tools/inworld-cli.md) with `npm install -g @inworld/cli`. Use it to generate speech and audiobooks, or clone and design voices. AI agents can use it too. ## Your first request Install the SDK with `npm install @inworld/tts` or `pip install inworld-tts`. Set `INWORLD_API_KEY` to the Base64 credential from the [Portal](https://platform.inworld.ai), then generate an audio file: ```javascript JavaScript import { InworldTTS } from '@inworld/tts'; import fs from 'fs'; const tts = InworldTTS(); const audio = await tts.generate({ text: 'What a wonderful day to be a text-to-speech model!', voice: 'Ashley', }); fs.writeFileSync('output.mp3', audio); ``` ```python Python from inworld_tts import InworldTTS tts = InworldTTS() tts.generate( text="What a wonderful day to be a text-to-speech model!", voice="Ashley", output_file="output.mp3", ) ``` The [quickstart](https://docs.inworld.ai/quickstart-tts.md) explains this request step by step. It also covers streaming, which we recommend for realtime applications. ## Explore Learn how to use the Portal playground. Realtime TTS-2 and TTS-2 Flash, with a feature comparison. Voices, languages, formats, and context for natural delivery. Clone a voice from a short sample. Direct emotion, pace, and style in natural language. Choose a synchronous, streaming, WebSocket, async, or batch endpoint. Make speech expressive, fast, and ready for production. To generate text with an LLM and speak it in one API call, use [Voice responses](https://docs.inworld.ai/router/guides/llm-plus-tts.md). For a live two-way conversation, use the [Realtime API](https://docs.inworld.ai/realtime/overview.md). --- #### TTS quickstart Source: https://docs.inworld.ai/quickstart-tts > Learn how to make your first Inworld TTS API request Use the [TTS Playground](https://platform.inworld.ai/tts-playground) to try Inworld’s Text-to-Speech models, compare voices, adjust parameters, and preview instant voice clones. To build a real-time application, use the API for advanced features and integration options. This quickstart shows how to make your first Text-to-Speech API request and generate high-quality, realistic speech from text. These examples run locally or on your server. `INWORLD_API_KEY` contains the complete copied Base64 credential. Direct HTTP requests use Basic. Browser and mobile apps should use the [backend token flow](https://docs.inworld.ai/portal/ephemeral-tokens.md#connect-a-client-through-your-backend). For auth errors, see [recovery steps](https://docs.inworld.ai/portal/api-key-auth.md#recover-from-authentication-failures). ## Make your first streaming TTS API request This quickstart walks through making your first streaming API request, which we recommend for realtime, low-latency applications. For batch audio generation, pre-rendered content, and anywhere latency isn't critical, see [Make a non-streaming request](#make-a-non-streaming-request) below. Create an [Inworld account](https://platform.inworld.ai/signup). In [Inworld Portal](https://platform.inworld.ai/), open **Manage** > [**API Keys**](https://platform.inworld.ai/api-keys) and click **Generate new key**. Choose **Standard**, click **Generate**, then copy **Basic (Base64)** as-is; do not encode it again. Use a **Standard** key for direct TTS calls; Voice and Router write permissions are not needed for synthesis. API Keys page with Basic (Base64), UI-masked credentials and a hidden email API Keys page with Basic (Base64), UI-masked credentials and a hidden email Set your API key as an environment variable. ```shell macOS and Linux export INWORLD_API_KEY='your-base64-api-key-here' ``` ```shell Windows setx INWORLD_API_KEY "your-base64-api-key-here" ``` Install the TTS SDK in the language of your choice. ```bash JavaScript npm install @inworld/tts ``` ```bash Python pip install inworld-tts ``` Create a new file called `inworld_stream_quickstart.py` or `inworld_stream_quickstart.js`, confirm `INWORLD_API_KEY` is set in your environment, and copy the code below into the file. This example uses the `WAV` encoding so you can write the streamed chunks directly into a single `.wav` file. The WAV header arrives on the first chunk. The rest are raw PCM samples. ```javascript JavaScript import { InworldTTS } from '@inworld/tts'; import fs from 'fs'; const tts = InworldTTS(); const chunks = []; for await (const chunk of tts.stream({ text: "What a wonderful day to be a text-to-speech model! I'm super excited to show you how streaming works.", voice: 'Ashley', encoding: 'WAV', sampleRate: 48000, })) { chunks.push(chunk); console.log(`Received ${chunk.length} bytes`); } fs.writeFileSync('output_stream.wav', Buffer.concat(chunks)); console.log('Audio saved to output_stream.wav'); ``` ```python Python import asyncio from inworld_tts import InworldTTS async def main(): tts = InworldTTS() chunks = [] async for chunk in tts.stream( text="What a wonderful day to be a text-to-speech model! I'm super excited to show you how streaming works.", voice="Ashley", encoding="WAV", sample_rate=48000, ): chunks.append(chunk) print(f"Received {len(chunk)} bytes") with open("output_stream.wav", "wb") as f: f.write(b"".join(chunks)) print("Audio saved to output_stream.wav") asyncio.run(main()) ``` This example uses `WAV`: the first chunk carries one WAV header, and the rest carry raw PCM samples. Concatenating them directly produces a valid `.wav` file. With `LINEAR16`, every chunk is a complete WAV file, so direct concatenation produces audible clicks at chunk boundaries. Strip the per-chunk RIFF headers yourself, or use `WAV` for direct `.wav` output. `PCM` is raw headerless sample data. Use it only if your client handles containerization or playback, such as by adding a WAV header or feeding the samples to an audio API. See [Generating Audio](https://docs.inworld.ai/tts/capabilities/generating-audio.md) for the full format reference. Run the code for Python or JavaScript. The console will print out as streamed bytes are written to the audio file. ```javascript JavaScript node inworld_stream_quickstart.js ``` ```python Python python inworld_stream_quickstart.py ``` You should see a saved file called `output_stream.wav`. You can play this file with any audio player. ## Make a non-streaming request The synchronous endpoint is the simplest way to try Inworld TTS and works well for batch audio generation, pre-rendered content, and anywhere latency isn't critical. Assuming you've already set up your API key and installed the SDK: For Python or JavaScript, create a new file called `inworld_quickstart.py` or `inworld_quickstart.js`. Copy the corresponding code into the file. ```javascript JavaScript import { InworldTTS } from '@inworld/tts'; import fs from 'fs'; const tts = InworldTTS(); const audio = await tts.generate({ text: 'What a wonderful day to be a text-to-speech model!', voice: 'Ashley', }); fs.writeFileSync('output.mp3', audio); console.log('Audio saved to output.mp3'); ``` ```python Python from inworld_tts import InworldTTS tts = InworldTTS() tts.generate( text="What a wonderful day to be a text-to-speech model!", voice="Ashley", output_file="output.mp3", ) print("Audio saved to output.mp3") ``` Run the code for Python or JavaScript, or enter the curl command into your terminal. ```bash JavaScript node inworld_quickstart.js ``` ```bash Python python inworld_quickstart.py ``` You should see a saved file called `output.mp3`. You can play this file with any audio player. ## Next Steps Now that you've tried out the TTS API, you can explore more TTS capabilities. Understand the capabilities of Inworld's Realtime TTS models. Create a personalized voice clone with as little as 3 seconds of audio. The checklist for expressive, fast, production-ready TTS. --- #### TTS Playground Source: https://docs.inworld.ai/tts/tts-playground > Explore TTS models and test voices Use the TTS Playground to find a voice for your project, test text inputs, adjust voice settings, and try audio markup tags. TTS Playground with voice, language, model and delivery controls TTS Playground with voice, language, model and delivery controls ## Get Started In [Portal](https://platform.inworld.ai/), select **Text-to-Speech** in the sidebar to open TTS Playground. Enter the text you want to convert to speech. If you need some ideas, you can select one of the suggestion chips at the bottom of the screen. Note that character limits in TTS playground depend on your plan, see the [Pricing page](https://inworld.ai/pricing) for details. You can also use our API to generate longer content, see [Long Text Input](https://docs.inworld.ai/tts/capabilities/long-text-input.md). On the right-hand side, click on the voice dropdown to browse available voices. You can filter by language or search by name, and click the play button next to each voice to hear how the voice sounds. Select a voice. Click **Generate** below the text editor. The audio will automatically start playing once it's been generated. You can also download the clip to save it. ## Advanced Features For greater control over the generated audio, you can try out the following: 1. **Try a different model** - Select a different model from the right-hand side panel to see how it compares. See [Models](https://docs.inworld.ai/tts/tts-models.md) for more information about each model. 2. **Adjust configurations** - Use the controls on the right-hand side panel to adjust **Talking speed** and the options available for your selected model. Realtime TTS-2 shows **Delivery**; models that support temperature show **Temperature**. See [here](https://docs.inworld.ai/tts/capabilities/generating-audio.md#additional-configurations) for more information. 3. **Add pauses** - Try adding SSML break tags like `` to insert pauses in your speech. See [Pause Controls](https://docs.inworld.ai/tts/capabilities/pause-controls.md) for more information. ## Create a Voice In the TTS Playground, click **Create Voice** to design a new voice from a text description or clone one from audio: - **[Voice Design](https://docs.inworld.ai/tts/voice-design.md)** — Describe the voice you want in text (age, accent, tone, etc.) and get AI-generated voice candidates - **[Voice Cloning](https://docs.inworld.ai/tts/instant-voice-cloning.md)** — Clone a voice from as little as 3 seconds of audio (up to 30 seconds — longer samples improve similarity) On the Create Voice page, choose **Upload or record audio** for an instant clone, or enter a description under **Describe a voice** and click **Continue**. Create Voice page with instant and professional cloning and a voice description form Create Voice page with instant and professional cloning and a voice description form ## Multi-Voice Generation Create a multi-speaker composition — a dialogue, scene, or podcast — with a different voice per line, generated as a single stitched track. Click **Add Speaker** to add a line. Each line has its own voice, model, and settings, plus inline steering tags like `[laugh]`. Enter the text for each speaker and click **Generate All** to synthesize and stitch every line into one track. Regenerate any line individually to refine it — the combined audio updates automatically. Use the arrows on a line to compare or revert to an earlier take — restoring that line's text and settings — then play back, download, or share the result. ## Audio Generation History Open the History panel using the button next to **Generate** at the bottom of the playground. Every clip you generate is automatically saved there, capturing the voice, model, and text used. From the History panel you can: - **Replay any past generation instantly** — cached audio plays without re-running the model - **Reload settings into the playground** — send a past entry back to populate the text, voice, model, and all generation parameters in one click - **Search and filter** by text content, voice, model, or date - **Bulk manage entries** — enter Manage mode to multi-select for download or deletion - **Download individual takes** as MP3 with one click History is stored locally per device and per workspace — it isn't synced across browsers or shared between teammates. Clearing browser storage will reset it. ## Next Steps Ready for more? Whether you're looking to clone a voice, design one from text, or start building with our API, we've got you covered. Create a voice from a text description—no audio needed. Create a personalized voice clone with as little as 3 seconds of audio. The checklist for expressive, fast, production-ready TTS. Learn how to make your first API call in minutes. --- #### TTS models Source: https://docs.inworld.ai/tts/tts-models Choose an Inworld TTS model based on your use case, quality needs, and performance requirements. ### Our flagship, top-ranked model — the best choice for production - Best quality and steerability, with natural language steering for speech that fits its context - Support for 200+ languages and locales - Ultra-low latency (100 ms TTFB\*) at high concurrency - High quality instant voice cloning - Enhanced timestamps with phonetic details and visemes ### Our fastest, most cost-efficient model — built for latency-critical, high-volume workloads - Our lowest latency — 20 ms TTFB\*, 5× faster than `inworld-tts-2` - Lowest cost per character - Same 200+ languages and locales as `inworld-tts-2` - High quality instant voice cloning \* P90 time to first audio byte, measured server-side — excludes network latency. ### Models overview | **Name** | **Model ID** | **Description** | **Supported languages** | | :---- | :---- | :---- | :---- | | **Realtime TTS-2** | `inworld-tts-2`               | Our most powerful model with natural language steering and the strongest multilingual capabilities — the best choice for production quality | 200+ languages and locales — see [Languages](https://docs.inworld.ai/tts/capabilities/multilingual.md) | | **Realtime TTS-2 Flash** | `inworld-tts-2-flash`               | Our fastest, most cost-efficient model — significantly lower latency than `inworld-tts-2`, ideal for real-time agents and high-volume workloads | 200+ languages and locales — see [Languages](https://docs.inworld.ai/tts/capabilities/multilingual.md) | **Using an earlier model?** Previous-generation models (`inworld-tts-1`, `inworld-tts-1-max`, `inworld-tts-1.5-max`, `inworld-tts-1.5-mini`) are deprecated and not recommended for new projects. `inworld-tts-1` and `inworld-tts-1-max` were discontinued on **June 15, 2026**. Requests to those two models are automatically routed to newer models. If you still use any of these earlier models, we recommend the TTS-2 family for better quality, latency, and cost. ## Feature comparison | **Feature** | **Realtime TTS-2** | **Realtime TTS-2 Flash** | | :---- | :---- | :---- | | Best for                 | Production workloads that need the best quality and steerability | Latency-critical, high-volume, and cost-sensitive workloads | | Quality                 | Top-ranked flagship — best quality and steerability | High quality at the lowest latency and cost | | Latency (TTFB) \*                 | 100 ms | 20 ms | | [Instant voice cloning](https://docs.inworld.ai/tts/instant-voice-cloning.md)                 | | | | [Professional voice cloning](https://docs.inworld.ai/tts/professional-voice-cloning.md) Beta                 | | | | [Inline pronunciation](https://docs.inworld.ai/tts/capabilities/custom-pronunciation.md)                 | | | | [Multilingual](https://docs.inworld.ai/tts/capabilities/multilingual.md)                 | 200+ languages | 200+ languages | | [Steering](https://docs.inworld.ai/tts/capabilities/steering.md)                 | | | | [Pause controls](https://docs.inworld.ai/tts/capabilities/pause-controls.md)                 | | | | [Timestamp alignment](https://docs.inworld.ai/tts/capabilities/timestamps.md)                 | | | | [Zero data retention](https://docs.inworld.ai/portal/zero-data-retention.md#tts)                 | | | \* P90 time to first audio byte, measured server-side — excludes network latency. --- #### Capabilities ##### Generating audio Source: https://docs.inworld.ai/tts/capabilities/generating-audio ## Voices Inworld's built-in voices cover a range of languages, vocal characteristics, and styles. Try them in TTS Playground or use them in your applications. For greater customization, we recommend [voice cloning](https://docs.inworld.ai/tts/instant-voice-cloning.md). Create distinct, personalized voices tailored to your experience, with as little as 3 seconds of audio. Regardless of model, a voice delivers the best speaker similarity when synthesizing text in its native language — the language it was cloned in. For synthesizing other languages, see [Cross-lingual support](https://docs.inworld.ai/tts/capabilities/multilingual.md#cross-lingual-support). ## Language Support The Realtime TTS-2 models support 200+ languages and locales. See [Languages](https://docs.inworld.ai/tts/capabilities/multilingual.md) for the full list and tips for synthesizing multilingual text. ## Supported Formats Multiple audio formats are available via API to support different application requirements. The default is MP3. - **MP3:** Popular compressed format with broad device and platform compatibility. - Sample rate: 16kHz - 48kHz - Bit rates: 32kbps - 320kbps - **PCM (`PCM`):** Raw uncompressed 16-bit signed little-endian samples with no WAV header. Recommended for WebSocket use cases and real-time applications that process raw audio samples directly without needing container metadata. - Sample rate: 8kHz - 48kHz - Bit depth: 16-bit - **WAV (`WAV`):** Uncompressed 16-bit signed little-endian samples with WAV header optimized for HTTP streaming. For non-streaming, the WAV header is included in the response. For HTTP streaming, only the first audio chunk includes a WAV header. You can concatenate all chunks in that response directly into a single valid WAV file. For WebSocket streaming, the first audio chunk of each `flush`/`flush_completed` event includes a WAV header. Direct concatenation without processing is valid only within a single flush. To build one continuous WAV file across multiple flushes, strip or rebuild the repeated headers between flushes. - Sample rate: 8kHz - 48kHz - Bit depth: 16-bit - **Linear PCM (`LINEAR16`):** Uncompressed 16-bit signed little-endian samples with WAV header. Maintained for backward compatibility. For non-streaming, the WAV header is included in the response. For streaming (HTTP streaming or WebSocket), the WAV header is included in every audio chunk, so each chunk is a valid WAV file on its own. Clients must strip headers when concatenating chunks. - Sample rate: 8kHz - 48kHz - Bit depth: 16-bit - **Opus:** High-quality compressed format optimized for low latency web and mobile applications. - Sample rate: 8kHz - 48kHz - Bit rates: 32kbps - 192kbps - **μ-law:** Compressed telephony format ideal for voice applications with bandwidth constraints. - Sample rate: 8kHz - **A-law:** Compressed telephony format ideal for voice applications with bandwidth constraints. - Sample rate: 8kHz ## Additional Configurations Use these optional settings to control speech synthesis: - **Language**: Language the voice should speak the text in. If a localized voice prompt exists for the language, it will be used. If not specified, the original voice prompt will be used and the language will be auto-detected from the input text. - **Delivery mode**: Only supported by `inworld-tts-2`. Controls how varied the model outputs can be. `STABLE` produces more reliable, predictable speech — best when the output must match the input exactly. `CREATIVE` produces more varied speech with greater emotional range — great for more creative use cases. Defaults to `BALANCED`. - **Temperature**: Not supported by `inworld-tts-2`. Higher values increase variation, which can produce more diverse outputs with desirable outcomes, but also increases the chances of bad generations and hallucinations. Lower values improve stability and speaker similarity, though going too low increases the chances of broken generation. The default is 1.0. - **Talking Speed**: Controls how fast the voice speaks. 1.0 is the normal native speed, while 0.5 is half the normal speed and 1.5 is 1.5x faster than the normal speed. - **Synthesis context**: Supply the text of previous requests from the same session or conversation in the [`synthesisContext`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-synthesis-context) field. This gives the model additional context, especially for short phrases where preceding turns help determine the language, meaning, and situation. Realtime TTS-2 is trained to condition on the audio of prior conversational turns — the model hears the difference and adjusts how it speaks based on how it was spoken to. This feature is available via the [Realtime API](https://docs.inworld.ai/realtime/overview.md). For more information, please reach out to [support@inworld.ai](mailto:support@inworld.ai). --- ##### Language support Source: https://docs.inworld.ai/tts/capabilities/multilingual > Inworld TTS built for multilingual applications. Synthesize speech in many languages with the same voice. The Realtime TTS-2 models — `inworld-tts-2` and `inworld-tts-2-flash` — share the same language support. For multilingual applications, `inworld-tts-2` is the most capable choice, with the most accurate pronunciation and intonation; `inworld-tts-2-flash` offers the same language coverage at the lowest latency and cost. ## Supported languages The Realtime TTS-2 models accept **623 language and locale codes** (BCP-47) in the [`language`](#specifying-a-language) field, covering **470 languages** — from broad languages like `es` (Spanish) to regional locales like `es-MX` (Mexican Spanish). They fall into three tiers: Tier 1 and Tier 2 cover 94 languages (241 codes), and Tier 3 covers the other 376 (382 codes). [Timestamp alignment](https://docs.inworld.ai/tts/capabilities/timestamps.md) is available in every language the model supports. ### Tier 1 Tier 1 languages are the most extensively evaluated, offering the highest, most consistent quality and the most natural pronunciation. The **Default locale** is the regional code a short code is treated as (see [Language codes and default locales](#language-codes-and-default-locales)): | Language | Code | Default locale | | --- | --- | --- | | Arabic | `ar` | `ar-SA` | | Chinese | `zh` | `zh-CN` | | Dutch | `nl` | `nl-NL` | | English | `en` | `en-US` | | French | `fr` | `fr-FR` | | German | `de` | `de-DE` | | Hebrew | `he` | `he-IL` | | Hindi | `hi` | `hi-IN` | | Italian | `it` | `it-IT` | | Japanese | `ja` | `ja-JP` | | Korean | `ko` | `ko-KR` | | Polish | `pl` | `pl-PL` | | Portuguese | `pt` | `pt-BR` | | Russian | `ru` | `ru-RU` | | Spanish | `es` | `es-ES` | ### Tier 2 Tier 2 languages are fully supported for synthesis, but results can vary more by language and voice than in Tier 1. Try your target language in the [TTS Playground](https://platform.inworld.ai/tts). For best results, [design](https://docs.inworld.ai/tts/voice-design.md) or [clone a voice](https://docs.inworld.ai/tts/instant-voice-cloning.md) in that language. | Language | Code \* | Default locale | | --- | --- | --- | | Afrikaans | `af` | `af-ZA` | | Albanian | `sq` | `sq-AL` | | Amharic | `am` | `am-ET` | | Armenian | `hy` | `hy-AM` | | Assamese | `as` | `as-IN` | | Azerbaijani | `az` | `az-AZ` | | Basque | `eu` | `eu-ES` | | Belarusian | `be` | `be-BY` | | Bengali | `bn` | `bn-IN` | | Bulgarian | `bg` | `bg-BG` | | Burmese | `my` | `my-MM` | | Cantonese | `yue` | `yue-HK` | | Catalan | `ca` | — | | Cebuano | `ceb` | `ceb-PH` | | Croatian | `hr` | `hr-HR` | | Czech | `cs` | `cs-CZ` | | Danish | `da` | `da-DK` | | Eastern Yiddish | `ydd` | — | | Egyptian Arabic | `arz` | — | | Estonian | `et` | `et-EE` | | Filipino | `fil` | `fil-PH` | | Finnish | `fi` | — | | Galician | `gl` | `gl-ES` | | Garhwali | `gbm` | — | | Georgian | `ka` | `ka-GE` | | Greek | `el` | `el-GR` | | Gujarati | `gu` | `gu-IN` | | Gulf Arabic | `afb` | — | | Haitian Creole | `ht` | `ht-HT` | | Hijazi Arabic | `acw` | — | | Hungarian | `hu` | `hu-HU` | | Icelandic | `is` | `is-IS` | | Indonesian | `id` | `id-ID` | | Javanese | `jv` | `jv-ID` | | Kannada | `kn` | `kn-IN` | | Kazakh | `kk` | `kk-KZ` | | Konkani | `kok` | `kok-IN` | | Lao | `lo` | `lo-LA` | | Latvian | `lv` | `lv-LV` | | Libyan Arabic | `ayl` | — | | Lithuanian | `lt` | `lt-LT` | | Luxembourgish | `lb` | `lb-LU` | | Macedonian | `mk` | `mk-MK` | | Maithili | `mai` | `mai-IN` | | Malagasy | `mg` | `mg-MG` | | Malay | `ms` | `ms-MY` | | Malayalam | `ml` | `ml-IN` | | Marathi | `mr` | `mr-IN` | | Mongolian | `mn` | `mn-MN` | | Najdi Arabic | `ars` | — | | Nepali | `ne` | `ne-NP` | | Northern Uzbek | `uzn` | — | | Norwegian Bokmål | `nb` | `nb-NO` | | Norwegian Nynorsk | `nn` | `nn-NO` | | Odia | `or` | `or-IN` | | Omani Arabic | `acx` | — | | Pashto | `ps` | `ps-AF` | | Persian | `fa` | `fa-IR` | | Piedmontese | `pms` | — | | Punjabi | `pa` | `pa-IN` | | Romanian | `ro` | `ro-RO` | | Serbian | `sr` | `sr-RS` | | Sindhi | `sd` | — | | Sinhala | `si` | — | | Slovak | `sk` | `sk-SK` | | Slovenian | `sl` | `sl-SI` | | Swahili | `sw` | `sw-KE` | | Swedish | `sv` | `sv-SE` | | Tamil | `ta` | `ta-IN` | | Telugu | `te` | `te-IN` | | Thai | `th` | `th-TH` | | Tunisian Arabic | `aeb` | — | | Turkish | `tr` | `tr-TR` | | Ukrainian | `uk` | `uk-UA` | | Urdu | `ur` | `ur-PK` | | Uzbek | `uz` | `uz-UZ` | | Vietnamese | `vi` | `vi-VN` | | Võro | `vro` | — | | Welsh | `cy` | — | \* Codes are base language subtags (ISO 639-1, or ISO 639-3 for less common languages). A dash (—) means the code has no default locale and is used as is. ### Tier 3 Tier 3 covers every other language the `language` field accepts. These languages are not evaluated: the model makes a best-effort attempt, and quality can vary widely by language and voice, including pronunciation that doesn't sound native. Text normalization isn't applied, so write numbers, dates, and currency amounts out as words. Test your target language in the [TTS Playground](https://platform.inworld.ai/tts) before you rely on it, and [design](https://docs.inworld.ai/tts/voice-design.md) or [clone a voice](https://docs.inworld.ai/tts/instant-voice-cloning.md) in that language for the best results. | Language | Code | Default locale | | --- | --- | --- | | Abadi | `kbt` | — | | Abron | `abr` | — | | Abua | `abn` | — | | Afade | `aal` | — | | Agwagwune | `yay` | — | | Akebu | `keu` | — | | Alago | `ala` | — | | Algerian Arabic | `arq` | — | | Algerian Saharan Arabic | `aao` | — | | Ambonese Malay | `abs` | — | | Anaang | `anw` | — | | Angika | `anp` | — | | Antankarana Malagasy | `xmv` | — | | Arbereshe Albanian | `aae` | — | | Ashe | `ahs` | — | | Askopan | `eiv` | — | | Asturian | `ast` | — | | Avaric | `av` | — | | Awak | `awo` | — | | Bacama | `bcy` | — | | Bade | `bde` | — | | Bagirmi Fulfulde | `fui` | — | | Bago-Kusuntu | `bqg` | — | | Baharna Arabic | `abv` | — | | Balanta-Ganja | `bjt` | — | | Bangwinji | `bsj` | — | | Banjar | `bjn` | — | | Bara Malagasy | `bhr` | — | | Barok | `bjk` | — | | Basa | `bzw` | — | | Bashkir | `ba` | — | | Batak Mandailing | `btm` | — | | Bavarian | `bar` | — | | Bayot | `bda` | — | | Betawi | `bew` | — | | Bhili | `bhb` | — | | Bhojpuri | `bho` | — | | Bilur | `bxf` | — | | Bima | `bhp` | — | | Bodo | `brx` | `brx-IN` | | Boghom | `bux` | — | | Bokyi | `bky` | — | | Bomu | `bmq` | — | | Bondei | `bou` | — | | Borgu Fulfulde | `fue` | — | | Bosnian | `bs` | — | | Brahui | `brh` | — | | Braj | `bra` | — | | Buduma | `bdm` | — | | Buginese | `bug` | — | | Bukharic | `bhh` | — | | Bundeli | `bns` | — | | Bura-Pabir | `bwr` | — | | Burak | `bys` | — | | Cacaloxtepec Mixtec | `miu` | — | | Cakfem-Mushere | `cky` | — | | Campidanese Sardinian | `sro` | — | | Cen | `cen` | — | | Central Nahuatl | `nhn` | — | | Central Pame | `pbs` | — | | Central Pashto | `pst` | — | | Central-Eastern Niger Fulfulde | `fuq` | — | | Chadian Arabic | `shu` | — | | Chhattisgarhi | `hne` | `hne-IN` | | Chichicapan Zapotec | `zpv` | — | | Chiga | `cgg` | — | | Chimalapa Zoque | `zoh` | — | | Chimborazo Highland Quichua | `qug` | — | | Chitwania Tharu | `the` | — | | Chuvash | `cv` | — | | Cibak | `ckl` | — | | Coastal Konjo | `kjc` | — | | Cornish | `kw` | — | | Cross River Mbembe | `mfn` | — | | Cuyamecalco Mixtec | `xtu` | — | | Dadiya | `dbd` | — | | Dazaga | `dzg` | — | | Deccan | `dcc` | — | | Degema | `deg` | — | | Dera | `kna` | — | | Dghwede | `dgh` | — | | Dhofari Arabic | `adf` | — | | Dijim-Bwilim | `cfa` | — | | Dogri | `doi` | `doi-IN` | | Dotyali | `dty` | — | | Duya | `ldb` | — | | Eastern Bolivian Guarani | `gui` | — | | Eastern Egyptian Bedawi Arabic | `avl` | — | | Eastern Krahn | `kqo` | — | | Eastern Mari | `mhr` | — | | Eggon | `ego` | — | | Ejagham | `etu` | — | | Eleme | `elm` | — | | Eloyi | `afo` | — | | Embu | `ebu` | — | | Esan | `ish` | — | | Esperanto | `eo` | — | | Extremaduran | `ext` | — | | Fanti | `fat` | — | | Farefare | `gur` | — | | Filomena Mata-Coahuitlan Totonac | `tlp` | — | | Fipa | `fip` | — | | Gambian Wolof | `wof` | — | | Gbagyi | `gbr` | — | | Gbari | `gby` | — | | Geser-Gorom | `ges` | — | | Gheg Albanian | `aln` | — | | Glavda | `glw` | — | | Goan Konkani | `gom` | — | | Goemai | `ank` | — | | Gola | `gol` | — | | Guduf-Gava | `gdf` | — | | Guerrero Amuzgo | `amu` | — | | Gusii | `guz` | — | | Gusilay | `gsl` | — | | Gweno | `gwe` | — | | Gyaazi | `gyz` | — | | Güilá Zapotec | `ztu` | — | | Hadothi | `hoj` | — | | Hahon | `hah` | — | | Hako | `hao` | — | | Halia | `hla` | — | | Hawaiian | `haw` | — | | Herero | `her` | — | | Highland Konjo | `kjk` | — | | Huaxcaleca Nahuatl | `nhq` | — | | Huba | `hbb` | — | | Huitepec Mixtec | `mxs` | — | | Hula | `hul` | — | | Hunjara-Kaina Ke | `hkk` | — | | Hwana | `hwo` | — | | Idakho-Isukha-Tiriki | `ida` | — | | Ido | `io` | — | | Idoma | `idu` | — | | Igo | `ahl` | — | | Ikposo | `kpo` | — | | Ikwere | `ikw` | — | | Ilocano | `ilo` | — | | Interlingua | `ia` | — | | Interlingue | `ie` | — | | Irish | `ga` | — | | Isekiri | `its` | — | | Isoko | `iso` | — | | Ito | `itw` | — | | Itza | `itz` | — | | Ixtayutla Mixtec | `vmj` | — | | Izon | `ijc` | — | | Jambi Malay | `jax` | — | | Jaunsari | `jns` | — | | Jiba | `juo` | — | | Jju | `kaj` | — | | Judeo-Moroccan Arabic | `aju` | — | | Juxtlahuaca Mixtec | `vmc` | — | | Kabras | `lkb` | — | | Kachi Koli | `gjk` | — | | Kairak | `ckr` | — | | Kalabari | `ijn` | — | | Kamo | `kcq` | — | | Kanauji | `bjj` | — | | Kanembu | `kbl` | — | | Karekare | `kai` | — | | Kashmiri | `ks` | `ks-IN` | | Kathoriya Tharu | `tkt` | — | | Keiyo | `eyo` | — | | Khana | `ogo` | — | | Khmer | `km` | — | | Kinga | `zga` | — | | Kinnauri | `kfk` | — | | Kirya-Konzel | `fkk` | — | | Kochila Tharu | `thq` | — | | Kohumono | `bcs` | — | | Kok Borok | `trp` | — | | Kol | `kol` | — | | Koma | `kmy` | — | | Konkani (individual language) | `knn` | — | | Konzo | `koo` | — | | Korwa | `kfp` | — | | Kota | `kfe` | — | | Kuanua | `ksd` | — | | Kuanyama | `kua` | — | | Kui | `uki` | — | | Kulung | `bbu` | — | | Kuot | `kto` | — | | Kurdish | `ku` | — | | Kushi | `kuh` | — | | Kwambi | `kwm` | — | | Kyrgyz | `ky` | — | | Lala-Roba | `lla` | — | | Lamang | `hia` | — | | Langi | `lag` | — | | Larike-Wakasihu | `alo` | — | | Latin | `la` | `la-VA` | | Lezghian | `lez` | — | | Liana-Seti | `ste` | — | | Liberia Kpelle | `xpe` | — | | Liberian English | `lir` | — | | Ligurian | `lij` | — | | Lijili | `mgi` | — | | Logooli | `rag` | — | | Logudorese Sardinian | `src` | — | | Lojban | `jbo` | — | | Loloda | `loa` | — | | Longuda | `lnu` | — | | Low German | `nds` | — | | Loxicha Zapotec | `ztp` | — | | Lushai | `lus` | — | | Maasina Fulfulde | `ffm` | — | | Maba | `mde` | — | | Mafa | `maf` | — | | Mali | `gcc` | — | | Malinaltepec Me'phaa | `tcf` | — | | Maltese | `mt` | — | | Mandara | `tbf` | — | | Mandjak | `mfv` | — | | Manggarai | `mqy` | — | | Manipuri | `mni` | — | | Mansoanka | `msw` | — | | Marghi Central | `mrt` | — | | Marghi South | `mfm` | — | | Maria | `mrr` | — | | Masikoro Malagasy | `msh` | — | | Mazaltepec Zapotec | `zpy` | — | | Mazatlan Mazatec | `vmz` | — | | Mazatlan Mixe | `mzl` | — | | Mbe | `mfo` | — | | Mekeo | `mek` | — | | Meru | `mer` | — | | Mesopotamian Arabic | `acm` | — | | Mewari | `mtr` | — | | Mingrelian | `xmf` | — | | Mirandese | `mwl` | — | | Mitlatongo Mixtec | `vmm` | — | | Miya | `mkf` | — | | Mom Jango | `ver` | — | | Moroccan Arabic | `ary` | — | | Motu | `meu` | — | | Musi | `mui` | — | | Naba | `mne` | — | | Nalik | `nal` | — | | Ndonga | `ndo` | — | | Neapolitan | `nap` | — | | Ngamo | `nbh` | — | | Ngas | `anc` | — | | Ngizim | `ngi` | — | | Nigerian Fulfulde | `fuv` | — | | Nimadi | `noe` | — | | Nobiin | `fia` | — | | North Levantine Arabic | `apc` | — | | North Mesopotamian Arabic | `ayp` | — | | North Moluccan Malay | `max` | — | | Northern Betsimisaraka Malagasy | `bmm` | — | | Northern Hindko | `hno` | — | | Northern Pame | `pmq` | — | | Northern Pashto | `pbu` | — | | Norwegian | `no` | — | | Notsi | `ncf` | — | | Nyankpa | `yes` | — | | Nzanyi | `nja` | — | | Occitan | `oc` | — | | Od | `odk` | — | | Odual | `odu` | — | | Omie | `aom` | — | | Orma | `orc` | — | | Pahari-Potwari | `phr` | — | | Papuan Malay | `pmy` | — | | Pero | `pip` | — | | Petats | `pex` | — | | Piya-Kwonci | `piy` | — | | Plateau Malagasy | `plt` | — | | Pokoot | `pko` | — | | Poqomam | `poc` | — | | Pulaar | `fuc` | — | | Pular | `fuf` | — | | Qaqet | `byx` | — | | Quiotepec Chinantec | `chq` | — | | Rana Tharu | `thr` | — | | Rapoisi | `kyx` | — | | Ratahan | `rth` | — | | Rayon Zoque | `zor` | — | | Rombo | `rof` | — | | Rotokas | `roo` | — | | Sacapulteco | `quv` | — | | Saidi Arabic | `aec` | — | | Sakalava Malagasy | `skg` | — | | Sakha | `sah` | — | | Saleman | `sau` | — | | Samba Daka | `ccg` | — | | Samba Leko | `ndi` | — | | San Felipe Otlaltepec Popoloca | `pow` | — | | San Francisco Del Mar Huave | `hue` | — | | San Juan Atzingo Popoloca | `poe` | — | | San Martin Itunyoso Triqui | `trq` | — | | San Miguel El Grande Mixtec | `mig` | — | | Sanskrit | `sa` | `sa-IN` | | Santa Catarina Albarradas Zapotec | `ztn` | — | | Santali | `sat` | — | | Saposa | `sps` | — | | Saya | `say` | — | | Siar-Lak | `sjr` | — | | Sibe | `nco` | — | | Sicilian | `scn` | — | | Sikkimese | `sip` | — | | Sinaugoro | `snc` | — | | Sinicahua Mixtec | `xti` | — | | Sipacapense | `qum` | — | | Siwai | `siw` | — | | Solos | `sol` | — | | Soninke | `snk` | — | | Southeastern Nochixtlan Mixtec | `mxy` | — | | Southern Betsimisaraka Malagasy | `bzc` | — | | Southern Pashto | `pbt` | — | | Soyaltepec Mazatec | `vmp` | — | | Sudanese Arabic | `apd` | — | | Sulka | `sua` | — | | Sundanese | `su` | — | | Tae | `rob` | — | | Tahaggart Tamahaq | `thv` | — | | Tajik | `tg` | — | | Tandroy-Mahafaly Malagasy | `tdx` | — | | Tangale | `tan` | — | | Tanosy Malagasy | `txy` | — | | Tarok | `yer` | — | | Tatar | `tt` | — | | Tedaga | `tuq` | — | | Teop | `tio` | — | | Tepinapa Chinantec | `cte` | — | | Tera | `ttr` | — | | Terei | `buo` | — | | Termanu | `twu` | — | | Tesaka Malagasy | `tkg` | — | | Tetelcingo Nahuatl | `nhg` | — | | Tibetan | `bo` | — | | Tidaa Mixtec | `mtx` | — | | Tidore | `tvo` | — | | Tigak | `tgc` | — | | Tilquiapan Zapotec | `zts` | — | | Tinputz | `tpz` | — | | Tlacoapa Me'phaa | `tpl` | — | | Tlacoatzintepec Chinantec | `ctl` | — | | Tomoip | `tqp` | — | | Tondano | `tdn` | — | | Tonsea | `txs` | — | | Tooro | `ttj` | — | | Torau | `ttu` | — | | Tsimihety Malagasy | `xmw` | — | | Tsotso | `lto` | — | | Tugen | `tuy` | — | | Tula | `tul` | — | | Tulu | `tcy` | — | | Tungag | `lcm` | — | | Turkana | `tuv` | — | | Tututepec Mixtec | `mtu` | — | | Ubaghara | `byc` | — | | Vai | `vai` | — | | Volapük | `vo` | — | | Waci Gbe | `wci` | — | | Wadiyara Koli | `kxp` | — | | Waja | `wja` | — | | Wanga | `lwg` | — | | Wapan | `juk` | — | | Waray | `war` | — | | Warji | `wji` | — | | Wemale | `weo` | — | | Western Frisian | `fy` | — | | Western Juxtlahuaca Mixtec | `jmx` | — | | Western Maninkakan | `mlq` | — | | Western Niger Fulfulde | `fuh` | — | | Western Panjabi | `pnb` | — | | Wu Chinese | `wuu` | — | | Xanaguia Zapotec | `ztg` | — | | Yace | `ekr` | — | | Yalahatan | `jal` | — | | Yekhee | `ets` | — | | Yiddish | `yi` | — | | Yutanduchi Mixtec | `mab` | — | | Zarma | `dje` | — | A dash (—) means the code has no default locale and is used as is. ### Language codes and default locales The `language` field takes any code from the tables on this page: - **A short code is treated as its default locale.** `en` is treated as `en-US`, `es` as `es-ES` (European Spanish), `pt` as `pt-BR`, `zh` as `zh-CN`, and `ar` as `ar-SA`; see the **Default locale** column for each language. To target another variety, pass its regional code, such as `en-GB`, `es-MX`, `es-419` (Latin American Spanish), or `pt-PT`. A code without a default locale, such as `fi` or `cy`, is used as is. - **An unlisted region falls back to the language's default.** `en-ZW` is treated as `en-US`, and `es-US` as `es-ES`. One listed code also falls back: `en-UG` uses `en-GB`. - **Case doesn't matter in a language-region code**, and `_` works in place of `-`: `en_gb` is the same as `en-GB`. - **Any other code is rejected** with a 400 error. To let the model detect the language instead, omit `language`. Languages with more than one regional or accent code. Every other language accepts its code, plus its default locale where its tier's table shows one. | Language | Regional and accent codes | | --- | --- | | Arabic | `ar-SA` Saudi (default), `ar-EG` Egyptian, `ar-AE` Emirati, `ar-SY` Syrian, `ar-LB` Lebanese, `ar-IQ` Iraqi, `ar-JO` Jordanian, `ar-YE` Yemeni, `ar-KW` Kuwaiti, `ar-MA` Moroccan, `ar-TN` Tunisian, `ar-PS` Palestinian, `ar-DZ` Algerian, `ar-BH` Bahraini, `ar-SD` Sudanese, `ar-001` Modern Standard, `ar-QA` Qatari, `ar-OM` Omani, `ar-LY` Libyan, `ar-MR` Mauritanian | | Bengali | `bn-IN` Indian (default), `bn-BD` Bangladeshi | | Chinese | `zh-CN` Mandarin (Simplified) (default), `zh-TW` Mandarin (Traditional), `zh-MY` Malaysian, `zh-SG` Singaporean | | Dutch | `nl-NL` Dutch (default), `nl-BE` Belgian | | English | `en-US` American (default), `en-GB` British, `en-AU` Australian, `en-IN` Indian, `en-SG` Singaporean, `en-CA` Canadian, `en-NG` Nigerian, `en-PH` Philippine, `en-IE` Irish, `en-ZA` South African, `en-NZ` New Zealand, `en-KE` Kenyan, `en-MY` Malaysian, `en-ID` Indonesian, `en-JM` Jamaican, `en-HK` Hong Kong, `en-SC` Seychellois, `en-GB-u-sd-gbwls` Welsh, `en-029` Caribbean, `en-southern` Southern American, `en-scottish` Scottish, `en-UG` Ugandan (uses `en-GB`) | | French | `fr-FR` French (default), `fr-BE` Belgian, `fr-CA` Canadian | | Portuguese | `pt-BR` Brazilian (default), `pt-PT` European | | Punjabi | `pa-IN` Indian (default), `pa-PK` Pakistani | | Spanish | `es-ES` European (default), `es-MX` Mexican, `es-419` Latin American, `es-AR` Argentine, `es-PE` Peruvian, `es-CO` Colombian, `es-CL` Chilean, `es-VE` Venezuelan, `es-PR` Puerto Rican, `es-BO` Bolivian, `es-CR` Costa Rican, `es-CU` Cuban, `es-DO` Dominican, `es-EC` Ecuadorian, `es-GT` Guatemalan, `es-HN` Honduran, `es-NI` Nicaraguan, `es-PA` Panamanian, `es-PY` Paraguayan, `es-SV` Salvadoran, `es-UY` Uruguayan | | Urdu | `ur-PK` Pakistani (default), `ur-IN` Indian | **Accents and regional variants:** A voice reproduces the specific accent it was [cloned](https://docs.inworld.ai/tts/instant-voice-cloning.md) in (e.g., a voice cloned in British English speaks `en-GB` natively). If your voice wasn't cloned in the accent you need, pass the matching [BCP-47](https://www.rfc-editor.org/info/bcp47) regional code in the [`language`](#specifying-a-language) field (e.g., `en-GB`, `es-MX`, `pt-BR`) to steer it. See [Specifying a language](#specifying-a-language) below. Text normalization is language-specific, not locale-specific — English is normalized the same way whether you target `en-US` or `en-GB`, even though conventions for dates, currencies, and the like can differ. If you need locale-specific formatting, normalize that text yourself. ## Cross-lingual support A voice delivers the best speaker similarity when it speaks the language it was [cloned](https://docs.inworld.ai/tts/instant-voice-cloning.md) in — its native tongue. The Realtime TTS-2 models support cross-lingual synthesis. When synthesizing a different language, the model makes a best-effort attempt to speak the target language natively, without carrying over the accent of the voice's original language. If you want the voice to keep its original accent, prompt for it explicitly (e.g., with [steering](https://docs.inworld.ai/tts/capabilities/steering.md) on `inworld-tts-2`). For the most natural, consistent cross-lingual results, we recommend [specifying the language](#specifying-a-language) to synthesize in and [localizing the voice](#voice-localization). ### Specifying a language Use the `language` field to tell the model which language (as specified by a BCP-47 language tag) the voice should speak the text in. When set, the service will: - Apply text normalization (if enabled) for the target language (e.g., speaking numbers in the target language). - Use the voice's localized prompt for that language, if one exists. If no localized prompt is available, the model will be steered to speak in the target language. When `language` is omitted, the original voice prompt is used and the language for normalization (if enabled) is auto-detected from the input text. When a text mixes languages and you need each part spoken in a specific language, including a switch in the middle of a sentence, use [language tags](https://docs.inworld.ai/tts/capabilities/language-tags.md) instead: `perro`. If you are sending short requests with only numbers or dates (e.g., `123-456-7890`), auto-detection may not have enough context to detect the right language. In those cases, we recommend specifying a language for most consistent results. If you can't, pass the text of earlier requests from the same conversation in the [`synthesisContext`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-synthesis-context) field: when the current text is too short to detect on its own, auto-detection uses that context. A `language` you set always takes precedence over it. A voice's native (prompt) language shapes how it sounds in other languages — speaker similarity is always highest in the voice's native tongue. For accent-free, native delivery in another language, [localize the voice](#voice-localization). ### Voice localization Voice localization adapts a voice to a target language so it sounds like a native speaker of that language — delivering fluent, natural speech without carrying over the accent of the voice's original language. (By contrast, [specifying a language](#specifying-a-language) on an un-localized voice may retain the original accent.) It is supported for all Inworld TTS models. In [Inworld Portal](https://platform.inworld.ai/), go to **Voices** and select the **My voices** tab. Hover over an English voice to open its details panel on the right. Voice localization is currently only supported for voices where the original audio was in English. Support for additional languages is coming soon. With a voice selected, click **Localize Voice** in the right panel. ![Voices page with Localize Voice action](https://static.inworld.ai/portal-doc/img/tts/voices-localize.99250b3e.png) Pick a target language from the dropdown and click **Localize**, which will start generating localized prompt candidates. This may take up to 2 minutes. ![Localize Voice screen with target language dropdown](https://static.inworld.ai/portal-doc/img/tts/localize-voice-select-language.fe40566c.png) After generation, you'll see a few localized prompt candidates. Listen to each and pick the one that sounds most natural and native, then click **Save**. If none sound right, you can click **Regenerate**. You may want to change the script before regenerating, since the script influences the generated voice. ![Review candidates screen with multiple localized voice options](https://static.inworld.ai/portal-doc/img/tts/localize-voice-review-candidates.6ec93b45.png) Once saved, the voice will use your localized prompt when you specify the `language` to match the language of your localized prompt. Try it out in TTS Playground or via API (the voice ID remains the same). ![TTS Playground generating speech with a localized voice](https://static.inworld.ai/portal-doc/img/tts/localized-voice-generation.306e6116.png) ## Next steps Clone a voice once and reuse it across languages. Use IPA notation to control pronunciation of proper nouns and edge cases. --- ##### Instant voice cloning Source: https://docs.inworld.ai/tts/instant-voice-cloning > Create a personalized voice clone in seconds Inworld's Instant Voice Cloning creates a personalized voice clone in seconds, from as little as 3 seconds of audio. The longer the sample (up to the 30-second limit), the better the speaker similarity. Also known as zero-shot cloning, it's available to all users through Portal or via API. The 30-second prompt limit applies to voices cloned on or after August 15, 2026. Voices cloned before that date continue to use their original 15-second prompt — re-clone a voice with a longer sample to take advantage of the higher limit. For a voice clone that more closely matches the original speaker and is more stable, see [Professional Voice Cloning](https://docs.inworld.ai/tts/professional-voice-cloning.md) Beta. It uses more audio and an asynchronous training step. Don't have audio samples? Use [Voice Design](https://docs.inworld.ai/tts/voice-design.md) to create a voice from a text description instead. You can also clone a voice in one command with the [Inworld CLI](https://docs.inworld.ai/developer-tools/inworld-cli.md): `inworld tts clone sample.wav --name "My Voice"` Choose one of two ways to use Instant Voice Cloning: 1. Through [**Inworld Portal**](https://platform.inworld.ai) - Open [**Text-to-Speech**](https://platform.inworld.ai/tts-playground) > **Create Voice** > **Upload or record audio** and follow the guided flow. 2. Via [API](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/clone-voice.md) or [SDK](#clone-a-voice-via-api) - Useful if you want to clone a lot of voices or expose this capability to your users. ## Clone a Voice in Portal In Portal, select **Text-to-Speech** in the sidebar. Click **Create Voice**, then **Upload or record audio** under **Instant Voice Clone**. Name your voice and select the language, which should match the audio samples. Select `Other` if you're cloning in a [Tier 2 language](https://docs.inworld.ai/tts/capabilities/multilingual.md#tier-2). Instant Voice Clone form with voice details, audio upload and consent controls Instant Voice Clone form with voice details, audio upload and consent controls You can either upload or record audio: - **Upload**: Drag and drop or browse to upload 1 audio file. Accepted formats: wav, mp3, webm. Maximum file size is 12MB. Audio samples longer than 30 seconds will be automatically trimmed to 30 seconds. - **Record**: Click "Record audio" and record your audio. You can use the suggested scripts to help guide your recording, or use your own script. For best results, record in a quiet place to minimize background noise, avoid mic noise, and speak with a variety of emotions to capture the full range of the voice. Enable "Remove background noise" if you wish to remove background noise from your audio. Confirm you have the rights to clone the voice, then click "Continue". Check out our [Voice Cloning Best Practices](https://docs.inworld.ai/tts/best-practices/voice-cloning.md#best-practices-for-instant-voice-cloning) for helpful tips and tricks to improve the quality of your voice clones. ![](https://static.inworld.ai/portal-doc/img/tts/try_cloned_voice.915dc7d0.png) Once voice cloning completes, you'll see the "Try your cloned voice" interface. Enter text in the input field and press play to hear your cloned voice. You can test different phrases to ensure the voice sounds as expected. If the voice doesn't sound quite right, you can delete the voice and start over, create another voice, or test it in the TTS Playground for more advanced testing options. The number of cloned voices you can store depends on your subscription plan. See the [Pricing page](https://inworld.ai/pricing) for per-plan limits. ![](https://static.inworld.ai/portal-doc/img/portal/tts-playground-copy.a7b73759.png) To use the cloned voice via API, copy the voice ID for your cloned voice in TTS Playground. Use that value for the `voiceId` when making an API call. See our [Quickstart](https://docs.inworld.ai/quickstart-tts.md) to learn how to make your first API call. Instant voice cloning may not perform well for less common voices, such as children's voices or unique accents. For those use cases, we recommend [Professional Voice Cloning](https://docs.inworld.ai/tts/professional-voice-cloning.md). ## Clone a Voice via API If you want to automate voice cloning (for example, to support creator onboarding at scale), use the Voice Cloning API. - **API reference**: [Clone a voice](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/clone-voice.md) - **Node.js SDK**: [`cloneVoice()`](https://docs.inworld.ai/tts/node-sdk.md#clonevoiceoptions) — clone a voice in a few lines with the `@inworld/tts` SDK - **Python example**: [example_voice_clone.py](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/python/example_voice_clone.py) - **JavaScript example**: [example_voice_clone.js](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/js/example_voice_clone.js) Voice cloning has lower rate limits than regular speech synthesis. For details, see [Rate limits](https://docs.inworld.ai/resources/rate-limits.md). ## Next Steps Looking for more tips and tricks? Check out the resources below to get started\! Get a more similar, stable voice clone using a larger set of audio samples. Learn best practices for producing high-quality voice clones. Learn best practices for synthesizing high-quality speech. Explore Python and JavaScript code examples for TTS integration. --- ##### Professional voice cloning Source: https://docs.inworld.ai/tts/professional-voice-cloning > Fine-tune a highly similar, stable voice clone from a larger set of audio samples Professional Voice Cloning is currently in beta. Professional Voice Cloning produces a clone that more closely matches the original speaker and is more stable than [Instant Voice Cloning](https://docs.inworld.ai/tts/instant-voice-cloning.md). It requires more audio and an asynchronous training step, so the result is not instant. To get started, create a Professional Voice Clone through [**Inworld Portal**](https://platform.inworld.ai). Open [**Text-to-Speech**](https://platform.inworld.ai/tts-playground) > **Create Voice** > **Start professional clone** and follow the guided flow. You can also create a Professional Voice Clone via the [**PVC API**](#create-a-professional-voice-clone-via-api) if you want to automate cloning or expose it to your own users. ## Create a Professional Voice Clone in Portal In Portal, select **Text-to-Speech** in the sidebar. In the TTS Playground, click **Create Voice**. Create Voice link in TTS Playground Create Voice link in TTS Playground On the Create Voice page, click **Start professional clone** under **Professional Voice Clone**. Professional Voice Clone card on the Create Voice page Professional Voice Clone card on the Create Voice page ![](https://static.inworld.ai/portal-doc/img/tts/professional-clone-form.f586470e.png) Enter a **Voice name**. Description and tags are optional. Multi-language support for Professional Voice Cloning is experimental. English (`en-US`) is fully supported. ![](https://static.inworld.ai/portal-doc/img/tts/professional-clone-start-training.11009abb.png) Click **Add Audio** to upload your audio samples. - You can upload up to **50 audio samples** per voice, up to a combined limit of 1 GB (1,000,000,000 bytes). - Uploaded audio must total at least 10 minutes combined. More clean, high-quality audio generally produces a better clone. Enable **Remove background noise** if you wish to remove background noise from your audio. Confirm you have the rights to clone the voice, then click **Start Training**. Check out our [Voice Cloning Best Practices](https://docs.inworld.ai/tts/best-practices/voice-cloning.md#best-practices-for-professional-voice-cloning) for tips on recording and preparing audio for a professional voice clone. ![](https://static.inworld.ai/portal-doc/img/tts/professional-clone-training-started.eed61938.png) Once training starts, you can leave the page — training continues in the background and can take several minutes for longer samples. ![](https://static.inworld.ai/portal-doc/img/tts/professional-clone-voices-list.1a83654f.png) Training status is displayed on the Voices page, where your request appears under **Professional clone requests** along with its current status. ![](https://static.inworld.ai/portal-doc/img/tts/professional-clone-my-voices-ready.927bed1a.png) Once training finishes, the voice moves to **My voices**, ready to use in the TTS Playground or via the TTS API. To use it via the TTS API, copy the voice ID and use that value for the `voiceId` when making an API call. See our [Quickstart](https://docs.inworld.ai/quickstart-tts.md) to learn how to make your first TTS API call. ## Create a Professional Voice Clone via API If you want to automate professional voice cloning, use the PVC API. Unlike Instant Voice Cloning, it's a multi-step, asynchronous flow: create a draft voice, upload audio samples, then train. [Create a PVC voice](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/create-pvc-voice.md) with a `displayName`. It starts in `PVC_VOICE_STATE_DRAFT`. [Upload PVC voice samples](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/upload-pvc-voice-samples.md), repeating until you have at least 10 minutes of cumulative audio. A voice holds up to **50 samples** totalling **1 GB** (1,000,000,000 bytes). Each request carries at most **10 files**, so larger sets need several calls. Optionally [trim](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/trim-pvc-voice-sample.md) individual samples first. [Train the voice](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/train-pvc-voice.md). This queues training and returns immediately — poll [Get a PVC voice](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/get-pvc-voice.md) to watch `state` move from `PVC_VOICE_STATE_QUEUED` to `PVC_VOICE_STATE_TRAINING` to `PVC_VOICE_STATE_READY`. Once `state` is `PVC_VOICE_STATE_READY`, the `customVoice` field names the trained voice. Find its `voiceId` with [List voices](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/list-voices.md), and use it anywhere you'd use a regular voice, e.g. [Synthesize speech](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md). See our [Quickstart](https://docs.inworld.ai/quickstart-tts.md) to learn how to make your first TTS API call. Check out our [Voice Cloning Best Practices](https://docs.inworld.ai/tts/best-practices/voice-cloning.md#best-practices-for-professional-voice-cloning) for tips on recording and preparing audio for a professional voice clone. Every voice — including failed or unfinished attempts, which are draft voices — counts toward your plan's PVC voice slots. Delete the draft and failed attempts you don't need with [Delete a PVC voice](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/delete-pvc-voice.md) to free their slots. A trained voice frees its slot only when you delete the voice itself — see below. ## Plan limits Two limits apply to Professional Voice Cloning, both set by your subscription plan: - **Voice slots** — how many professional voice clones you can hold at once. - **New trainings** — how often you can start a training run. | Plan | Voice slots | New trainings | | --- | --- | --- | | On-Demand | 2 | 1 per day | | Creator | 4 | 2 per day | | Builder | 10 | 4 per day | | Developer | 25 | 12 per day | | Growth | 40 | 1 per hour | | Enterprise | 100, extendable with add-ons | 4 per hour | On-Demand accounts also need a payment method on file before they can create a professional voice clone. Slots are held, not consumed: a slot stays occupied for as long as the voice exists, and is released when you delete the voice. Every voice counts, including drafts and failed attempts. A run of failed training jobs can fill your slots even though none of them produced a usable voice. Use [List PVC voices](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/list-pvc-voices.md) to find them and [Delete a PVC voice](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/delete-pvc-voice.md) to free their slots. A trained voice can't be deleted with the PVC API; to free its slot, delete the voice itself — see [Retrain or remove a trained voice](https://docs.inworld.ai/tts/professional-voice-cloning.md#retrain-or-remove-a-trained-voice). ### Checking your limits programmatically Rather than hard-coding the slot numbers above, read them at runtime from [Resolve upload limits](https://docs.inworld.ai/api-reference/pvcAPI/pvcvoiceservice/resolve-pvc-upload-limits.md), which reports your plan's slot ceiling alongside live usage: ```bash curl -s 'https://api.inworld.ai/voices/v1/pvcVoices:resolveUploadLimits' \ -H "Authorization: Basic $INWORLD_API_KEY" ``` `usedPvcVoiceSlots` and `maxPvcVoiceSlots` tell you where you stand on slots. Creating a voice when all slots are taken is refused until you free one or upgrade. The training rate is **not** returned by this endpoint — the table above is the reference for it. Starting a training run above your plan's rate returns `429`, and the error message names the limit you hit; wait for the window to reset rather than retrying in a loop. ## Retrain or remove a trained voice Once a PVC voice reaches `PVC_VOICE_STATE_READY`, it's final: you can't edit its samples, train it again, or delete it with the PVC API. Those requests return `409`. - **To remove a trained voice** and free its slot, delete the voice itself — from the voice library in Portal, or with [Delete a voice](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/delete-voice.md) using its `voiceId`. - **To retrain with different audio**, create a new PVC voice and train that. If you're at your slot limit, delete the old trained voice first. ## Next Steps Looking for more tips and tricks? Check out the resources below to get started\! Clone a voice in seconds with just a few seconds of audio. Learn best practices for producing high-quality professional voice clones. Automate professional voice cloning with the PVC API. --- ##### Voice design Source: https://docs.inworld.ai/tts/voice-design > Create a custom voice from a text description Create a new voice from a text description with Inworld's Voice Design. Use it when the Voice Library does not have the voice you need and you have no recordings for voice cloning. Voice Design uses a model to generate a voice based on the following two inputs: 1. **Voice description** - A text description of the voice you have in mind (e.g., age, gender, accent, tone, pitch). 2. **Script** - The text the voice will speak. This shapes the generated voice, so using a script that matches the intended voice produces the best results. When you design a voice to save to your library, we'll return up to three voice previews so you can listen, compare, and select the ones that work best for your project. You can also design a voice from your terminal with the [Inworld CLI](https://docs.inworld.ai/developer-tools/inworld-cli.md): `inworld tts design "A warm, deep narrator voice"` runs an interactive wizard that generates previews, lets you listen and pick, then publishes. To design and save a reusable voice, choose one of these methods: 1. Through [**Inworld Portal**](https://platform.inworld.ai) - Open [**Text-to-Speech**](https://platform.inworld.ai/tts-playground) > **Create Voice**, enter a description under **Describe a voice**, and click **Continue**. Follow the guided flow. 2. Via [API](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/design-voice.md) or [SDK](#design-a-voice-with-the-sdk) - Useful if you want to generate a lot of voices or expose this capability to your users. ## Ad-hoc voice design (Preview) **Preview.** Ad-hoc voice design is a preview feature and may be refined before it is marked stable. See [release stages](https://docs.inworld.ai/portal/support.md). Generate speech directly from a voice description by passing `voiceDesign.designPrompt` instead of `voiceId`. The response contains synthesized audio, without creating or saving a voice in your library. ```bash curl --request POST 'https://api.inworld.ai/tts/v1/voice' \ --header "Authorization: Basic $INWORLD_API_KEY" \ --header 'Content-Type: application/json' \ --data '{ "text": "Welcome aboard. Our journey begins at sunrise.", "modelId": "inworld-tts-2", "voiceDesign": { "designPrompt": "A warm, resonant male narrator with a clear American accent and calm delivery." }, "audioConfig": { "audioEncoding": "MP3" } }' ``` The unary response contains base64-encoded audio in `audioContent`. The voice description controls how the voice sounds; `text` contains the words it speaks. - Use `inworld-tts-2` and provide a description of 7–1,024 characters. Leading and trailing whitespace does not count toward the minimum. - Set either `voiceId` or `voiceDesign`, not both. - Each request designs its own voice. Repeating the same description does not guarantee the same voice across requests. To reuse a voice, design and publish it through the [Voices API](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/design-voice.md), then synthesize with its `voiceId`. - Audio settings, text normalization, timestamps, and text limits follow the selected synthesis endpoint. See [language support](https://docs.inworld.ai/tts/capabilities/multilingual.md) and [timestamps](https://docs.inworld.ai/tts/capabilities/timestamps.md) for their supported languages and options. | Synthesis method | Where to supply the description | | --- | --- | | [Unary](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md) | `voiceDesign.designPrompt` in the request body | | [Server-streaming](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md) | `voiceDesign.designPrompt` in the request body | | [Async](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-async.md) | `voiceDesign.designPrompt` in the request body; poll the operation for audio results | | [Batch](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-batch.md) | `items[].request.voiceDesign.designPrompt`; each item designs independently, and a batch can mix descriptions with catalog voice IDs | Ad-hoc voice design is not supported by the WebSocket or OpenAI-compatible synthesis endpoints. ## Design a Voice in Portal In [Portal](https://platform.inworld.ai/), select [**Text-to-Speech**](https://platform.inworld.ai/tts-playground) in the sidebar. Click **Create Voice**. Describe a voice form on the Create Voice page Describe a voice form on the Create Voice page Under **Describe a voice**, enter your description and click **Continue**. Voice Design has two modes, switchable with the tabs at the top: - **Freeform** - Describe the voice in plain English and let us build the voice profile for you. The easiest option and the best starting point. - **Structured** - Work on the structured voice profile itself — the exact prompt the model receives, one attribute per line. Describe the voice you want to create, in English, up to 1,000 characters — including the character, if you have one in mind. Descriptions under 30 characters rarely produce a useful voice. The preset chips (e.g. **Agent**, **Narrator**, **Companion**) give you a starting point to edit. Keep your description concise but specific, so the model can most accurately produce what you have in mind. A good voice description should include: - **Gender and age range** (e.g., "a mid-20s to early 30s female voice", "a middle-aged male voice") - **Pitch and pace** (e.g., "low-pitched", "fast-paced", "steady pace") - **Tone and emotion** (e.g., "warm and friendly", "authoritative and composed") - **Timbre** (e.g., "rich and smooth", "slightly raspy", "clear and bright") **Example**: "A middle-aged male voice with a clear British accent speaking at a steady pace and with a neutral tone." Select a **Language** and **Accent** below the description — they apply to the generated voice and its preview script. You can generate directly from your description, or click **Review structured profile** to see the full voice profile we build from it and fine-tune any attribute before generating. In the **Structured** tab you edit the exact voice profile the model receives, one attribute per line: dialect, gender, age, emotion, tone, pitch, volume, speed, clarity, fluency, personality, texture, and environment. Edit any value, add your own lines, or start from a preset. The **dialect** line carries the language and accent. **Clear** resets the profile to the blank template. This mode is built for speed and repeatability: once you have a voice profile that works, you can tweak individual attributes or paste in profiles generated elsewhere. Click **Generate voice**, which will create up to 3 voice previews. Listen to each preview by clicking the play button, then select the voice(s) you want to keep. Each generation produces slightly different results. If the first set of voices doesn't sound right, click **Generate voice** again to regenerate, or adjust your description or voice profile to better match what you have in mind before regenerating. Check out our [Voice Design Best Practices](https://docs.inworld.ai/tts/best-practices/voice-design.md) guide for helpful tips and tricks to improve your designed voices. After selecting one or more voices, give each voice a name, add optional tags, and save them to your voice library. Your designed voices will appear alongside your other voices in the TTS Playground. To use your designed voice via API, copy the voice ID from the TTS Playground. Use that value for the `voiceId` when making an API call. See our [Quickstart](https://docs.inworld.ai/quickstart-tts.md) to learn how to make your first API call. ## Design a Voice with the SDK You can also design voices programmatically using [`designVoice()`](https://docs.inworld.ai/tts/node-sdk.md#designvoiceoptions) / [`design_voice()`](https://docs.inworld.ai/tts/python-sdk.md#design_voiceoptions) and [`publishVoice()`](https://docs.inworld.ai/tts/node-sdk.md#publishvoiceoptions) / [`publish_voice()`](https://docs.inworld.ai/tts/python-sdk.md#publish_voiceoptions) from the Inworld TTS SDK. ```javascript JavaScript (SDK) import { InworldTTS } from '@inworld/tts'; const tts = InworldTTS(); // reads INWORLD_API_KEY from env const result = await tts.designVoice({ designPrompt: 'A warm, friendly female voice with a slight British accent', previewText: 'Hello! Welcome to our application.', numberOfSamples: 3, }); // Listen to previews, then publish const voice = await tts.publishVoice({ voice: result.previewVoices[0].voiceId, displayName: 'My Designed Voice', }); ``` ```python Python (SDK) from inworld_tts import InworldTTS tts = InworldTTS() # reads INWORLD_API_KEY from env result = tts.design_voice( design_prompt="A warm, friendly female voice with a slight British accent", preview_text="Hello! Welcome to our application.", number_of_samples=3, ) # Listen to previews, then publish voice = tts.publish_voice( voice=result.preview_voices[0].voice_id, display_name="My Designed Voice", ) ``` ## Voice Design API Reference And Examples If you want to automate voice design (for example, to support creator onboarding at scale), use the Voice Design API. - **Design a voice API reference**: [Design a voice](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/design-voice.md) - **Publish a voice API reference**: [Publish a voice](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/publish-voice.md) - **Python example**: [example_voice_design_publish.py](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/python/example_voice_design_publish.py) - **JavaScript example**: [example_voice_design_publish.js](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/js/example_voice_design_publish.js) ## Next Steps Learn best practices for designing voices. Clone an existing voice with as little as 3 seconds of audio. Learn how to make your first TTS API call in minutes. --- ##### Voice sharing Source: https://docs.inworld.ai/tts/voice-sharing > Publish your custom voices to the community Voice Library, and discover voices shared by others Publish a custom voice (cloned or designed) to the Voice Library to make it publicly available to all Inworld users. Use the library to share your voices and find voices published by others. To move a voice privately to a specific workspace, see [Voice Export](https://docs.inworld.ai/tts/voice-export.md). You always remain the owner of a voice you share. Publishing makes it discoverable to others. They can generate audio with it but cannot edit it, re-clone it, or take ownership. You can remove it from the library at any time. By publishing, you grant Inworld a license to host and distribute the voice; see the [Voice Library Addendum](https://inworld.ai/voice-library-addendum). ## Publish a voice to the Voice Library In [Portal](https://platform.inworld.ai/), go to [**Voice Library**](https://platform.inworld.ai/voice-library) and select one of your custom voices to open its details panel. Only custom voices you own can be published. Click **Publish to Voice Library**. Review the voice's details (name, description, tags) — these are shown publicly and are **locked after publishing**. Make sure they're accurate and free of anything you wouldn't want public before continuing. Confirm that you have the right to share the voice and agree to the [Voice Library Addendum](https://inworld.ai/voice-library-addendum). Click **Publish to Voice Library**. Now the voice is published and is discoverable to all users. After a voice is published, its **display name, description, and tags can no longer be edited**. Other users and applications may be relying on them. If you need to change these details, contact [support@inworld.ai](mailto:support@inworld.ai). ## Remove a voice from the Voice Library You can unpublish a voice at any time from the same details panel (**Remove from Voice Library**). To avoid disrupting projects that already depend on it, removal follows a **2-year notice period**: - The voice stays available in the library for the duration of the notice period (but new users will no longer see the voice in the Voice Library UI). - After the notice period elapses, the voice is fully removed from the library. It remains in your own workspace as a private custom voice. If you change your mind during the notice period, you can click **Cancel Removal**. ## Discover and use community voices Shared voices show up in the **Voice Library** in Portal alongside Inworld and your own voices. You can **star** a voice to save it, and use the **Source** filter to narrow the list to Inworld, your own, or community voices. ### Using community voices via the API Community voices are fully usable through the public Voice and TTS APIs. The one thing to know up front: a shared voice has **two IDs**. | ID | Looks like | Who sees it | | --- | --- | --- | | **Private voice ID** | `{workspace}__{voice}` | The owner only | | **Public (community) voice ID** | `community-…` | Everyone — use this to browse, fetch, and synthesize | When you browse the library as a non-owner, each voice is returned with its public `community-…` ID (the owner's workspace is never exposed). Owners see their original resource name, with the public ID available on the voice's `sharing.publicVoiceId` field. ### Browse the community library By default, [List voices](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/list-voices.md) returns your workspace's own voices plus Inworld system voices — **community voices are not included**. To browse the community library, add the filter `community = "true"`: ```bash curl --get 'https://api.inworld.ai/voices/v1/voices' \ --header "Authorization: Basic $INWORLD_API_KEY" \ --data-urlencode 'filter=community = "true"' ``` --- ##### Voice export Source: https://docs.inworld.ai/tts/voice-export > Copy a custom voice into another workspace, or hand it off to another user, using a copy token Use a copy token to copy one of your custom voices into another workspace. Move it between your own workspaces, or share the token with specific people so they can copy the voice into theirs. Looking to share a voice publicly with all Inworld users instead? See [Voice Sharing](https://docs.inworld.ai/tts/voice-sharing.md). Voice Export is a private transfer to specific people and workspaces; Voice Sharing publishes the voice to the community Voice Library. ## Export a voice In [Portal](https://platform.inworld.ai/), select [**Voices**](https://platform.inworld.ai/voice-library) in the sidebar and select one of your custom voices under the **My voices** tab to open its details panel. Click **Export Voice**. Enter the email addresses of the people who can redeem the token. Include your own email to copy the voice into another of your workspaces. Press Enter, comma, or space after each email to add it, or paste a list of addresses. Click **Create Copy Token**, then copy the token and send it to the people you listed. They redeem it from their own workspace via **Import Voice** (see below). - The token expires **7 days** after creation. - Only the emails listed on the token can redeem it. - Each redemption creates an **independent copy** — later changes to your original voice are not reflected in copies. - Treat copy tokens like secrets: send them directly to the people you listed rather than posting them anywhere public. Even if a token leaks, only the listed emails can redeem it, and it expires on its own after 7 days. ## Import a voice In the destination workspace, open **Voices** in the sidebar and click **Import Voice** (next to **Create Voice**). Paste the copy token the voice owner shared with you. Optionally enter a **Voice name** — it defaults to the original voice's name. Click **Copy Voice**. A **Voice copied to your workspace** notification confirms success, and the **Edit Voice** window opens in case you want to edit the voice's metadata — display name, description, tags, gender, age, and category. The voice is now your own private, independent voice with its own voice ID, ready to use in the TTS Playground or via the API. ## Export and import via API Use [Issue a voice copy token](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/issue-copy-token.md) with the source workspace's API key, then [Redeem a voice copy token](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/redeem-copy-token.md) with the destination workspace's API key. Both keys need voice write permission. The API authorizes destination workspace IDs rather than user emails. Its tokens are separate from the email-based Portal tokens described above. Each redemption creates an independent private copy, and tokens expire after seven days. --- ##### Steering Source: https://docs.inworld.ai/tts/capabilities/steering > Direct the performance of any voice using natural language instructions. Steering lets you direct how a voice speaks with natural-language instructions. Control articulation, pitch, speed, volume, and other aspects of delivery to make speech sound more natural and expressive. Think of it as giving direction to a voice actor. Wrap your instructions in square brackets and place them before the text they apply to. No markup languages or numeric parameters required. - Steering is supported only on `inworld-tts-2`. On `inworld-tts-2-flash`, steering is not supported — instruction tags are ignored, though [non-verbal](#non-verbals) tags like `[laugh]` work as usual. For steering, use `inworld-tts-2`. - Steering instructions must be written in English, even when the input text is in another language. ## How instructions work **A `[tag]` applies from where you write it until you change it.** This is the same convention as a stage direction in a screenplay. Once *(shouting)* appears, every line after it is shouted until a new direction says otherwise. A `[tag]` behaves the same way: it takes effect at the point you write it and stays in force for the rest of the text. To end a styled passage, write `[reset]`. To switch to a different style, just write the new tag. | You write | How it sounds | | :---- | :---- | | `[shouting] We need to leave now! Do you understand me?` | Both sentences shouted. | | `[shouting] We need to leave now! [reset] Do you understand me?` | First sentence shouted, second delivered normally. | | `[shouting] Go! [whisper] Quietly now.` | First shouted, then a whisper. | | `We need to leave now! [shouting] Do you understand me?` | First sentence normal, second shouted. | | `[reset] Do you understand me?` | Normal — clears an inline instruction set earlier in the same WebSocket context. | ### What does not change the instruction An instruction stays in force until you change it. None of the following ends it: - **The end of a sentence or a paragraph break.** Instructions are not scoped to a sentence. - **Very long input text.** Long input is internally split for synthesis, and every part inherits the instruction in force. - **A `` pause.** See [Pause Controls](https://docs.inworld.ai/tts/capabilities/pause-controls.md) — a pause is a pause, and never alters delivery. - **A non-verbal tag.** `[laugh]` inserts a sound; it does not clear a style. Only another `[tag]` or `[reset]` changes the instruction. ### Ending an instruction with `[reset]` `[reset]` removes the active instruction for the rest of the text. > [say slowly with a trembling voice] I don't think we're alone in here. [reset] Anyway, the report is on your desk. A few details worth knowing: - **`[reset]` is never spoken.** Like every instruction tag, it is removed from the text before synthesis. - **It is case-insensitive.** `[Reset]` and `[RESET]` work the same as `[reset]`. - **It removes the instruction, not the voice.** A voice whose own character is angry or breathy stays that way after a reset. `[reset]` returns the voice to itself, not to a flat, emotionless read — which is exactly why it is not called `[neutral]`. - **It is not final.** A later tag takes over and persists in turn: `[reset] One. [whisper] Two. Three.` speaks the first sentence normally and whispers the other two. - **In a run of adjacent tags, the last instruction wins.** `[whisper] [reset] Hi.` clears the instruction; `[reset] [whisper] Hi.` whispers. Non-verbal tags in the run are unaffected: `[reset] [laugh] Hi.` clears the instruction and keeps the laugh. ## Free-form turn instructions Describe the full character of a delivery in natural language, like a director coaching an actor before a take. A single instruction can capture emotion, energy, pacing, and intent all at once. The more fully you describe the delivery — layering mood, physical manner, and intent — the more precisely the voice will perform. > [speak as if barely holding back rage forcing every word through gritted teeth] I have told you. Repeatedly. And you STILL didn't listen. > [overwhelmed with excitement and barely able to contain yourself] We just hit a million users. I still can't believe it — we actually did it! > [slow and hushed with every word weighted by grief] I got the call this morning. He's gone. ## Metadata-based instructions Single-property instructions that target one aspect of delivery at a time. The examples below are starting points — feel free to experiment with your own natural language phrasing. - **Articulation:** How words are shaped and delivered — add force, crispness, or deliberate rhythm. *Examples:* `[say with force]` `[articulate clearly]` `[say with deliberate pauses]` > [articulate clearly] Each step must be followed in order. Do not skip ahead. - **Intonation:** Controls how pitch moves through a phrase — whether it lands decisively or stays open-ended. *Examples:* `[say with a falling pitch]` `[say with a rising pitch]` > [say with a falling pitch] That's my final answer. I'm not changing my mind. - **Volume:** The overall amplitude of the voice, from barely audible to a full, room-filling projection. *Examples:* `[very loud]` `[very quiet]` > [very quiet] Don't make a sound. There's someone right outside the door. - **Pitch:** The baseline register of the voice — lower for gravity and weight, higher for energy and presence. *Examples:* `[say in a low tone]` `[say in a high pitch]` > [say in a high pitch] We just got the green light, the product launches tomorrow! - **Range:** How much pitch varies across the utterance — flat for monotony, expressive for warmth or playfulness. *Examples:* `[say playfully]` `[say with no pitch variation]` > [say playfully] So anyway, I was telling her about the trip, and she just laughed the whole time. - **Speed:** The speed of delivery — faster for urgency and tension, slower for weight and clarity. *Examples:* `[very fast]` `[very slow]` > [very fast] Run, they're right behind us, don't stop, keep moving! - **Vocal style:** Changes how the voice itself sounds — shifting from normal speech into a different mode like whispering or singing. *Examples:* `[sing joyfully]` `[whisper in a hushed style]` `[give a nasal quality]` > [sing joyfully] The sun is rising and the world feels new, everything I dreamed is finally coming true. ## Non-verbals Insert organic, human sounds at any point in the text to add realism. > [clear throat] If I could have everyone's attention, please. > I told him what happened, and he just [laugh] couldn't believe it! Non-verbal tags and instruction tags share the same `[bracket]` syntax but behave in opposite ways: | | Non-verbal tag | Instruction tag | | :---- | :---- | :---- | | **Example** | `[laugh]` | `[shouting]` | | **What it does** | Produces a sound at that point | Changes how the surrounding words are spoken | | **How long it lasts** | One occurrence | Until you change it or `[reset]` | | **Where to put it** | Exactly where the sound belongs | Before the text it applies to | Only the list below determines whether a bracketed tag is a non-verbal or an instruction. A tag that names a recognized sound is a non-verbal. **Anything else in brackets is treated as an instruction.** ### Recognized sounds The most reliable non-verbals — and the ones to reach for first — are `[laugh]` `[breathe]` `[clear throat]` `[sigh]` `[cough]` `[yawn]`. The full set of recognized sounds is below. Matching ignores case, spacing, and punctuation, and accepts common inflections, so `[Clear Throat]`, `[clear_throat]`, `[clears throat]`, and `[throat clearing]` all resolve to the same sound. | Category | Tags | | :---- | :---- | | **Breath and effort** | `[breathe]` `[sigh]` `[gasp]` `[pant]` `[huff]` `[grunt]` `[groan]` `[moan]` | | **Laughter** | `[laugh]` `[chuckle]` `[giggle]` `[cackle]` `[snort]` `[scoff]` | | **Distress** | `[cry]` `[sob]` `[wail]` `[whimper]` `[whine]` `[sniffle]` `[sniff]` `[shriek]` `[squeal]` `[howl]` | | **Throat and chest** | `[clear throat]` `[cough]` `[sneeze]` `[hiccup]` `[yawn]` `[burp]` `[snore]` `[choke]` `[gag]` `[swallow]` `[gulp]` `[spit]` | | **Mouth** | `[tongue click]` `[mouth click]` `[mouth sound]` `[lip smack]` `[kiss]` `[shush]` `[raspberry]` `[whistle]` `[bleh]` | | **Eating and drinking** | `[chew]` `[slurp]` | | **Other** | `[babble]` `[beatbox]` `[growl]` | `[shout]`, `[scream]`, `[sing]`, `[hum]`, and `[mumble]` describe *how words are spoken*, so they are instructions, not sounds. Like any instruction, they persist until you change them — `[shout] Get down! Everyone stay calm.` shouts both sentences. Use `[reset]` if you only want the first one shouted. ## Emphasis Capitalize letters within your input text to draw attention to specific words or syllables. Fully capitalizing a word stresses the entire word, while capitalizing individual letters within a word emphasizes a specific syllable. > I told you NOT to open that door. > Are you seriously asking if I want pizza? AbsoLUTEly I do. ## Common gotchas An instruction is not scoped to a sentence — it applies from where you write it until you change it. Write `[reset]` at the point where normal delivery should resume, or write a new tag there. *Instead of:* `[whisper] Don't move. They're still out there. It's clear now, we can go.` *Write:* `[whisper] Don't move. They're still out there. [reset] It's clear now, we can go.` A later inline tag replaces the active instruction from the point where it appears, and the earlier instruction does not come back. For example, `[whisper] One. [shout] Two. Three.` whispers the first sentence and shouts the other two. It should not. `[reset]` removes the *instruction* you gave, not the character of the voice itself — a voice that is inherently warm, gruff, or excitable keeps that character. If a voice sounds flat after a reset, the expressiveness you were hearing came from the instruction, so use a lighter instruction rather than clearing it entirely. Check the model. Steering is supported only on `inworld-tts-2`. `inworld-tts-2-flash` ignores instruction tags entirely (non-verbals like `[laugh]` still work). Older models may read them aloud. Also check the spelling of non-verbals — an unrecognized sound name is treated as an instruction instead of producing a sound. It should not, and a `` never clears an instruction. If the delivery changed across a pause, the cause is elsewhere in the text — most often a second `[tag]` after the break. ## Best practices **Write instructions in English.** Steering instructions must be in English regardless of the language of the input text. For best results, avoid capital letters and punctuation in your instructions. For example, `[say quietly in a low tone with deliberate pauses] Bonjour, je suis ravi de vous rencontrer.` **Use free-form descriptive instructions for maximum control.** The more you describe how you want the voice to perform, the better the output. A bare tag like `[sad]` gives the model one dimension to work with. A fuller instruction like `[say sadly with deliberate pauses in a low voice and hushed style]` combines mood, rhythm, pitch, and mode — producing a more nuanced and convincing performance. **Avoid conflicting instructions.** Combining opposing directions, for example `[whisper in a hushed style]` and `[very loud]` in the same tag, produces unpredictable results. Use one clear instruction per tag. **Match the instruction to the text.** The content being spoken should be consistent with the delivery style. A mismatch like `[say sadly]` applied to `This is the happiest day of my life, I just landed my dream job and fell in love!` sends contradictory signals and may degrade output quality. **Change the instruction only where the delivery should change.** Because a tag persists, one tag at the start is enough for a passage that keeps a single delivery. Add a tag mid-text when the performance shifts, and `[reset]` when it should return to normal. Re-tagging every sentence gives the model less continuity to work with and can make delivery uneven. **Use pause controls for longer pauses.** Use [pause controls](https://docs.inworld.ai/tts/capabilities/pause-controls.md) if you want to add longer pauses for added emphasis. --- ##### Voice tags Source: https://docs.inworld.ai/tts/capabilities/voice-tags Voice tags are free-form labels attached to each voice for filtering, grouping, and finding voices. Use them to describe vocal style, voice quality, and use cases. Tags are **not** where gender, age group, or categories go. Those are separate structured fields with fixed enums (`gender`, `ageGroup`, `categories`) — set them via [UpdateVoice](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/update-voice.md) after cloning. Putting `"female"` or `"adult"` in `tags` will not populate those fields. ## Understanding voice tags Each voice includes a `tags` array with free-form labels such as: - **Vocal style**: `energetic`, `calm`, `professional`, `friendly`, `warm` - **Voice quality**: `smooth`, `clear`, `expressive`, `conversational` - **Use-case hints**: `narrator`, `announcer`, `character` Structured metadata (set via [UpdateVoice](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/update-voice.md), not `tags`): - **`gender`**: `male`, `female`, `neutral` - **`ageGroup`**: `young`, `middle_aged`, `elderly` - **`categories`**: `companions`, `developer_assistants`, `education_training`, `enterprise`, `healthcare`, `interactive_media` ## Adding voice tags You can add custom tags to a voice when cloning it in the [TTS Playground](https://platform.inworld.ai/tts-playground): 1. In the TTS Playground, click **Create Voice**, then **Upload or record audio** under **Instant Voice Clone**. 2. Enter a name for your voice, then add tags — press Enter after each tag to add it to the list. ![Voice tags](https://static.inworld.ai/portal-doc/img/tts/voice_tags.bef3e352.svg) 3. Upload or record your audio sample and complete the cloning flow. See [Voice Cloning](https://docs.inworld.ai/tts/instant-voice-cloning.md) for the full walkthrough. 4. Your new voice will appear in your voice list with the assigned tags. ![Saved voice tags](https://static.inworld.ai/portal-doc/img/tts/saved_voice_tags.c301eb4f.svg) ## Using voice tags Voice tags are returned in the [List voices in a workspace](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/list-voices.md) endpoint response, so you can filter and select voices programmatically: ```json { "voices": [ { "voiceId": "my_workspace__my_custom_voice", "displayName": "My Custom Voice", "description": "A warm narrator voice", "promptLanguages": ["en-US"], "tags": ["warm", "expressive", "narrator"], "gender": "female", "ageGroup": "young", "categories": ["companions"], "source": "IVC" } ], "totalSize": 1, "nextPageToken": "" } ``` --- ##### Pause controls Source: https://docs.inworld.ai/tts/capabilities/pause-controls Use **SSML break tags** to control pacing by inserting silence where you need it. ## SSML break tags *Use when you need precise control over silence duration and position.* You can insert silences at specific points in the generated speech. The TTS API and Inworld Portal support SSML `` in text input for streaming, non-streaming, and WebSocket requests, in all languages. You can specify silences in milliseconds or seconds. For example, `` and `` produce the same result. **Constraints:** - Use well-formed SSML: specify the slash and brackets—for example, ``. - Tag names and attributes are **case insensitive**; for example, `` works. - Up to **20** break tags are supported per request. After the first 20 tags, the remaining ones will be ignored. - Each break is at most **10 seconds**—for example, `time="10s"` or `time="10000ms"`. **Example:** ``` One second pause two seconds pause this is the end. ``` A break never changes delivery. Any [steering](https://docs.inworld.ai/tts/capabilities/steering.md) instruction in force before a break is still in force after it — use `[reset]` or a new `[tag]` to change it. --- ##### Inline custom pronunciation Source: https://docs.inworld.ai/tts/capabilities/custom-pronunciation > Control how a word sounds in one request by replacing it with inline IPA. Inline custom pronunciation controls how a word sounds in one synthesis request by replacing the written word with its International Phonetic Alphabet (IPA) pronunciation. It is useful for generated text, one-off corrections, and applications that want to choose pronunciations per request. ## Add inline IPA Inworld TTS supports inline IPA phoneme notation for custom pronunciation. Use the [International Phonetic Alphabet (IPA)](https://www.vocabulary.com/resources/ipa-pronunciation/) format, wrapped in slashes (`/ /`). **Use standard English IPA phonemes only.** Do not use CMU dictionary phonemes (ARPAbet) or phoneme sets from other languages. Inworld TTS is optimized for English IPA symbols. For example: - Suppose you are building an AI travel agent, and it is recommending the destination Crete, which is pronounced /kriːt/ ("kreet") in English. - You can ensure the correct pronunciation by passing it inline: `Your interests are a perfect match for a honeymoon in /kriːt/.` **Replace one word at a time.** Wrap only a single word in slashes for each substitution. Do not wrap multiple words or phrases within the same slash delimiters. - Correct: `Welcome to /kriːt/, the beautiful island.` - Incorrect: `Welcome to /kriːt ðə bjuːtɪfəl aɪlənd/.` The model uses the IPA pronunciation wherever it appears inline. For LLM-generated text, you can replace the original spelling with IPA before sending it to TTS. ## Find the right IPA phonemes To find the correct phonemes: - **Ask an LLM like ChatGPT**: For example, you can ask: > "What are the IPA phonemes for the word Crete, pronounced like 'kreet'?" - **Use reference websites**: - [toPhonetics](https://tophonetics.com/) is a free tool that converts English text to IPA transcription. - [Vocabulary.com's IPA Pronunciation Guide](https://www.vocabulary.com/resources/ipa-pronunciation/) provides tables of IPA symbols with example words. Once you have the correct phonemes, you can embed them directly into your TTS request: `Your adventure in /kriːt/ begins today.` ## When to use inline pronunciation Use inline pronunciation when the correction should apply to one request, when an application already transforms generated text, or when the same spelling must sound differently in different contexts. --- ##### Verbatim tags Source: https://docs.inworld.ai/tts/capabilities/verbatim Alphanumeric strings such as booking references, license plates, serial numbers, and IDs can be ambiguous for TTS. The model might read `AHAA7771Z` as a word, a number, or a mix of both. Wrap the sequence in `` tags to spell it out character by character: letters as letter names, digits as single digits, and symbols by name (for example, `@` is read as "at"). **Example:** ``` Your booking reference is AHAA7771Z. ``` The tagged sequence is read as "A H A A seven seven seven one Z" instead of being read as a word or a number. **Constraints:** - Use well-formed tags with the sequence inside: `AHAA7771Z`. Tag names are **case insensitive**; `` is an equivalent, SSML-standard spelling of the same tag. - Keep the content to letters, digits, spaces, and these symbols: `( ) - # : . , + @ / _ *`. Tags containing other characters may be read as regular text instead of being spelled out. - Verbatim tags require [text normalization](https://docs.inworld.ai/tts/best-practices/generating-speech.md#normalize-complex-text) to be enabled. If `applyTextNormalization` is set to `OFF`, the tags are removed and their content is read as regular text. - Expansion is currently English-oriented: characters are read with English letter and number names. Verbatim tags are most reliable on `inworld-tts-2` — we recommend it for alphanumeric-heavy content. As with any generative TTS, exact renditions can vary; keep tagged sequences short where possible and verify output for your use case. --- ##### Language tags Source: https://docs.inworld.ai/tts/capabilities/language-tags > Speak each part of a text in the language you choose with inline lang tags. A single text often mixes languages: a language tutor quoting a Spanish word, a travel guide naming a Japanese temple, an assistant reading a French title. Wrap each part of the text in `` tags that name its language, and each part is spoken in that language, with that language's pronunciation. ``` In Spanish, the word llama means flame. ``` "In Spanish, the word" and "means flame" are spoken in English, and "llama" in Spanish. Language tags work with `inworld-tts-2` on every endpoint: [non-streaming](https://docs.inworld.ai/tts/synthesize-speech.md), [streaming](https://docs.inworld.ai/tts/synthesize-speech-streaming.md), [WebSocket](https://docs.inworld.ai/tts/synthesize-speech-websocket.md), [async](https://docs.inworld.ai/tts/synthesize-speech-async.md) and [batch](https://docs.inworld.ai/tts/synthesize-speech-batch.md). ## When to use language tags Use language tags to choose the language for part of a sentence. They are especially useful when text has the same spelling in two languages. Without a tag, the model must guess which reading you mean. - **Chinese and Japanese** share characters. `日本大学` is *Rìběn dàxué* in Chinese and *Nihon daigaku* in Japanese: ``` 这所学校的日语名字是日本大学。 ``` - **English and Spanish** share many spellings. `llama` is the animal in English, but in Spanish it means "flame" and is pronounced differently, as in the example above. ## Syntax - Open a span with `` and close it with ``, where `CODE` is a [BCP-47](https://www.rfc-editor.org/info/bcp47) language tag, such as `es`, `es-MX` or `ja-JP`. See [supported languages](https://docs.inworld.ai/tts/capabilities/multilingual.md#supported-languages). - The SSML spelling `` is accepted too, so SSML generated by other tools works unchanged. - Tag and attribute names are **case insensitive**, and the code may use single, double or no quotes. - Tags do not nest. Close each span before opening the next. - A `` with no open span, and a tag whose code is not a valid language tag, are ignored. ## WebSocket How you send language tags on a [WebSocket](https://docs.inworld.ai/tts/synthesize-speech-websocket.md) context depends on how [the context buffers text](https://docs.inworld.ai/tts/synthesize-speech-websocket.md#choose-how-text-is-buffered): - **`autoMode: false`**: make each flush self-contained. Wrap every part of the flushed text in the language it should be spoken in, and close every span you open before you flush. - **`autoMode: true` with `CLIENT_SEGMENTED`**: make each `sendText` payload self-contained in the same way. - **`autoMode: true` with `SENTENCE_BOUNDARY`**: send text as it arrives, including individual tokens from an LLM. A tag may be split across messages, and a span may continue across several. Tags count toward the characters billed for a request, as other markup does. --- ##### Timestamps Source: https://docs.inworld.ai/tts/capabilities/timestamps Timestamp alignment returns timing information for the generated audio. Use it for word highlighting, karaoke-style captions, and lip-sync. This page's examples and behavior apply to the Realtime TTS-2 models (`inworld-tts-2`, `inworld-tts-2-flash`). Timestamp alignment is available in every language the model supports — see [Languages](https://docs.inworld.ai/tts/capabilities/multilingual.md#supported-languages). Set the `timestampType` request parameter to control granularity: - `WORD`: Return timestamps for every token in the original text — words, punctuation, and whitespace — in the exact order they were given, with phoneme-level timing and viseme symbols. - `CHARACTER`: Return timestamps for each character or punctuation Enabling timestamp alignment can increase latency (especially for the non-streaming endpoint). When enabled, the response includes timestamp arrays: - `WORD`: `timestampInfo.wordAlignment` with `words`, `wordStartTimeSeconds`, `wordEndTimeSeconds`, and `phoneticDetails`. The `words` array covers every token from the original input in order, so the alignment maps back to the full text without gaps. - `CHARACTER`: `timestampInfo.characterAlignment` with `characters`, `characterStartTimeSeconds`, `characterEndTimeSeconds` See the [API reference](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech) for full details. ## Streaming behavior You can control how timestamp data is delivered alongside audio using [`timestampTransportStrategy`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-timestamp-transport-strategy). ### Sync (default) Audio and alignment arrive together in each chunk. Every chunk contains both audio data and its corresponding timestamps. ``` Chunk 1: audio + timestamps for chunk 1 Chunk 2: audio + timestamps for chunk 2 Chunk 3: audio + timestamps for chunk 3 ``` This is the simplest approach, but it slightly delays the first audio. ### Async Audio chunks arrive first, followed by separate trailing messages containing only timestamp data. This reduces time-to-first-audio, since the server doesn't need to wait for alignment computation before sending audio. ``` Chunk 1: audio only Chunk 2: audio only Chunk 3: audio only Chunk 4: timestamps only (alignment for chunks 1–3) Chunk 5: timestamps only ... ``` Use async when you prioritize playback speed and can handle timestamps arriving after their corresponding audio. Use sync when you need audio and timestamps together in each chunk (e.g., for real-time lip-sync or word highlighting during playback). Set `timestampTransportStrategy` to `SYNC` or `ASYNC` in your request. See the [API reference](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-timestamp-transport-strategy) for details. ### Response structure Returns alignment data with phoneme-level timing and viseme symbols for lip-sync animation. ```json { "timestampInfo": { "wordAlignment": { "words": ["", "Hello", " ", "world", ", ", "this", " ", "will", " ", "be", " ", "saved", "."], "wordStartTimeSeconds": [0, 0.24, 0.59, 0.59, 1.09, 1.29, 1.52, 1.52, 1.62, 1.62, 1.73, 1.73, 2.34], "wordEndTimeSeconds": [0.24, 0.59, 0.59, 1.09, 1.29, 1.52, 1.52, 1.62, 1.62, 1.73, 1.73, 2.34, 2.34], "phoneticDetails": [ { "wordIndex": 0, "phones": [ {"phoneSymbol": "[silence]", "startTimeSeconds": 0, "durationSeconds": 0.24, "visemeSymbol": "bmp"} ] }, { "wordIndex": 1, "phones": [ {"phoneSymbol": "h", "startTimeSeconds": 0.24, "durationSeconds": 0.14, "visemeSymbol": "cdgknstxyz"}, {"phoneSymbol": "ɛ", "startTimeSeconds": 0.38, "durationSeconds": 0.04, "visemeSymbol": "aei"}, {"phoneSymbol": "l", "startTimeSeconds": 0.42, "durationSeconds": 0.05, "visemeSymbol": "l"}, {"phoneSymbol": "ə", "startTimeSeconds": 0.47, "durationSeconds": 0.07, "visemeSymbol": "aei"}, {"phoneSymbol": "ʊ", "startTimeSeconds": 0.54, "durationSeconds": 0.05, "visemeSymbol": "o"} ] }, { "wordIndex": 2, "phones": [ {"phoneSymbol": "[silence]", "startTimeSeconds": 0.59, "durationSeconds": 0, "visemeSymbol": "bmp"} ] }, { "wordIndex": 3, "phones": [ {"phoneSymbol": "w", "startTimeSeconds": 0.59, "durationSeconds": 0.1, "visemeSymbol": "qw"}, {"phoneSymbol": "ˈɝ", "startTimeSeconds": 0.69, "durationSeconds": 0.03, "visemeSymbol": "r"}, {"phoneSymbol": "ɫ", "startTimeSeconds": 0.72, "durationSeconds": 0.23, "visemeSymbol": "l"}, {"phoneSymbol": "d", "startTimeSeconds": 0.95, "durationSeconds": 0.14, "visemeSymbol": "cdgknstxyz"} ] }, { "wordIndex": 4, "phones": [ {"phoneSymbol": "[silence]", "startTimeSeconds": 1.09, "durationSeconds": 0.2, "visemeSymbol": "bmp"} ] }, { "wordIndex": 5, "phones": [ {"phoneSymbol": "ð", "startTimeSeconds": 1.29, "durationSeconds": 0.07, "visemeSymbol": "th"}, {"phoneSymbol": "ɪ", "startTimeSeconds": 1.36, "durationSeconds": 0.05, "visemeSymbol": "ee"}, {"phoneSymbol": "s", "startTimeSeconds": 1.41, "durationSeconds": 0.11, "visemeSymbol": "cdgknstxyz"} ] }, { "wordIndex": 6, "phones": [ {"phoneSymbol": "[silence]", "startTimeSeconds": 1.52, "durationSeconds": 0, "visemeSymbol": "bmp"} ] }, { "wordIndex": 7, "phones": [ {"phoneSymbol": "w", "startTimeSeconds": 1.52, "durationSeconds": 0.03, "visemeSymbol": "qw"}, {"phoneSymbol": "ə", "startTimeSeconds": 1.55, "durationSeconds": 0.03, "visemeSymbol": "aei"}, {"phoneSymbol": "ɫ", "startTimeSeconds": 1.58, "durationSeconds": 0.04, "visemeSymbol": "l"} ] }, { "wordIndex": 8, "phones": [ {"phoneSymbol": "[silence]", "startTimeSeconds": 1.62, "durationSeconds": 0, "visemeSymbol": "bmp"} ] }, { "wordIndex": 9, "phones": [ {"phoneSymbol": "b", "startTimeSeconds": 1.62, "durationSeconds": 0.05, "visemeSymbol": "bmp"}, {"phoneSymbol": "i", "startTimeSeconds": 1.67, "durationSeconds": 0.06, "visemeSymbol": "ee"} ] }, { "wordIndex": 10, "phones": [ {"phoneSymbol": "[silence]", "startTimeSeconds": 1.73, "durationSeconds": 0, "visemeSymbol": "bmp"} ] }, { "wordIndex": 11, "phones": [ {"phoneSymbol": "s", "startTimeSeconds": 1.73, "durationSeconds": 0.14, "visemeSymbol": "cdgknstxyz"}, {"phoneSymbol": "ˈe", "startTimeSeconds": 1.87, "durationSeconds": 0.17, "visemeSymbol": "ee"}, {"phoneSymbol": "ɪ", "startTimeSeconds": 2.04, "durationSeconds": 0.05, "visemeSymbol": "ee"}, {"phoneSymbol": "v", "startTimeSeconds": 2.09, "durationSeconds": 0.06, "visemeSymbol": "fv"}, {"phoneSymbol": "d", "startTimeSeconds": 2.15, "durationSeconds": 0.19, "visemeSymbol": "cdgknstxyz"} ] }, { "wordIndex": 12, "phones": [ {"phoneSymbol": "[silence]", "startTimeSeconds": 2.34, "durationSeconds": 0, "visemeSymbol": "bmp"} ] } ] } } } ``` #### Phonetic details structure Each entry in `phoneticDetails` contains: | Field | Description | | :---- | :---- | | `wordIndex` | 0-based index into the `words` array. Speech tokens have full phoneme breakdowns; non-speech tokens have a single `[silence]` phone (may be zero-length). | | `phones` | Array of phonemes for this token. | | `isPartial` | Deprecated. This field may still appear in responses for backward compatibility, but it is always `false`. | Each phone entry contains: | Field | Description | | :---- | :---- | | `phoneSymbol` | The phone symbol: IPA for speech phones, or `[silence]` for non-speech tokens. | | `startTimeSeconds` | Start time of the phoneme in seconds. | | `durationSeconds` | Duration of the phoneme in seconds. | | `visemeSymbol` | The viseme symbol for lip-sync animation. | #### Viseme symbols The following viseme symbols are used for lip-sync animation: | Viseme | Description | | :---- | :---- | | `aei` | Open mouth vowels (a, e, i, ə, ʌ, æ, ɑ, etc.) | | `o` | Rounded vowels (o, ʊ, əʊ, oʊ, etc.) | | `ee` | Front vowels (i, ɪ, eɪ, etc.) | | `bmp` | Bilabial consonants (b, m, p) | | `fv` | Labiodental consonants (f, v) | | `l` | Lateral consonant (l) | | `r` | Rhotic sounds (r, ɝ, ɚ) | | `th` | Dental fricatives (θ, ð) | | `qw` | Rounded consonants (w, ʍ) | | `chjsh` | Postalveolar/palatal consonants (tʃ, dʒ, ʃ, ʝ) | | `cdgknstxyz` | Alveolar/velar consonants (c, d, g, k, n, s, t, x, y, z) | --- ##### Long text input Source: https://docs.inworld.ai/tts/capabilities/long-text-input > Synthesize speech from text longer than 2,000 characters, server-side or by chunking it yourself The synchronous TTS API accepts up to **2,000 characters** per request. For longer content — articles, book chapters, scripts — you have two options. **Start with the [Async API](https://docs.inworld.ai/tts/synthesize-speech-async.md)**, which accepts up to **100,000 characters**. Submit the text as one job. The server splits it into chunks, synthesizes each one, and joins the audio into a single file to download. You do not need to maintain a pipeline or tune the joins between chunks. **Chunk it yourself** when you need something a job cannot give you: text beyond 100,000 characters, control over where the splits land, or audio as it is produced rather than at the end. We provide ready-to-run scripts in **[Python](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/python/example_tts_long_input.py)** and **[JavaScript](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/js/example_tts_long_input_compressed.js)** that handle that entire pipeline for you. ## How the Scripts Work The input is split into segments under the 2,000-character API limit. The chunking algorithm looks for natural break points in the following priority order: 1. Paragraph breaks (`\n\n`) 2. Line breaks (`\n`) 3. Sentence endings (`.` `!` `?`) 4. Last space (fallback) This ensures audio segments end at natural pauses, producing smooth-sounding output. Each chunk is sent to the TTS API with controlled concurrency and automatic retry logic for rate limits. Chunks are processed in parallel (default: 2 concurrent requests) to speed up synthesis while respecting API rate limits. The individual audio responses are combined into a single output file. The Python script produces a **WAV** file with configurable silence between segments, while the JavaScript script produces an **MP3** file and uses **ffmpeg** to merge segments with correct duration metadata. ## Configuration Both scripts share the same tunable parameters: | Parameter | Default | Description | | :--- | :--- | :--- | | `MIN_CHUNK_SIZE` | 500 | Minimum characters before looking for a break point | | `MAX_CHUNK_SIZE` | 1,600 | Maximum chunk size (stays under the 2,000-char API limit) | | `MAX_CONCURRENT_REQUESTS` | 2 | Parallel API requests (increase with caution to avoid rate limits) | | `MAX_RETRIES` | 3 | Retry attempts for rate-limited requests with exponential backoff | ## Running the Scripts ### Prerequisites - An **Inworld API key** set as the `INWORLD_API_KEY` environment variable - A text file with your long-form content - **Python 3** (for the Python script) or **Node.js** (for the JavaScript script) - **ffmpeg** (optional, for the JS script — produces correct MP3 duration metadata) ### Python ```bash export INWORLD_API_KEY=your_api_key_here pip install requests python-dotenv python example_tts_long_input.py ``` The script reads the input text file, chunks it, synthesizes all chunks with the Inworld TTS API, and saves the combined audio as a **WAV** file. It also prints a splice report showing the exact timestamps where chunks were joined, useful for quality checking. ### JavaScript ```bash export INWORLD_API_KEY=your_api_key_here node example_tts_long_input_compressed.js ``` The script follows the same chunking and synthesis pipeline, outputting a compressed **MP3** file. When **ffmpeg** is available, it merges segments with correct duration metadata. Otherwise, it falls back to raw concatenation. ## Code Examples WAV output with splice report and configurable silence between segments Compressed MP3 output with ffmpeg-based segment merging ## Next Steps Submit up to 100,000 characters as one job and skip the pipeline. Learn about the standard (non-streaming) synthesis API. Use streaming for real-time playback of shorter content. Optimize time-to-first-audio for real-time use cases. --- #### APIs ##### Synthesize speech Source: https://docs.inworld.ai/tts/synthesize-speech > Send up to 2,000 characters and receive the complete audio in a single HTTP response The non-streaming API is the simplest way to synthesize a short piece of text: send one request and receive the complete audio in one HTTP response. It accepts up to **2,000 characters** per request. The server returns audio only after generating it all, so playback starts later than with streaming. Use this API when a simple, one-shot integration matters more than latency or input size. For most other use cases, choose a method below. ## Choose a synthesis method | If you need | Use | Why | | --- | --- | --- | | The lowest possible latency | [WebSocket API](https://docs.inworld.ai/tts/synthesize-speech-websocket.md) | Reuses a persistent connection and streams audio chunks as they are generated. | | Low latency with a simpler integration | [Streaming API](https://docs.inworld.ai/tts/synthesize-speech-streaming.md) | Streams audio over HTTP and accepts up to **4,000 characters** per request. | | A drop-in replacement for OpenAI text-to-speech | [OpenAI compatibility](https://docs.inworld.ai/tts/openai-compatibility.md) | Works with the OpenAI SDKs once the base URL, API key, model, and voice point at Inworld, and streams audio like the Streaming API. | | Long-form audio such as audiobooks, podcasts, or voiceovers | [Async API](https://docs.inworld.ai/tts/synthesize-speech-async.md) | Accepts up to **100,000 characters** per job and returns the result after background generation. On-Demand accounts are capped at 10,000 characters per job. | | Lower-cost generation that can finish later | [Batch API](https://docs.inworld.ai/tts/synthesize-speech-batch.md) | Costs 20% less per character and processes many independent requests in the background. | | The simplest integration for short text | Synthesize Speech | Returns the complete audio in one response, with a **2,000-character** request limit. | See the [latency best practices](https://docs.inworld.ai/tts/best-practices/latency.md) for more ways to reduce time to first audio. ## Code Examples Use `generate()` from the `@inworld/tts` SDK — handles chunking, retries, and encoding automatically. View our JavaScript implementation example Use `generate()` from the `inworld-tts` SDK — handles chunking, retries, and encoding automatically. ## API Reference View the complete API specification ## Next Steps Learn best practices for producing high-quality voice clones. Learn best practices for synthesizing high-quality speech. Explore Python and JavaScript code examples for TTS integration. --- ##### Synthesize speech (streaming) Source: https://docs.inworld.ai/tts/synthesize-speech-streaming > Send text, receive audio chunks over HTTP as they are generated Send text to receive audio chunks over HTTP as they are generated. Start playback before synthesis finishes to reduce time-to-first-audio. Best for real-time applications and conversational AI when you want low-latency playback without managing a persistent connection. Each request accepts up to **4,000 characters**. For the lowest latency, use the [WebSocket API](https://docs.inworld.ai/tts/synthesize-speech-websocket.md). For long-form content such as audiobooks, podcasts, or voiceovers, use the [Async API](https://docs.inworld.ai/tts/synthesize-speech-async.md). For tips on optimizing latency, see the [latency best practices guide](https://docs.inworld.ai/tts/best-practices/latency.md). ## HTTP request and response framing Send `POST https://api.inworld.ai/tts/v1/voice:stream` with `Authorization: Basic ` and `Content-Type: application/json`. Use the Portal's Base64 credentials unchanged, without encoding them again. Keep the key on your server; see [Authentication](https://docs.inworld.ai/portal/authentication.md) for browser and mobile token options. ```bash curl --no-buffer 'https://api.inworld.ai/tts/v1/voice:stream' \ --header "Authorization: Basic $INWORLD_API_KEY" \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data '{ "text": "Hello, world!", "voiceId": "Ashley", "modelId": "inworld-tts-2", "audioConfig": { "audioEncoding": "PCM", "sampleRateHertz": 48000 } }' ``` The JSON response is **newline-delimited JSON (NDJSON)**: one JSON object per line, not SSE (`data:` events) or a single JSON document. Each successful message wraps its payload in `result`. Audio is Base64-encoded inside `result.audioContent`, not raw audio in the HTTP body. Example lines below use placeholders for the audio bytes: ```json {"result":{"audioContent":"","usage":{"processedCharactersCount":13,"modelId":"inworld-tts-2"}}} {"result":{"audioContent":"","usage":{"processedCharactersCount":0,"modelId":"inworld-tts-2"}}} ``` A network read may contain part of a line or several lines. Buffer until a newline, parse each complete object, and Base64-decode its audio independently. Do not call `response.json()` on the whole streaming response or concatenate Base64 strings before decoding. Process a final non-empty line at EOF too. Timestamp-only messages can arrive after the audio; keep reading until EOF. | Request field | Meaning | | :--- | :--- | | `text` | Up to **4,000 UTF-16 code units** per HTTP streaming request | | `voiceId` | Use the exact `voiceId` from [List voices](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/list-voices.md), such as `Ashley` | | `modelId` | Set explicitly; choose from [Models](https://docs.inworld.ai/tts/tts-models.md), for example `inworld-tts-2` | | `audioConfig.audioEncoding` | Encoding of the decoded audio bytes; see the table below | | `audioConfig.sampleRateHertz` | Requested output sample rate in Hz; configure your PCM player to match | The [non-streaming endpoint](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md) accepts **2,000 UTF-16 code units**, while this endpoint accepts **4,000**. In JavaScript, `text.length` measures this unit; some emoji count as two. An oversized request returns `INVALID_ARGUMENT` (code `3`) with a message such as `text length should not exceed 4000 characters.` SDK versions can enforce their own smaller limits or split text into multiple requests. Set `modelId` explicitly and consult the documentation for the installed SDK version instead of relying on its default model or chunk size. ## Choose an audio encoding The table below compares common streaming choices; it is not the full encoding list. The API also exposes `OGG_OPUS`, `ALAW`, `MULAW`, and `FLAC`. See the [`audioConfig.audioEncoding` API reference](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md) for the complete enum and format descriptions. | Encoding | Decoded bytes in an HTTP stream | | :--- | :--- | | `PCM` | Raw signed 16-bit little-endian samples, with no WAV header; suitable for a PCM playback queue | | `LINEAR16` | A complete WAV header in **every** audio chunk; do not feed the headers to a raw PCM player or concatenate chunks as one ordinary WAV file | | `WAV` | A streaming WAV header in the first audio chunk only | | `MP3` | Compressed audio; use an MP3-capable streaming decoder/player | The encoding names describe the audio after Base64 decoding. `PCM` and `LINEAR16` contain the same sample format but have different framing. ## Read the stream in Node.js This server-side example uses Node.js 20+ built-in `fetch`. `onAudio` receives decoded PCM bytes; enqueue them for playback or write them to a raw PCM file. A raw PCM file needs its sample rate and channel count supplied to the player. ```javascript async function streamSpeech(text, onAudio, { signal } = {}) { const response = await fetch('https://api.inworld.ai/tts/v1/voice:stream', { method: 'POST', headers: { Authorization: `Basic ${process.env.INWORLD_API_KEY}`, 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ text, voiceId: 'Ashley', modelId: 'inworld-tts-2', audioConfig: { audioEncoding: 'PCM', sampleRateHertz: 48000 }, }), signal, }); if (!response.ok) { const body = await response.text(); let message = body; try { const payload = JSON.parse(body); message = (payload.error ?? payload).message ?? body; } catch { /* An intermediary may return a non-JSON error. */ } throw new Error(`HTTP ${response.status}: ${message}`); } if (!response.body) throw new Error('Missing response stream'); const decoder = new TextDecoder(); let pending = ''; let processedCharacters = 0; async function consume(line) { if (!line.trim()) return; const message = JSON.parse(line); if (message.error) throw new Error(message.error.message ?? 'Synthesis failed'); const result = message.result; processedCharacters += result?.usage?.processedCharactersCount ?? 0; if (result?.audioContent) { await onAudio(Buffer.from(result.audioContent, 'base64')); } } for await (const bytes of response.body) { pending += decoder.decode(bytes, { stream: true }); let newline; while ((newline = pending.indexOf('\n')) !== -1) { const line = pending.slice(0, newline); pending = pending.slice(newline + 1); await consume(line); } } pending += decoder.decode(); await consume(pending); return { processedCharacters }; } ``` ## Errors, usage, and cancellation Check both the initial HTTP status and each parsed message. A stream can return an HTTP `200` and later end with an error message: ```json {"error":{"code":3,"message":"Invalid request"}} ``` Non-streaming endpoints can instead return `{"code":5,"message":"Voice not found"}`. For shared error handling, inspect `payload.error ?? payload`. Codes in these objects are gRPC status codes, not HTTP status numbers. Preserve any audio already delivered as partial output and report the failure; a successful HTTP status alone does not establish that synthesis finished. For HTTP streaming, `usage.processedCharactersCount` carries the full input's UTF-16 length on the **first response message**; subsequent messages report zero. Sum this field across messages, or retain the first value. Reading only the last message reports zero and loses the request's usage. Pass an `AbortController` signal to `streamSpeech` and call `abort()` to cancel the fetch and response reader. Handle the rejection as cancellation, and stop and clear your playback queue separately. Cancellation does not refund text already metered: the full submitted text is metered when the first response is emitted, rather than in proportion to the audio you play. Aborting before you receive audio is not proof that the server has not already metered the request. Audio chunk size, arrival spacing, and time to first audio vary with text, model, encoding, and load. Buffer for continuous playback rather than assuming fixed chunk durations or an arrival rate equal to playback speed. For mono PCM16, `durationSeconds = decodedByteLength / (2 * sampleRateHertz)`; at 48 kHz, 9,600 decoded bytes represent 100 ms of audio. These are sizing calculations, not latency guarantees. ## Timestamp transport strategy When using [timestamp alignment](https://docs.inworld.ai/tts/capabilities/timestamps.md), you can choose how timestamps are delivered alongside audio using `timestampTransportStrategy`: - **`SYNC`** (default): Each chunk contains both audio and timestamps together. - **`ASYNC`**: Audio chunks arrive first, with timestamps following in separate trailing messages. This reduces time-to-first-audio. See [Timestamps](https://docs.inworld.ai/tts/capabilities/timestamps.md#streaming-behavior) for details on how each mode works. ## Code Examples Use `stream()` from the `@inworld/tts` SDK — async iteration over audio chunks with automatic retries. View our JavaScript implementation example Use `stream()` from the `inworld-tts` SDK — async iteration over audio chunks with automatic retries. ## API Reference View the complete API specification ## Next Steps Learn best practices for producing high-quality voice clones. Learn best practices for synthesizing high-quality speech. Explore Python and JavaScript code examples for TTS integration. --- ##### Synthesize speech (WebSocket) Source: https://docs.inworld.ai/tts/synthesize-speech-websocket > Persistent connection for lowest-latency audio streaming Open a persistent WebSocket connection and send text messages. The server streams audio chunks over the same connection, avoiding per-request overhead and repeated handshakes for the lowest possible latency. Best for voice agents and interactive applications that send multiple synthesis requests in a session, where avoiding connection setup on every call makes a measurable difference. If you only need a single request-response with chunked audio, the [Streaming API](https://docs.inworld.ai/tts/synthesize-speech-streaming.md) is simpler to integrate. For tips on optimizing latency, see the [latency best practices guide](https://docs.inworld.ai/tts/best-practices/latency.md). ## Message pipelining Messages on a connection are processed in order, so you don't need to wait for the `contextCreated` acknowledgment before sending text. For the lowest latency, send each message as soon as it is ready — waiting for the acknowledgment adds a full network round trip before the first audio chunk arrives. If you omit `contextId`, messages are automatically routed to the connection's auto-created context. ## Choose when speech generation starts Choose a mode based on how text arrives in your application: | Your input | Choose | Who decides when to generate speech? | | --- | --- | --- | | You have the full text, or want to control batching yourself | **Manual** — `autoMode: false` (default) | Your app calls `flushContext`, or `closeContext` when finished. Buffering safeguards also apply. | | Your app already splits text into complete sentences or phrases | **Client segmented** — `autoMode: true`, `autoModeStrategy: "CLIENT_SEGMENTED"` | The server schedules the segments your app sends. | | You receive partial text or individual tokens from an LLM | **Sentence boundary** — `autoMode: true`, `autoModeStrategy: "SENTENCE_BOUNDARY"` (Preview) | The server detects sentence boundaries in the incoming text. | A **context** holds the settings and text for a speech-generation session within the WebSocket connection. The **buffer** holds text waiting to be synthesized. To **flush** is to request synthesis of the buffered text, even if it has no final punctuation. Complete any markup tags before flushing. ![Three text paths: manual text waits for your app to flush; client-split sentences are scheduled by the server; raw LLM tokens are buffered until the server detects a boundary. Each path leads to speech generation.](https://static.inworld.ai/portal-doc/img/tts/websocket-text-strategies.02ef45a6.svg) Set `autoMode` and `autoModeStrategy` in the `create` message when creating a context. Leave the [buffering safeguards](#text-limits) at their defaults for normal use. ### Manual flushing Use `autoMode: false` when you have a complete reply or want to decide how to batch text. Send text with `sendText`, then call `flushContext` to request speech generation. If you have no more text for this context, use `closeContext` instead. To synthesize buffered text and keep the context open for more text, send: ```javascript ws.send(JSON.stringify({contextId: "reply-1", flushContext: {}})); ``` Use `flushContext` between batches you want to control yourself. When finished, [close the context](#close-a-context-in-any-mode) to release any remaining text and finish receiving audio. `autoModeStrategy` is ignored in this mode. See the runnable [manual-flushing guide](https://github.com/inworld-ai/inworld-api-examples/tree/main/tts/python/websocket_guides/base), which also covers interruptions and keeping LLM history aligned with what the user heard. ### Client-segmented streaming Use `autoMode: true` with `autoModeStrategy: "CLIENT_SEGMENTED"` when your app already splits text into sentences or phrases. Send each complete segment as it becomes available. The server controls flushing and can combine queued segments while earlier audio is streaming. **Auto-mode default:** `autoMode: true` on its own selects `CLIENT_SEGMENTED`. Omitting `autoModeStrategy`, or setting it to `AUTO_MODE_STRATEGY_UNSPECIFIED`, has the same effect. To send unsplit LLM tokens, explicitly select `SENTENCE_BOUNDARY`. See the runnable [client-side sentence-splitting guide](https://github.com/inworld-ai/inworld-api-examples/tree/main/tts/python/websocket_guides/client_segmentation). ### Server sentence-boundary streaming Use `autoMode: true` with `autoModeStrategy: "SENTENCE_BOUNDARY"` to send text as your LLM produces it. The server buffers incoming fragments and detects sentence boundaries and complete break tags. **Preview:** `SENTENCE_BOUNDARY` supports `inworld-tts-2` and `inworld-tts-2-flash` where those models are available. Selecting it with auto mode enabled on an unsupported model returns `INVALID_ARGUMENT`. The **tail** is the remaining buffered text that has not reached a confirmed boundary. When your text source finishes, [close the context](#close-a-context-in-any-mode) to release this remaining text. If you need to release the tail and continue using the same context, you can call `flushContext` instead. Auto mode does not use the buffer delay timer. See the runnable [LLM token-streaming guide](https://github.com/inworld-ai/inworld-api-examples/tree/main/tts/python/websocket_guides/streaming_tokens). ### Stream incremental text This example uses an already connected, authenticated WebSocket. Each `ws.send` sends one message. In your application, send each fragment as your LLM produces it. ```javascript ws.send(JSON.stringify({ contextId: "reply-1", create: { voiceId: "Ashley", modelId: "inworld-tts-2", autoMode: true, autoModeStrategy: "SENTENCE_BOUNDARY" } })); for (const text of ["Hello from ", "sentence boundary streaming.", " Next we will finish this thought"]) { ws.send(JSON.stringify({contextId: "reply-1", sendText: {text}})); } // The LLM has finished: synthesize remaining text and close this context. ws.send(JSON.stringify({contextId: "reply-1", closeContext: {}})); // Keep receiving audio until the server sends contextClosed. ``` Here is how the buffer changes as those messages arrive: | Step | Your app sends | What happens on the server | | --- | --- | --- | | 1 | `"Hello from "` | Text waits in the buffer. | | 2 | `"sentence boundary streaming."` | The period may need more text to confirm a sentence boundary. | | 3 | `" Next we will finish this thought"` | `Hello from sentence boundary streaming.` becomes eligible for synthesis. `Next we will finish this thought` stays buffered. | | 4 | `closeContext` | The remaining text becomes eligible for synthesis, even without final punctuation. The server finishes sending audio, then sends `contextClosed`. | A confirmed sentence is ready to be synthesized, but may be combined with other ready text while earlier audio is streaming. ### Close a context in any mode In all three modes, send `closeContext` when you have no more text for this context. It requests synthesis of any remaining buffered text and closes the context after the response finishes. You cannot send more text to that context after requesting close. ```javascript ws.send(JSON.stringify({contextId: "reply-1", closeContext: {}})); ``` **Wait for `contextClosed` before you stop receiving audio for that context.** Sending `closeContext` begins closing it; audio may still be arriving. Closing the WebSocket immediately can cut off the remaining audio. ### Advanced: synthesis batches and timestamps The server can combine ready sentences while earlier audio is streaming, including when a flush or close releases the remaining text. Do not expect one audio response or `flushCompleted` event per `sendText` message or detected sentence. `flushCompleted` marks the end of a synthesis batch (a flush). Alignment timestamps reset for the next flush. ### Markup and unfinished input With `SENTENCE_BOUNDARY`, a supported markup tag can span multiple `sendText` messages: ```text First message: "[whis" Second message: "per] Hello." Combined text: "[whisper] Hello." ``` - An incomplete tag stays buffered; the server does not send a partial tag to synthesis. - Instructions stay with the speech that follows them. See [Voice steering](https://docs.inworld.ai/tts/capabilities/steering.md) for model-specific instruction support. - Finish every markup tag before flushing or closing. Incomplete markup at end of input returns `INVALID_ARGUMENT`. - Ordinary text does not need final punctuation: an explicit flush or close releases it. A complete break tag, such as ``, provides a boundary and produces the requested pause. ## Text limits | Limit | What it means for your app | | --- | --- | | **2,000 characters per `sendText` message** | Split longer input across multiple messages. | | **UTF-16 character counting** | Request limits and billed usage count UTF-16 code units. Most characters count as one; many emoji count as two. | | **Buffering safeguards** | The server can flush accumulated text before your app requests it. In `SENTENCE_BOUNDARY` mode, safeguards can force a cut while keeping supported markup intact. Leave them at their defaults for normal use. | | **No buffer delay timer in auto mode** | Explicitly flush or close when your text source finishes so short remaining text is not left waiting. | For optional `bufferCharThreshold` and `maxBufferDelayMs` settings, see the [API reference](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-websocket.md). Have one large block of text? The [Streaming API](https://docs.inworld.ai/tts/synthesize-speech-streaming.md) accepts up to **4,000 characters** per request. For connection and concurrency limits, see [Rate limits](https://docs.inworld.ai/resources/rate-limits.md). ## Timestamp transport strategy When using [timestamp alignment](https://docs.inworld.ai/tts/capabilities/timestamps.md), you can choose how timestamps are delivered alongside audio using `timestampTransportStrategy`: - **`SYNC`** (default): Each chunk contains both audio and timestamps together. - **`ASYNC`**: Audio chunks arrive first, with timestamps following in separate trailing messages. This reduces time-to-first-audio. See [Timestamps](https://docs.inworld.ai/tts/capabilities/timestamps.md#streaming-behavior) for details on how each mode works. ## Code examples View our JavaScript implementation example View our Python WebSocket implementation example Python guides for barge-in, auto mode and sentence-boundary streaming, with a local playground ## API reference View the complete API specification ## Next steps Learn best practices for producing high-quality voice clones. Learn best practices for synthesizing high-quality speech. Explore Python and JavaScript code examples for TTS integration. --- ##### Synthesize speech (async) Source: https://docs.inworld.ai/tts/synthesize-speech-async > Submit a synthesis job, poll for completion, and download the results from signed URLs **Preview.** This API is a preview release and may be further refined before it is marked stable. See [release stages](https://docs.inworld.ai/portal/support.md) for what that means. Submit text to receive a [long-running operation](https://google.aip.dev/151) immediately while synthesis runs in the background. Poll until the operation is done, then download the finished audio and optional timestamps from time-limited signed URLs. Use it for long-form content such as audiobooks, podcasts, and video voiceovers, or batch pipelines that should not keep an HTTP connection open during synthesis. For the simplest integration with short text up to 2,000 characters, use the [Synthesize Speech API](https://docs.inworld.ai/tts/synthesize-speech.md). For real-time playback, use the [Streaming API](https://docs.inworld.ai/tts/synthesize-speech-streaming.md) or, for the lowest latency, the [WebSocket API](https://docs.inworld.ai/tts/synthesize-speech-websocket.md). To submit many separate requests at a lower price and collect the results later, use the [Batch API](https://docs.inworld.ai/tts/synthesize-speech-batch.md). ## How it works `POST /tts/v1/voice:synthesizeAsync` with the same request body as [synchronous synthesis](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md). The response is an operation with a `name` like `workspaces/{workspace}/ttsAsyncJobs/{job}/operations/{operation}` and `done: false`. `GET /lro/v1alpha/{name}` with the full operation name (including its slashes) in the URL path. Poll at a modest interval — every few seconds is plenty. Short inputs typically finish within seconds; long inputs can take minutes. When `done` is `true`, a successful operation carries a `response`, while a failed one carries an `error` status instead. The `response` contains `audioUri` and (if `timestampType` was requested) `timestampsUri`. These are pre-signed URLs — fetch them **without** an `Authorization` header. They expire at `expireTime`, approximately 7 days after completion, so download results you want to keep. To have the results written to your own bucket instead, set `outputConfig.resultsUri` to a pre-signed upload URL: the job delivers one archive there and nothing is retained by Inworld. Workspaces with [Zero Data Retention](https://docs.inworld.ai/portal/zero-data-retention.md) enabled must use it — a job that names no destination is refused at submit. See [Results delivery](https://docs.inworld.ai/tts/job-results-delivery.md). ```bash cURL # 1. Submit — returns an operation name OPERATION=$(curl -s 'https://api.inworld.ai/tts/v1/voice:synthesizeAsync' \ --header "Authorization: Basic $INWORLD_API_KEY" \ --header 'Content-Type: application/json' \ --data '{ "text": "Hello, world! What a wonderful day to be a text-to-speech model!", "voiceId": "Dennis", "modelId": "inworld-tts-2", "audioConfig": { "audioEncoding": "MP3" }, "timestampType": "WORD" }' | jq -r '.name') # 2. Poll until done: true until curl -s "https://api.inworld.ai/lro/v1alpha/$OPERATION" \ --header "Authorization: Basic $INWORLD_API_KEY" \ | tee operation.json | jq -e '.done == true' > /dev/null; do sleep 5 done # 3. Download (signed URLs — no Authorization header) curl -s -o output.mp3 "$(jq -r '.response.audioUri' operation.json)" curl -s -o timestamps.json "$(jq -r '.response.timestampsUri' operation.json)" ``` ```python Python import time import requests BASE = "https://api.inworld.ai" HEADERS = {"Authorization": "Basic "} # 1. Submit operation = requests.post( f"{BASE}/tts/v1/voice:synthesizeAsync", headers=HEADERS, json={ "text": "Hello, world! What a wonderful day to be a text-to-speech model!", "voiceId": "Dennis", "modelId": "inworld-tts-2", "audioConfig": {"audioEncoding": "MP3"}, "timestampType": "WORD", }, ).json() # 2. Poll while not operation.get("done"): time.sleep(5) operation = requests.get( f"{BASE}/lro/v1alpha/{operation['name']}", headers=HEADERS ).json() # 3. Download (signed URLs — no Authorization header) if "error" in operation: raise RuntimeError(f"Synthesis failed: {operation['error']['message']}") result = operation["response"] open("output.mp3", "wb").write(requests.get(result["audioUri"]).content) if "timestampsUri" in result: # present only when timestampType was requested timestamps = requests.get(result["timestampsUri"]).json() # a list; see below ``` The number of async jobs that can run concurrently is limited per account. Submissions over the limit are rejected with a `RESOURCE_EXHAUSTED` error and create nothing — retry once earlier jobs finish. See [Job Concurrency Limits](https://docs.inworld.ai/resources/job-concurrency-limits.md). ## When a job requests timestamps A job that sets `timestampType` needs alignment to succeed as well as synthesis. If alignment cannot be produced, the whole operation fails and carries an `error`. Async never returns an `audioUri` with `timestampsUri` quietly missing when timestamps were requested. Timestamp availability is tracked separately from synthesis, so a language can in principle synthesize while alignment for it is unavailable. **Wherever that is true, the submit fails before you pay for audio you cannot use due to the lack of timestamps.** The request is rejected before any job exists: ``` INVALID_ARGUMENT: timestamps are not available for language 'zz-ZZ'. Omit timestamp_type to synthesize audio without them, or use a supported language. ``` Nothing is created, nothing is charged, and no concurrency slot is used. The error names the language, so you do not need to check anything in advance: if a job is accepted, timestamps are available for it. Drop `timestampType` if you want the audio without timing. A job that passes submit can still fail during synthesis. Two outcomes are worth telling apart, because only one is worth retrying: | Operation `error.code` | Meaning | What to do | | --- | --- | --- | | `FAILED_PRECONDITION` | Alignment was not possible for this request — reported as `timestamps are not available for the requested language`. Reaches you rather than the submit rejection when the language could not be known at submit, most often because it was auto-detected from the text. | Permanent. Resubmit without `timestampType`, or set an explicit supported `language`. Retrying unchanged fails identically. | | `UNAVAILABLE` | Alignment was temporarily unreachable. | Transient. Resubmit the job. | The synchronous and streaming endpoints behave the **opposite** way: they keep the audio and return success with no `timestampInfo` at all. Only async and batch jobs turn a timestamp failure into a failure of the request. If you are porting code from those endpoints, a failed operation where you previously saw silently missing timestamps is expected. ### Reading the timestamps document The timestamps file is a JSON **array** of timestamp objects in timeline order. Each element has the shape of the `timestampInfo` a synchronous response carries: a `wordAlignment` or a `characterAlignment`, matching `timestampType`. A long job produces several elements. Today there is one per chunk of synthesized text (about 1,200 characters), one per SSML break and one per leading steering tag. These boundaries carry no meaning, and their number may change. Do not depend on them. Every word, character and phone time is measured from the start of the audio. The one value local to an element is `phoneticDetails[].wordIndex`, which counts words within that element. To get one alignment for the whole audio, concatenate the elements in order and offset each `wordIndex` by the number of words before it: ```python def join_timestamps(document): """Joins a timestamps document into one alignment for the whole audio.""" merged = {} for element in document: for kind, alignment in element.items(): # wordAlignment or characterAlignment joined = merged.setdefault(kind, {}) offset = len(joined.get("words", [])) # words in earlier elements for key, values in alignment.items(): if key == "phoneticDetails": values = [{**d, "wordIndex": d.get("wordIndex", 0) + offset} for d in values] joined.setdefault(key, []).extend(values) return merged ``` Zero values are omitted from the JSON, so the first word's `wordIndex` is absent rather than `0`; read it with a default, as above. ## Listing your jobs Persist the operation `name` from every submit response — it is the primary handle for polling. If you do lose one (a crash between submit and saving the name, a redeploy), list operations across all of your jobs with `-` in place of the job id: ```bash # Running async jobs only; drop the filter to include finished ones curl -s "https://api.inworld.ai/lro/v1alpha/ttsAsyncJobs/-/operations?filter=-done" \ --header "Authorization: Basic $INWORLD_API_KEY" ``` The workspace is resolved from your API key, so you never have to supply a workspace id. A few things to know: - `filter=-done` (or `NOT done`, or `done=false`) returns only running jobs; `filter=done` (or `done=true`) only finished ones. - Results are unordered and cover roughly the last 7 days — operations expire together with their results. - Paginate with `pageToken` until a response has **no** `nextPageToken`. A short or even empty page can still be followed by more results, so the absent token — not page size — is the end signal. The same shape works for batch jobs via `ttsBatchJobs/-`. The fully qualified form, `workspaces/{workspace}/ttsAsyncJobs/-/operations`, is also accepted. Take the workspace id from the first path segment of any operation `name`. `Operation.name` is always fully qualified. ## Limits | Limit | Value | | --- | --- | | Models | `inworld-tts-2` only. A request naming any other model is rejected at submit | | Text per request | 100,000 characters. On-Demand accounts are capped at 10,000 — see [Job Concurrency Limits](https://docs.inworld.ai/resources/job-concurrency-limits.md#per-job-size-on-the-on-demand-plan) | | Result retention | ~7 days from completion, after which `audioUri` and `timestampsUri` expire | | Concurrent jobs | Per account, by plan — see [Job Concurrency Limits](https://docs.inworld.ai/resources/job-concurrency-limits.md) | For more than 100,000 characters, split the text and submit several jobs. If the pieces are independent, you can use the [Batch API](https://docs.inworld.ai/tts/synthesize-speech-batch.md). Its per-item ceiling is also 100,000 characters, and one job carries up to 10,000 items. ## API Reference Submit an asynchronous synthesis job Poll a job's operation until it completes List your jobs across the workspace ## Next Steps Get word or character timing alignment alongside your audio. Learn how long inputs are handled across the TTS APIs. Learn best practices for synthesizing high-quality speech. --- ##### Synthesize speech (batch) Source: https://docs.inworld.ai/tts/synthesize-speech-batch > Submit many independent synthesis requests as one job, poll for completion, and download a results file listing every item **Preview.** This API is a preview release and may be further refined before it is marked stable. See [release stages](https://docs.inworld.ai/portal/support.md) for what that means. Submit a list of synthesis requests to receive a [long-running operation](https://google.aip.dev/151) immediately. The server synthesizes the whole set in the background. Poll until the operation is done, then download a results file that names every item's audio. Best for large libraries generated in one pass — every line of dialogue in a game, a catalogue of product descriptions, or re-recording an existing script after a voice change. Each item carries its own request, so a single batch can mix voices, languages, and audio formats freely. **Batch trades latency for a lower price, on purpose.** Characters synthesized through batch are billed at **20% less** per character than the same characters through the realtime endpoints. The discount applies on every plan, on top of whatever tier rate you already have, so it does not shrink as you scale up. See the [Pricing Page](https://inworld.ai/pricing) for current rates. What you give up is time. A batch synthesizes its items a group at a time, so a large batch runs for hours rather than minutes. A batch waiting behind your other jobs has not started yet. Submit work you will collect later, rather than work someone is waiting on. If you need a result now, the [synchronous](https://docs.inworld.ai/tts/synthesize-speech.md) and [streaming](https://docs.inworld.ai/tts/synthesize-speech-streaming.md) endpoints are the ones to reach for, at the realtime rate. Plan for a batch to come back later rather than soon: it is queued against your account's capacity and worked through in the background. We are working toward 24-hour delivery — see [Turnaround](#turnaround). For one long piece of text, use the [Async API](https://docs.inworld.ai/tts/synthesize-speech-async.md) — batch is for many separate requests, not for splitting one. For the simplest integration with short text up to 2,000 characters, use the [Synthesize Speech API](https://docs.inworld.ai/tts/synthesize-speech.md). ## How it works `POST /tts/v1/voice:synthesizeBatch` with an `items` array. Each item pairs a `customId` of your choosing with a `request` identical in shape to [synchronous synthesis](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md). The response is an operation named `workspaces/{workspace}/ttsBatchJobs/{batch}/operations/{operation}` with `done: false`. `GET /lro/v1alpha/{name}` with the full operation name (including its slashes) in the URL path — the same endpoint async jobs use. Poll every few seconds; a large batch can take hours. Each poll also returns a `metadata` object counting the items finished so far — see [Tracking progress](#tracking-progress). When `done` is `true`, the `response` carries a `resultsUri` rather than per-item URLs — a batch's results do not fit in one response. Fetch that file to get every item's outcome, then fetch each item's `audioUri`. See [Results file](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-batch.md#results-file) for the file's fields. All of these are pre-signed URLs: request them **without** an `Authorization` header. They expire at `expireTime`, approximately 7 days after completion. To deliver results to your own bucket instead, set `outputConfig.resultsUri` at the top level of the batch. The whole batch is delivered there as one archive, and nothing is retained by Inworld. Workspaces with [Zero Data Retention](https://docs.inworld.ai/portal/zero-data-retention.md) enabled must use it — a batch that names no destination is refused at submit. See [Results delivery](https://docs.inworld.ai/tts/job-results-delivery.md). ```bash cURL # 1. Submit — returns an operation name OPERATION=$(curl -s 'https://api.inworld.ai/tts/v1/voice:synthesizeBatch' \ --header "Authorization: Basic $INWORLD_API_KEY" \ --header 'Content-Type: application/json' \ --data '{ "items": [ { "customId": "chapter-01", "request": { "text": "The first chapter begins on a quiet morning.", "voiceId": "Ashley", "modelId": "inworld-tts-2", "audioConfig": { "audioEncoding": "MP3" } } }, { "customId": "chapter-02", "request": { "text": "The second chapter opens with a storm at sea.", "voiceId": "Dennis", "modelId": "inworld-tts-2", "audioConfig": { "audioEncoding": "MP3" } } } ] }' | jq -r '.name') # 2. Poll until done: true until curl -s "https://api.inworld.ai/lro/v1alpha/$OPERATION" \ --header "Authorization: Basic $INWORLD_API_KEY" \ | tee operation.json | jq -e '.done == true' > /dev/null; do sleep 5 done # 3. Download the results file, then each item's audio # (signed URLs — no Authorization header) curl -s -o results.json "$(jq -r '.response.resultsUri' operation.json)" jq -r '.results[] | select(.audioUri) | "\(.customId) \(.audioUri)"' results.json \ | while read -r ID URL; do curl -s -o "$ID.mp3" "$URL"; done ``` ```python Python import time import requests BASE = "https://api.inworld.ai" HEADERS = {"Authorization": "Basic "} def item(custom_id, text, voice_id): return { "customId": custom_id, "request": { "text": text, "voiceId": voice_id, "modelId": "inworld-tts-2", "audioConfig": {"audioEncoding": "MP3"}, }, } # 1. Submit operation = requests.post( f"{BASE}/tts/v1/voice:synthesizeBatch", headers=HEADERS, json={ "items": [ item("chapter-01", "The first chapter begins on a quiet morning.", "Ashley"), item("chapter-02", "The second chapter opens with a storm at sea.", "Dennis"), ] }, ).json() # 2. Poll while not operation.get("done"): time.sleep(5) operation = requests.get( f"{BASE}/lro/v1alpha/{operation['name']}", headers=HEADERS ).json() if "error" in operation: raise RuntimeError(f"Batch failed: {operation['error']['message']}") # 3. Download the results file, then each item's audio # (signed URLs — no Authorization header) results = requests.get(operation["response"]["resultsUri"]).json() # failedItems is omitted rather than sent as 0 when everything succeeded. if results.get("failedItems", 0): for entry in results["results"]: if "error" in entry: print(f"{entry['customId']} failed: {entry['error']['message']}") for entry in results["results"]: if "audioUri" in entry: audio = requests.get(entry["audioUri"]).content open(f"{entry['customId']}.mp3", "wb").write(audio) ``` ## Tracking progress While a batch is running, its operation carries a `metadata` object counting the items it has finished. A batch can occupy the queue for a long time. Compare the counters across polls to see whether more items have finished. Unchanged counters do not tell you whether the batch is waiting, still processing a group, or stalled. `done: false` only tells you that the operation has not finished. ```json { "name": "workspaces/my-workspace/ttsBatchJobs/8f3c.../operations/1a2b...", "done": false, "metadata": { "@type": "type.googleapis.com/ai.inworld.tts.v1.SynthesizeSpeechBatchMetadata", "totalItems": 120, "completedItems": 50, "failedItems": 2, "createTime": "2026-08-21T10:04:11Z" } } ``` | Field | Meaning | | --- | --- | | `totalItems` | Items in the batch. Fixed when the batch is accepted, so it is readable before any work starts. | | `completedItems` | Items synthesized successfully so far. | | `failedItems` | Items that finished with an error so far. These do **not** fail the batch — see [When items fail](#when-items-fail). | | `createTime` | When the batch was accepted. | Everything not yet counted is `totalItems - completedItems - failedItems`. That remainder mixes items being synthesized right now with items not started, and the counters cannot tell them apart. **The counters move in steps, not one item at a time.** Items are synthesized in groups, and the counts advance a whole group at once. A 120-item batch reports 0, then 50, then 100, then 120, rather than climbing smoothly. Use them for coarse progress and a stall signal, not for a per-item progress bar. An item's own outcome is only in the results file, after the batch is done. The counters are also on the terminal operation, so a completed or failed batch still says how far it got. Reading progress is the same `GET` you already poll with — no extra request: ```python while not operation.get("done"): time.sleep(5) operation = requests.get( f"{BASE}/lro/v1alpha/{operation['name']}", headers=HEADERS ).json() # Zero-valued fields are omitted, so read every counter with a default. progress = operation.get("metadata", {}) done_so_far = progress.get("completedItems", 0) + progress.get("failedItems", 0) print(f"{done_so_far}/{progress.get('totalItems', 0)} items") ``` **A zero counter may be missing rather than `0`, depending on which response you are reading.** The submit response spells zeros out (`"completedItems": 0`); the poll response omits them, so a batch with no failures has **no** `failedItems` key at all. Read every counter with a default, as in `progress.get("completedItems", 0)` above. Then the difference stops mattering. The same rule applies to `failedItems` in the results file. ## Correlating results `customId` is required, must be unique within the batch, and is the supported way to match a result back to what you submitted. Results also appear in submission order, but prefer the key — it stays correct if you ever submit in a different order than you read. Treat it as opaque. The service never interprets it; it only echoes it back in the results file and in any error naming the item it belongs to. Keep it under 64 characters; longer ids are truncated where they are echoed into error messages. ## When items fail A batch is rejected as a whole, at submit, if **any** item is invalid — an unknown voice, an unsupported model, text past the ceiling. The error names the offending item by both of your handles on it, for example `items[1] (custom_id "chapter-02"): Unknown voice: Nope not found!`. Nothing is synthesized and no job is created, so fix the item and resubmit. Once a batch is running, the rule inverts: an item that fails during synthesis gets an `error` in its results entry instead of an `audioUri`, and the rest of the batch still completes. Check `failedItems` and the per-item `error` field rather than assuming every entry has audio. `failedItems` is **omitted** from the results file rather than sent as `0` when every item succeeds, so read it with a default (`results.get("failedItems", 0)`) rather than testing whether the key is present. The same applies to any zero-valued field in these responses. ### Items that request timestamps An item that sets `timestampType` needs alignment to succeed as well as synthesis. If alignment cannot be produced, the item fails outright. Batch never returns audio with timestamps quietly missing when they were requested. Each item's timestamps file has the same form as an async job's: a JSON array of timestamp objects on one timeline from the start of the item's audio. See [Reading the timestamps document](https://docs.inworld.ai/tts/synthesize-speech-async.md#reading-the-timestamps-document) for how to join it into one alignment. Timestamp availability is tracked separately from synthesis, so a language can in principle synthesize while alignment for it is unavailable. **Wherever that is true, the submit fails before you pay for audio you cannot use due to the lack of timestamps.** Such an item is rejected before any job exists and, like every other invalid item, takes the whole batch with it: ``` INVALID_ARGUMENT: items[1] (custom_id "chapter-02"): timestamps are not available for language 'zz-ZZ'. Omit timestamp_type to synthesize audio without them, or use a supported language. ``` No job is created and nothing is charged. The error names both the item and the language, so you do not need to check anything in advance: if a batch is accepted, timestamps are available for every item in it. Fix the named item and resubmit. An item that passes submit can still fail during synthesis. Two outcomes are worth telling apart, because only one is worth retrying: | Item `error.code` | Meaning | What to do | | --- | --- | --- | | `FAILED_PRECONDITION` | Alignment was not possible for this item — reported as `timestamps are not available for the requested language`. Reaches you rather than the submit rejection when the item's language could not be known at submit, most often because it was auto-detected from the text. | Permanent. Resubmit without `timestampType`, or set an explicit supported `language`. Retrying unchanged fails identically. | | `UNAVAILABLE` | Alignment was temporarily unreachable. | Transient. Resubmit the item. | The synchronous and streaming endpoints behave the **opposite** way: they keep the audio and return success with no `timestampInfo` at all. Only async and batch jobs turn a timestamp failure into a failure of the request. If you are porting code from those endpoints, an item error where you previously saw silently missing timestamps is expected. ## Limits | Limit | Value | | --- | --- | | Models | `inworld-tts-2` only. An item naming any other model is rejected at submit, and takes the whole batch with it | | Items per batch | 1 – 10,000 | | Request size | 16 MiB total, roughly 4M characters — the item ceiling is only reachable with short items | | Text per item | 100,000 characters | | Text per batch | Bounded by the request size above and by your queued-character budget — see below. On-Demand accounts are capped at 10,000 characters across all items of one batch — see [Job Concurrency Limits](https://docs.inworld.ai/resources/job-concurrency-limits.md#per-job-size-on-the-on-demand-plan) | | Result retention | ~7 days from completion, after which every URL in the results file expires | | Concurrent batches | Per account, by plan — see [Job Concurrency Limits](https://docs.inworld.ai/resources/job-concurrency-limits.md) | | Queued characters | Per account, by plan — the total text across all of your unfinished batches | ### How much text fits in one batch There is no single "text per batch" number. Two things bound it, and which one binds depends on your plan: 1. **The request size** — 16 MiB, roughly 4M characters. 2. **Your queued-character budget** — shared across all of your unfinished batches, and a single batch has to fit inside it on its own. A batch bigger than the whole budget is rejected at submit. See [Queued characters](https://docs.inworld.ai/resources/job-concurrency-limits.md#queued-characters-batch-only). Both are checked at submit, so an oversized batch is refused immediately rather than failing later. A larger batch takes proportionally longer to finish. It waits for capacity alongside your other jobs, then works through its items. Where the work divides naturally, several batches finish sooner than one large one, at the same per-character rate, because they are scheduled independently. ### Turnaround We are working toward delivering every batch within 24 hours of acceptance. That is a target we are actively optimizing for rather than a guarantee today, and a very large batch can take longer. If a batch of yours has not completed within 24 hours, contact [support@inworld.ai](mailto:support@inworld.ai) — we would like to hear about it. Your job is not cancelled while you wait: keep polling the operation and it will complete. Submissions over either per-account limit are rejected with a `RESOURCE_EXHAUSTED` error and create nothing — no operation, and no partial synthesis. Retry once earlier batches finish. Because a batch holds up to 10,000 items, prefer packing work into one batch over submitting many small ones, within the sizing guidance above. See [Pack items into one batch](https://docs.inworld.ai/resources/job-concurrency-limits.md#pack-items-into-one-batch). Persist the operation `name` from every submit response. If you lose one, list operations across all of your batch jobs — `GET /lro/v1alpha/ttsBatchJobs/-/operations`, optionally with `filter=-done` for running batches only. See [Listing your jobs](https://docs.inworld.ai/tts/synthesize-speech-async.md#listing-your-jobs) for the full contract. ## API Reference Submit a batch synthesis job Poll a job's operation until it completes List your batch jobs across the workspace ## Next Steps Submit a single long-form job instead of many separate ones. Get word or character timing alignment alongside your audio. Learn best practices for synthesizing high-quality speech. --- ##### Deliver job results to your storage Source: https://docs.inworld.ai/tts/job-results-delivery > Have async and batch jobs upload their results to a bucket you own, on any cloud, with nothing retained by Inworld **Preview.** This capability is a preview release and may be further refined before it is marked stable. See [release stages](https://docs.inworld.ai/portal/support.md) for what that means. By default, an [async](https://docs.inworld.ai/tts/synthesize-speech-async.md) or [batch](https://docs.inworld.ai/tts/synthesize-speech-batch.md) job keeps its results in Inworld storage for about 7 days and returns signed download URLs. With results delivery, you provide a pre-signed upload URL to your own bucket. The job writes its results there as one zip archive. Results only pass through Inworld on the way to your bucket and are not retained. The job is billed once the upload succeeds. It works with any cloud whose storage issues pre-signed upload URLs, including Amazon S3 and S3-compatible services, Google Cloud Storage and Azure Blob Storage. Inworld needs no credentials of yours. The URL is the whole authorization, and it is yours to mint, scope and expire. If your workspace has [Zero Data Retention](https://docs.inworld.ai/portal/zero-data-retention.md) enabled, async and batch jobs **must** use results delivery. A job that names no destination is refused at submit with `FAILED_PRECONDITION`, because results kept in Inworld storage would be retained content. ## How it works Using your own cloud credentials, pre-sign a `PUT` to the object you want the archive written to, for example `tts/job-42.zip`. The signature must stay valid for at least 12 hours for an async job and 48 hours for a batch, and S3 and Google Cloud Storage cap a signature at 7 days. See [Minting a URL](#minting-a-url) for each cloud. Pass the URL as `outputConfig.resultsUri`, on the request for an async job or at the top level of a batch. Submit validates the URL, then writes an empty object to it as a probe, so a URL Inworld cannot write to is refused now rather than after synthesis. That empty object is the marker of an accepted job; the delivery overwrites it. `GET /lro/v1alpha/{name}`, exactly as for a hosted job. When `done` is `true`, a successful `response` carries `resultsUri` and `resultsPath`. The first is your own URL without its signature; the second is the name of the results document inside the archive. There are no signed URLs and no `expireTime`, because nothing is left with Inworld to expire. Fetch the object with your own credentials. It is a zip. Open `resultsPath` inside it and resolve every artifact path it names against the same archive. ```bash cURL # RESULTS_URI holds a URL you pre-signed for PUT (see "Minting a URL"). # It carries a signature, so keep it out of logs and shell history. # 1. Submit OPERATION=$(curl -s 'https://api.inworld.ai/tts/v1/voice:synthesizeAsync' \ --header "Authorization: Basic $INWORLD_API_KEY" \ --header 'Content-Type: application/json' \ --data "$(jq -n --arg uri "$RESULTS_URI" '{ text: "Hello, world! What a wonderful day to be a text-to-speech model!", voiceId: "Dennis", modelId: "inworld-tts-2", audioConfig: { audioEncoding: "MP3" }, timestampType: "WORD", outputConfig: { resultsUri: $uri } }')" | jq -r '.name') # 2. Poll until done: true until curl -s "https://api.inworld.ai/lro/v1alpha/$OPERATION" \ --header "Authorization: Basic $INWORLD_API_KEY" \ | tee operation.json | jq -e '.done == true' > /dev/null; do sleep 5 done # 3. The archive is in your bucket. resultsUri names it without the signature. jq '.response' operation.json # { # "resultsUri": "https://my-bucket.s3.us-east-2.amazonaws.com/tts/job-42.zip", # "resultsPath": "results.json" # } ``` ```python Python import json import os import time import zipfile import requests BASE = "https://api.inworld.ai" HEADERS = {"Authorization": "Basic "} # A URL you pre-signed for PUT (see "Minting a URL"). It carries a signature, # so keep it out of logs. results_uri = os.environ["RESULTS_URI"] # 1. Submit operation = requests.post( f"{BASE}/tts/v1/voice:synthesizeAsync", headers=HEADERS, json={ "text": "Hello, world! What a wonderful day to be a text-to-speech model!", "voiceId": "Dennis", "modelId": "inworld-tts-2", "audioConfig": {"audioEncoding": "MP3"}, "timestampType": "WORD", "outputConfig": {"resultsUri": results_uri}, }, ).json() # 2. Poll while not operation.get("done"): time.sleep(5) operation = requests.get( f"{BASE}/lro/v1alpha/{operation['name']}", headers=HEADERS ).json() if "error" in operation: raise RuntimeError(f"Job failed: {operation['error']['message']}") # 3. The archive is in your bucket at operation["response"]["resultsUri"]. # Fetch it with your own storage SDK or CLI, then read it: archive = zipfile.ZipFile("job-42.zip") results = json.loads(archive.read(operation["response"]["resultsPath"])) audio = archive.read(results["audioPath"]) ``` ## What the archive contains `resultsPath` is always `results.json` today; read it from the response rather than assuming the name, so the layout can gain a version without breaking your integration. Every path in the document is relative to the document itself, so it resolves against the same archive. An async job's archive: ```text results.json audio.mp3 # or the extension of the encoding you requested timestamps.json # only when timestampType was requested ``` with `results.json`, the [results document](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-async.md#results-document), holding the artifact paths: ```json { "audioPath": "audio.mp3", "timestampsPath": "timestamps.json" } ``` A batch's archive holds one entry per successful item, addressed by the item's position in the request: ```text results.json items/0.mp3 items/0.timestamps.json items/1.mp3 ... ``` and `results.json` is the same [results file](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-batch.md#results-file) a hosted batch produces, with paths in place of signed URLs. Correlate by `customId`, never by position: ```json { "totalItems": 3, "completedItems": 2, "failedItems": 1, "results": [ {"customId": "chapter-01", "audioPath": "items/0.mp3", "timestampsPath": "items/0.timestamps.json"}, {"customId": "chapter-02", "audioPath": "items/1.mp3"}, {"customId": "chapter-03", "error": {"code": 3, "message": "text exceeds per-item limit"}} ] } ``` A failed item has no files in the archive; its error is in the document. Every timestamps file has the form described in [Reading the timestamps document](https://docs.inworld.ai/tts/synthesize-speech-async.md#reading-the-timestamps-document). Audio is stored uncompressed inside the zip and JSON documents are deflated, so the archive is about the size of the audio it holds. ## Minting a URL Mint the URL from your own credentials, for a `PUT` to the exact object the archive should be written to. Each URL is single-use in practice: the job overwrites the probe's empty object with the archive, so give every job its own object name. Mint against the bucket's **regional** endpoint. A URL on the global endpoint (`bucket.s3.amazonaws.com`) can answer with a redirect for a bucket outside `us-east-1`, and the submit refuses redirects. ```python import boto3 from botocore.config import Config region = "us-east-2" s3 = boto3.client( "s3", region_name=region, endpoint_url=f"https://s3.{region}.amazonaws.com", config=Config(signature_version="s3v4", s3={"addressing_style": "virtual"}), ) results_uri = s3.generate_presigned_url( "put_object", Params={"Bucket": "my-bucket", "Key": "tts/job-42.zip"}, ExpiresIn=3 * 24 * 3600, # seconds; at most 7 days ) ``` The signing identity needs `s3:PutObject` on the key. A URL signed with temporary credentials (an assumed role, AWS SSO) stops working when those credentials expire, whatever `ExpiresIn` says. Sign with an IAM user's key or a role session that outlives the job. The `aws s3 presign` command mints `GET` URLs only. ```python from datetime import timedelta from google.cloud import storage blob = storage.Client().bucket("my-bucket").blob("tts/job-42.zip") results_uri = blob.generate_signed_url( version="v4", method="PUT", expiration=timedelta(days=3), # at most 7 days ) ``` Signing needs a private key or the IAM `signBlob` permission on a service account; the service account needs `storage.objects.create` on the bucket. The equivalent command is `gcloud storage sign-url --http-verb=PUT --duration=72h gs://my-bucket/tts/job-42.zip`. ```python from datetime import datetime, timedelta, timezone from azure.storage.blob import BlobSasPermissions, generate_blob_sas account, container, blob = "myaccount", "tts", "job-42.zip" sas = generate_blob_sas( account_name=account, container_name=container, blob_name=blob, account_key="", permission=BlobSasPermissions(create=True, write=True), expiry=datetime.now(timezone.utc) + timedelta(days=3), ) results_uri = f"https://{account}.blob.core.windows.net/{container}/{blob}?{sas}" ``` The SAS must carry the **write** permission (`sp` includes `w`). Create alone is not enough: the delivery overwrites the empty object the submit-time probe wrote, and a create-only SAS cannot overwrite. Container-level SAS URLs work too, pointed at the blob's path. ## Checks at submit Everything about the destination is checked before an operation exists, so a bad URL costs a submit call, never synthesis time. In order: | Check | Refusal | | --- | --- | | The URL is `https`, carries a recognizable signature (`X-Amz-Signature`, `X-Goog-Signature` or an Azure `sig`), and an Azure SAS grants write | `INVALID_ARGUMENT` naming the rule | | The signature stays valid for at least 12 hours (async) or 48 hours (batch) after submit, read from the URL itself | `INVALID_ARGUMENT` naming the expiry and the minimum | | The results are estimated to fit the destination's single-upload limit — 5 GiB on S3, 5,000 MiB on Azure (256 MiB under a SAS API version older than 2019-12-12), effectively unbounded on Google Cloud Storage. The estimate comes from your text, speaking rate and encoding, and errs high | `INVALID_ARGUMENT` naming both sizes; split the job | | A zero-byte `PUT` to the URL succeeds | `FAILED_PRECONDITION` with the HTTP status the destination returned, or `UNAVAILABLE` if it did not answer within 10 seconds | The size check runs before the probe, so a job refused for size leaves nothing in your bucket. ## When delivery fails The upload runs after synthesis. Transient failures such as a `408`, a `429`, a `5xx`, or a dropped connection are retried for a bounded window. Other `4xx` responses, redirects, or a URL whose signature lapsed before the results were ready end the job. The operation then carries an `error`: | Operation `error.code` | Meaning | What to do | | --- | --- | --- | | `FAILED_PRECONDITION` | The destination refused the upload; the message names the object URL (never the signature) and the HTTP status, or says the URL expired | Mint a fresh URL and resubmit | | `UNAVAILABLE` | The destination stayed unreachable through every retry | Resubmit | A job whose delivery fails is not billed and nothing of it is retained, so the synthesis is redone on resubmit. ## Packaging `outputConfig.packaging` chooses between one archive and one file per artifact: | Value | Delivered to `resultsUri` | Hosted by Inworld | | --- | --- | --- | | omitted | `ZIP` | `FILES` — today's per-file signed URLs | | `ZIP` | one archive, as above | one signed URL for an archive in Inworld storage, with `resultsPath` and `expireTime` | | `FILES` | rejected: a pre-signed URL receives exactly one object | today's per-file signed URLs | A delivered job and a hosted job with `packaging: ZIP` produce the same archive, so one integration that reads `resultsUri` and `resultsPath` serves both. A hosted job with the default `FILES` packaging keeps the per-artifact URLs described on the [async](https://docs.inworld.ai/tts/synthesize-speech-async.md) and [batch](https://docs.inworld.ai/tts/synthesize-speech-batch.md) pages, with no `resultsPath`. ## Limits | Limit | Value | | --- | --- | | Destinations per job | One URL, one object, one archive | | Signature validity | At least 12 hours after submit for async, 48 for batch; at most 7 days on S3 and Google Cloud Storage | | Archive size | The destination's single-upload limit — see [Checks at submit](#checks-at-submit) and [How much text fits](#how-much-text-fits) | | Retries | Transient upload failures are retried for a bounded window; refusals are not | ### How much text fits Submit estimates the archive from your text, speaking rate and encoding, and refuses a job whose estimate exceeds the destination's single-upload limit. The estimate errs high on purpose, so these are the most characters one job can carry to each destination at speaking rate 1.0: | Destination | MP3 or Opus, 128 kbps (default) | LINEAR16 or WAV, 48 kHz | | --- | --- | --- | | Amazon S3 (5 GiB) | ~3.3M Latin-script, ~2.5M Korean, ~1.3M Chinese or Japanese characters | ~550K, ~410K, ~230K | | Azure Blob Storage (5,000 MiB) | ~3.2M, ~2.4M, ~1.3M | ~540K, ~400K, ~220K | | Azure, SAS API version before 2019-12-12 (256 MiB) | ~160K, ~120K, ~69K | ~27K, ~20K, ~11K | | Google Cloud Storage | effectively unlimited | effectively unlimited | Chinese and Japanese characters and Korean syllable blocks each take longer to speak than a letter, so fewer fit. A slower speaking rate lowers every figure in proportion: at 0.5, half as many fit. An async job carries at most 100,000 characters, so only a legacy-version Azure SAS can refuse one for size. For a batch, the request size and your queued-character budget usually bind well before these figures. ## API reference Submit an asynchronous synthesis job Submit a batch of synthesis requests --- ##### OpenAI compatibility Source: https://docs.inworld.ai/tts/openai-compatibility > Call Inworld TTS from the OpenAI SDKs by pointing them at Inworld and choosing an Inworld model and voice Inworld TTS serves `POST /v1/audio/speech` in OpenAI's text-to-speech request and response format. If your app uses OpenAI text-to-speech, set the SDK's base URL to Inworld and use your Inworld API key, then set `model` and `voice` to Inworld IDs. Keep the same SDK calls. ## Endpoint ``` https://api.inworld.ai/v1 ``` Change your SDK base URL from `https://api.openai.com/v1` to `https://api.inworld.ai/v1`. In the US region, the same base URL also serves the [LLM Router](https://docs.inworld.ai/router/openai-compatibility.md), so one client configuration covers both LLM and TTS. For [regional deployments](https://docs.inworld.ai/portal/regions.md), use `https://api.eu.inworld.ai/v1` or `https://api.in.inworld.ai/v1` with an API key created in that region's Portal. Regional endpoints are provisioned per organization on Enterprise plans, and keys from the US region do not authenticate against them. The LLM Router is not available in regional deployments. ## Quickstart Use your Inworld [API key](https://docs.inworld.ai/portal/api-key-auth.md) as the SDK's API key. The OpenAI SDKs send it as `Authorization: Bearer `; the endpoint also accepts `Authorization: Basic `. Keep the key on your server. ```python Python from openai import OpenAI client = OpenAI( base_url="https://api.inworld.ai/v1", api_key="YOUR_INWORLD_API_KEY", ) with client.audio.speech.with_streaming_response.create( model="inworld-tts-2", voice="Dennis", input="Welcome back! Your order shipped this morning and should arrive by Thursday.", ) as response: response.stream_to_file("speech.mp3") ``` ```javascript Node import fs from 'node:fs'; import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'https://api.inworld.ai/v1', apiKey: 'YOUR_INWORLD_API_KEY', }); const response = await client.audio.speech.create({ model: 'inworld-tts-2', voice: 'Dennis', input: 'Welcome back! Your order shipped this morning and should arrive by Thursday.', }); fs.writeFileSync('speech.mp3', Buffer.from(await response.arrayBuffer())); ``` ```bash curl curl -X POST https://api.inworld.ai/v1/audio/speech \ -H "Authorization: Basic $INWORLD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "inworld-tts-2", "voice": "Dennis", "input": "Welcome back! Your order shipped this morning and should arrive by Thursday." }' \ --output speech.mp3 ``` ## Request parameters The [API reference](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/create-speech.md) has the full request and response schema. | Parameter | Required | Description | | --- | --- | --- | | `model` | Yes | An Inworld model ID, such as `inworld-tts-2` or `inworld-tts-2-flash`. See [Models](https://docs.inworld.ai/tts/tts-models.md). OpenAI model names such as `tts-1` return `400`. | | `input` | Yes | The text to synthesize, up to **4,000 characters**. Inline markup works as it does in the [Streaming API](https://docs.inworld.ai/tts/synthesize-speech-streaming.md), including [steering tags](https://docs.inworld.ai/tts/capabilities/steering.md), [pause controls](https://docs.inworld.ai/tts/capabilities/pause-controls.md), and [custom pronunciation](https://docs.inworld.ai/tts/capabilities/custom-pronunciation.md). | | `voice` | Yes | An Inworld voice ID, such as `Dennis`, or the ID of one of your custom voices. Voice IDs are case-sensitive. OpenAI voice names such as `alloy` return `404`. List available voices with [List voices](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/list-voices.md). | | `response_format` | No | `mp3` (default), `opus`, `flac`, `wav`, or `pcm`. `aac` is not supported. | | `speed` | No | Speaking rate from `0.5` to `1.5`. Default `1.0`. Values outside this range return `400`. | | `instructions` | No | A natural-language [steering](https://docs.inworld.ai/tts/capabilities/steering.md) instruction for the whole request. Supported on `inworld-tts-2` only; other models ignore it. | | `stream_format` | No | `audio` (default) returns raw audio bytes. `sse` returns server-sent events. | The language is detected from the input text. ## Audio formats All formats are mono. | `response_format` | Encoding | Sample rate | | --- | --- | --- | | `mp3` | MP3 at 128 kbps | 48 kHz | | `opus` | Opus in an Ogg container | 48 kHz | | `flac` | 16-bit FLAC | 48 kHz | | `wav` | 16-bit PCM in a WAV container | 48 kHz | | `pcm` | Raw 16-bit signed little-endian samples, no header | 24 kHz | Audio streams as it is generated, so a `wav` response starts before its length is known. Its RIFF and data chunk size fields are set to `0xFFFFFFFF`, a standard marker for streamed WAV. Most players and decoders read it; if a strict parser rejects the file, rewrite the size fields from the file length. ## Streaming With the default `stream_format`, audio bytes arrive as they are generated, so you can play them before synthesis finishes. Read the response incrementally instead of waiting for the whole body — for example with `with_streaming_response` in Python, or by reading `response.body` in Node. With `stream_format` set to `sse`, the response is a stream of server-sent events. Each `speech.audio.delta` event carries a base64-encoded audio chunk in `audio`, and the stream ends with a `speech.audio.done` event: ``` data: {"type":"speech.audio.delta","audio":"SUQzBAAAAAAAI1RTU0UAAAAPAAAD..."} data: {"type":"speech.audio.done"} ``` ## Errors Errors use OpenAI's error format, so the OpenAI SDKs raise their usual exception types: ```json { "error": { "message": "speed must be between 0.5 and 1.5", "type": "invalid_request_error", "param": "speed", "code": null } } ``` | Status | `type` | Cause | | --- | --- | --- | | `400` | `invalid_request_error` | A missing or invalid parameter. `param` names the parameter when the request failed validation. | | `401` | `authentication_error` | A missing API key, or an invalid key sent as `Bearer`. | | `402` | `insufficient_quota` | No credits remain on the account. | | `403` | `permission_error` | An invalid key sent as `Basic`, or a key without access to the request. | | `404` | `invalid_request_error` | The voice was not found. | | `429` | `rate_limit_error` | A [rate limit](https://docs.inworld.ai/resources/rate-limits.md) or [concurrency limit](https://docs.inworld.ai/resources/concurrency-limits.md) was exceeded. | | `5xx` | `api_error` | A server error. Retry with backoff. | ## Billing and limits Requests are billed per character and share [rate limits](https://docs.inworld.ai/resources/rate-limits.md) and [concurrency limits](https://docs.inworld.ai/resources/concurrency-limits.md) with the [Streaming API](https://docs.inworld.ai/tts/synthesize-speech-streaming.md). ## Features that need the native API The OpenAI format has no fields for some Inworld TTS features. Use the [native TTS API](https://docs.inworld.ai/tts/synthesize-speech.md#choose-a-synthesis-method) for: - [Timestamps](https://docs.inworld.ai/tts/capabilities/timestamps.md) for captions and lipsync - Sample rate, bit rate, and other audio encoding settings - [Synthesis context](https://docs.inworld.ai/tts/best-practices/generating-speech.md#give-the-model-conversation-context) from earlier conversation turns - Explicit language selection, [delivery mode](https://docs.inworld.ai/tts/best-practices/generating-speech.md#tune-the-delivery-mode), temperature, and text normalization settings - [Async](https://docs.inworld.ai/tts/synthesize-speech-async.md) and [batch](https://docs.inworld.ai/tts/synthesize-speech-batch.md) jobs --- #### Integrations ##### Node.js SDK Source: https://docs.inworld.ai/tts/node-sdk > Use the @inworld/tts package to add speech synthesis, voice cloning, and voice design to your Node.js app The [`@inworld/tts`](https://www.npmjs.com/package/@inworld/tts) Node.js SDK provides a typed interface to the Inworld TTS REST API. It automatically handles long-text chunking, retries with exponential backoff, and connections. A typical integration takes a few lines of code instead of 30+ lines of raw HTTP. ```bash npm install @inworld/tts ``` ## Quick Start ```javascript import { InworldTTS } from '@inworld/tts'; import fs from 'fs'; const tts = InworldTTS(); // reads INWORLD_API_KEY from env const audio = await tts.generate({ text: 'What a wonderful day to be a text-to-speech model!', voice: 'Ashley', }); fs.writeFileSync('output.mp3', audio); ``` ## Speech Synthesis ### `generate(options)` Synthesize speech and return the complete audio as a `Uint8Array`. Text longer than 2,000 characters is automatically chunked and sent in parallel. ```javascript const audio = await tts.generate({ text: 'Hello, world!', voice: 'Ashley', model: 'inworld-tts-2', encoding: 'MP3', outputFile: 'output.mp3', // optional — also writes to disk }); ``` | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `text` | `string` | Yes | — | Text to synthesize. Any length. Supports `` SSML. | | `voice` | `string` | Yes | — | Voice ID (e.g. `"Ashley"`, `"Dennis"`, or a custom voice ID). | | `model` | `string` | No | `"inworld-tts-2"` | Model ID. | | `encoding` | `string` | No | `"MP3"` | Audio format: `MP3`, `OGG_OPUS`, `FLAC`, `LINEAR16`, `WAV`, `PCM`, `ALAW`, `MULAW`. | | `sampleRate` | `number` | No | `48000` | Sample rate in Hz. | | `bitRate` | `number` | No | `128000` | Bit rate in bps (MP3 / OGG_OPUS only). | | `speakingRate` | `number` | No | `1.0` | Speed multiplier (0.5–1.5). | | `language` | `string` | No | — | BCP-47 language tag (e.g. `"en-US"`, `"fr-FR"`) telling the model which language the voice should speak. Auto-detected from the input text when omitted. | | `deliveryMode` | `string` | No | `"BALANCED"` | Trade-off between stability and expressiveness on `inworld-tts-2`: `"STABLE"`, `"BALANCED"`, or `"CREATIVE"`. Ignored on other models. | | `temperature` | `number` | No | `1.0` | Expressiveness (0.0–2.0). Higher = more expressive. **Ignored on `inworld-tts-2`** — use `deliveryMode` instead. | | `outputFile` | `string` | No | — | Write audio to this file path (Node.js only). | | `play` | `boolean` | No | `false` | Play audio immediately after synthesis (Node.js only). | **Returns:** `Uint8Array` — raw audio bytes in the requested encoding. ### `stream(options)` Stream audio chunks over HTTP as they are generated. Lower time-to-first-audio than `generate()`. Text of any length is accepted: longer input is split at sentence boundaries and streamed as sequential requests. See [Long Text](#long-text). ```javascript const chunks = []; for await (const chunk of tts.stream({ text: 'Streaming is great for real-time playback!', voice: 'Ashley', })) { chunks.push(chunk); } const audio = Buffer.concat(chunks); ``` Parameters are the same as [`generate()`](#generateoptions), plus an optional `maxChunkChars` that overrides the default 3,900-character chunk ceiling. See [Long Text](#long-text). **Yields:** `Uint8Array` — audio chunks as they arrive. ### `generateWithTimestamps(options)` Same as `generate()` but also returns word- or character-level timing data. Useful for lip-sync, karaoke, and subtitle alignment. ```javascript const { audio, timestamps } = await tts.generateWithTimestamps({ text: 'Timestamps are useful for lip sync.', voice: 'Ashley', timestampType: 'WORD', }); // timestamps.wordAlignment.words → ['Timestamps', 'are', 'useful', ...] // timestamps.wordAlignment.wordStartTimeSeconds → [0.0, 0.42, 0.61, ...] ``` Takes all the same parameters as [`generate()`](#generateoptions), plus: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `timestampType` | `"WORD"` \| `"CHARACTER"` | Yes | `"WORD"` returns word timing, phonemes, and visemes. `"CHARACTER"` returns per-character timing. | **Returns:** `{ audio: Uint8Array, timestamps: TimestampInfo }` ### `streamWithTimestamps(options)` Stream audio chunks, each paired with optional timestamp data. Text of any length is accepted: longer input is split at sentence boundaries and streamed as sequential requests. See [Long Text](#long-text). ```javascript for await (const chunk of tts.streamWithTimestamps({ text: 'Streaming with timestamps!', voice: 'Ashley', timestampType: 'WORD', })) { // chunk.audio: Uint8Array // chunk.timestamps: TimestampInfo | undefined } ``` Takes all the same parameters as [`stream()`](#streamoptions), plus `timestampType` (required). **Yields:** `{ audio: Uint8Array, timestamps?: TimestampInfo }` ### `play(audio, options)` Play audio from a `Uint8Array` or file path. Encoding is auto-detected from magic bytes unless overridden. ```javascript const audio = await tts.generate({ text: 'Listen to this!', voice: 'Ashley' }); await tts.play(audio); // Or play from a file await tts.play('output.mp3'); ``` | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `audio` | `Uint8Array` \| `string` | Yes | — | Raw audio bytes or a file path (Node.js only). | | `encoding` | `string` | No | auto-detected | Format hint (`"MP3"`, `"WAV"`, etc.). Inferred from extension for file paths. | ## Voice Management ### `listVoices(options)` List available voices, optionally filtered by language. ```javascript const voices = await tts.listVoices(); // Filter by language const enVoices = await tts.listVoices({ lang: 'EN_US' }); const multiLang = await tts.listVoices({ lang: ['EN_US', 'ES_ES'] }); ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `lang` | `string` \| `string[]` | No | Filter by language code(s). Returns all voices when omitted. | **Returns:** `VoiceInfo[]` ### `getVoice(voice)` Get details for a single voice. Works with custom voices in your workspace (cloned or designed voices). ```javascript const voice = await tts.getVoice('my-custom-voice-id'); // voice.voiceId, voice.displayName, voice.langCode, ... ``` **Returns:** `VoiceInfo` ### `cloneVoice(options)` Clone a voice from one or more audio recordings — as little as 3 seconds works, and longer samples (up to 30 seconds) improve similarity. ```javascript const result = await tts.cloneVoice({ audioSamples: ['./recording.wav'], displayName: 'My Cloned Voice', lang: 'EN_US', }); console.log(result.voice.voiceId); // use this ID in generate() ``` | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `audioSamples` | `Array` | Yes | — | Audio files as `Uint8Array`/Buffer, or file paths (Node.js only). WAV or MP3. | | `displayName` | `string` | No | `"Cloned Voice"` | Display name for the cloned voice. | | `lang` | `string` | No | `"EN_US"` | Language code of the recordings. | | `transcriptions` | `string[]` | No | — | Transcriptions aligned with each audio sample. Improves clone quality. | | `description` | `string` | No | — | Voice description. | | `tags` | `string[]` | No | — | Tags for filtering. | | `removeBackgroundNoise` | `boolean` | No | `false` | Apply noise reduction before cloning. | **Returns:** `CloneVoiceResult` — the cloned voice ID is at `result.voice.voiceId`. ### `designVoice(options)` Design a new voice from a text description — no audio recording needed. ```javascript const result = await tts.designVoice({ designPrompt: 'A warm, friendly female voice with a slight British accent', previewText: 'Hello! Welcome to our application.', numberOfSamples: 3, }); // Listen to previews, then publish the one you like const chosenVoice = result.previewVoices[0]; ``` | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `designPrompt` | `string` | Yes | — | Natural-language description of the voice (30–250 characters). | | `previewText` | `string` | Yes | — | Text the generated voice will speak in the preview. | | `lang` | `string` | No | `"EN_US"` | Language code. | | `numberOfSamples` | `number` | No | `1` | Number of preview candidates (1–3). | **Returns:** `DesignVoiceResult` — preview voices at `result.previewVoices`. ### `publishVoice(options)` Publish a designed or cloned voice preview to your library so it can be used in `generate()` and `stream()`. ```javascript const voice = await tts.publishVoice({ voice: chosenVoice.voiceId, displayName: 'My Designed Voice', }); ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `voice` | `string` | Yes | Voice ID from `designVoice()` or `cloneVoice()`. | | `displayName` | `string` | No | Display name for the published voice. | | `description` | `string` | No | Description. | | `tags` | `string[]` | No | Tags for filtering. | **Returns:** `VoiceInfo` ### `migrateFromElevenLabs(options)` Migrate a voice from ElevenLabs to your Inworld workspace. Fetches the voice's audio samples directly from ElevenLabs and clones them into Inworld. No ElevenLabs SDK required. ```javascript const result = await tts.migrateFromElevenLabs({ elevenLabsApiKey: process.env.ELEVEN_LABS_API_KEY, elevenLabsVoiceId: 'abc123', }); console.log(`Migrated "${result.elevenLabsName}" → ${result.inworldVoiceId}`); ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `elevenLabsApiKey` | `string` | Yes | Your ElevenLabs API key. | | `elevenLabsVoiceId` | `string` | Yes | ElevenLabs voice ID to migrate. | **Returns:** `{ elevenLabsVoiceId, elevenLabsName, inworldVoiceId }` ## Configuration Create a client with `InworldTTS()` or the equivalent `createClient()`: ```javascript import { InworldTTS, createClient } from '@inworld/tts'; const tts = InworldTTS(); // reads INWORLD_API_KEY from env const tts = InworldTTS({ apiKey: 'your_key' }); // or pass explicitly const tts = createClient(); // alias for InworldTTS() ``` | Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | `apiKey` | `string` | — | `INWORLD_API_KEY` env var | Inworld API key. Mutually exclusive with `token`. | | `token` | `string` | — | — | JWT token for browser use. Mutually exclusive with `apiKey`. | | `onTokenExpiring` | `() => Promise` | No | — | Called when `token` is about to expire. Return a fresh JWT. | | `dangerouslyAllowBrowser` | `boolean` | No | `false` | Allow `apiKey` in browser environments (key will be visible in DevTools). | | `baseUrl` | `string` | No | `https://api.inworld.ai` | Override the API base URL. | | `timeout` | `number` | No | per-method | Global HTTP timeout in milliseconds. | | `maxRetries` | `number` | No | `2` | Retry attempts on `NetworkError` or 5xx. Uses exponential backoff (1s, 2s, 4s… capped at 16s). `0` disables retries. | | `maxConcurrentRequests` | `number` | No | `2` | Max parallel chunk requests for long-text `generate()`. | | `debug` | `boolean` | No | `false` | Enable debug logging. Also activated by `DEBUG=inworld-tts` env var. | Either `apiKey` or `token` must be provided. If neither is set, a `MissingApiKeyError` is thrown. ## Browser The SDK works in browsers (Vite, webpack 5, Rollup, esbuild) with no extra configuration. Use JWT tokens instead of API keys to keep your credentials safe. ### Authentication with JWT Your backend keeps the API key and issues client tokens through a protected endpoint. For new integrations, prefer the [one-time token flow](https://docs.inworld.ai/portal/ephemeral-tokens.md#connect-a-client-through-your-backend) for direct connections, or a server proxy. The existing SDK pattern below uses a **multi-use session token**, fetched at initialization and refreshed near expiry. Session tokens are [deprecated](https://docs.inworld.ai/portal/session-tokens.md); this pattern is retained for existing integrations. Do not substitute a one-time token into a flow that reuses the same credential for several HTTP requests or reconnects. An expiry callback alone does not provide a fresh token for every attempt. The [server-side session mint](https://docs.inworld.ai/portal/session-tokens.md#server-side-mint-example) shows how your protected endpoint obtains the token. ```javascript import { InworldTTS } from '@inworld/tts'; async function fetchToken() { const res = await fetch('/api/tts-token', { method: 'POST', credentials: 'same-origin', cache: 'no-store', }); if (!res.ok) throw new Error(`Token endpoint failed (${res.status})`); const { token } = await res.json(); return token; } const tts = InworldTTS({ token: await fetchToken(), onTokenExpiring: fetchToken, // called automatically ~5 min before expiry }); ``` The `onTokenExpiring` callback fires automatically when the current token is about to expire. It must return a fresh JWT string. The SDK uses a stale-while-revalidate strategy — requests continue with the current token while the refresh happens in the background. ### Example: text-to-speech button A minimal browser example — user clicks a button, the SDK generates audio and plays it: ```html ``` `play()` must be called inside a user event handler (click, keypress, etc.) due to browser autoplay policies. ### Browser encoding compatibility Not all audio encodings are playable in all browsers. Use `MP3` for the widest compatibility. | Encoding | Chrome | Firefox | Safari | |----------|--------|---------|--------| | `MP3` | Yes | Yes | Yes | | `WAV` | Yes | Yes | Yes | | `OGG_OPUS` | Yes | Yes | No | | `FLAC` | Yes | Yes | No | | `LINEAR16`, `PCM`, `ALAW`, `MULAW` | — | — | — | For `LINEAR16`/`PCM` formats, use the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext) directly with the `Uint8Array` returned by `generate()` instead of `play()`. ### Browser limitations - **`outputFile`** — not supported, throws an error. Use the returned `Uint8Array` directly. - **`play()` with file paths** — not supported, pass a `Uint8Array` instead. - **`cloneVoice()` with file paths** — not supported, pass `Uint8Array` buffers for audio samples. ### Development shortcut For quick prototyping, you can use an API key directly in the browser by setting `dangerouslyAllowBrowser`: ```javascript // ⚠️ Development only — your API key will be visible in DevTools const tts = InworldTTS({ apiKey: 'your_key', dangerouslyAllowBrowser: true, }); ``` Never use `dangerouslyAllowBrowser` in production. Your API key will be visible in browser DevTools and billed to your account. Use a [backend-minted token](https://docs.inworld.ai/portal/authentication.md#pick-a-credential) or a server proxy instead. For one-time tokens, use the direct connection flow linked above and mint a fresh token for every attempt; the legacy SDK example reuses a session token. ## Long Text `generate()` and `generateWithTimestamps()` automatically chunk text longer than 2,000 characters and send chunks in parallel (controlled by `maxConcurrentRequests`). The resulting audio is seamlessly concatenated, and timestamp offsets are merged correctly. `stream()` and `streamWithTimestamps()` accept text of any length. The streaming endpoint takes up to 4,000 characters per request, so the SDK splits longer input at sentence boundaries into chunks of at most 3,900 characters and streams them as sequential requests. The audio arrives as one continuous sequence of chunks. The 100-character margin is deliberate. The SDK splits on estimated audio-equivalent length, where a `` tag counts as roughly 12 characters per second of silence and each CJK character counts as three. That estimate can differ from the raw character count the endpoint enforces. To set your own ceiling, pass `maxChunkChars`. The SDK does not check it against the endpoint limit, so a value above 4,000 produces requests that the API rejects. ## Error Handling The SDK exports three error classes, all extending `InworldTTSError`: ```javascript import { InworldTTS, ApiError, NetworkError, MissingApiKeyError } from '@inworld/tts'; const tts = InworldTTS(); // reads INWORLD_API_KEY from env try { const audio = await tts.generate({ text: 'Hello!', voice: 'Ashley' }); } catch (err) { if (err instanceof MissingApiKeyError) { // No API key or token provided } else if (err instanceof ApiError) { console.error(`HTTP ${err.code}: ${err.message}`, err.details); } else if (err instanceof NetworkError) { console.error(`Network error: ${err.message}`); } else { throw err; } } ``` | Error | When | |-------|------| | `MissingApiKeyError` | No `apiKey` or `token` was provided and `INWORLD_API_KEY` is not set. | | `ApiError` | The API returned a 4xx or 5xx response. Includes `.code` (HTTP status) and `.details`. | | `NetworkError` | Connection failure or timeout. Automatically retried up to `maxRetries` times before throwing. | ## Next Steps Create a personalized voice clone with as little as 3 seconds of audio. The checklist for expressive, fast, production-ready TTS. View the complete TTS API specification. --- ##### Python SDK Source: https://docs.inworld.ai/tts/python-sdk > Use the inworld-tts package to add speech synthesis, voice cloning, and voice design to your Python app The [`inworld-tts`](https://pypi.org/project/inworld-tts/) Python SDK provides a Pythonic interface to the Inworld TTS REST API. It automatically handles long-text chunking, retries with exponential backoff, and connections. A typical integration takes a few lines of code instead of 30+ lines of raw HTTP. ```bash pip install inworld-tts ``` Requires Python 3.10+. ## Quick Start ```python from inworld_tts import InworldTTS tts = InworldTTS() # reads INWORLD_API_KEY from env tts.generate( text="What a wonderful day to be a text-to-speech model!", voice="Ashley", output_file="output.mp3", ) ``` ## Speech Synthesis ### `generate(options)` Synthesize speech and return the complete audio as `bytes`. Text longer than 2,000 characters is automatically chunked and sent in parallel. ```python audio = tts.generate( text="Hello, world!", voice="Ashley", model="inworld-tts-2", encoding="MP3", output_file="output.mp3", # optional — also writes to disk ) ``` | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `text` | `str` | Yes | — | Text to synthesize. Any length. Supports `` SSML. | | `voice` | `str` | Yes | — | Voice ID (e.g. `"Ashley"`, `"Dennis"`, or a custom voice ID). | | `model` | `str` | No | `"inworld-tts-2"` | Model ID. | | `encoding` | `str` | No | `"MP3"` | Audio format: `MP3`, `OGG_OPUS`, `FLAC`, `LINEAR16`, `WAV`, `PCM`, `ALAW`, `MULAW`. | | `sample_rate` | `int` | No | `48000` | Sample rate in Hz. | | `bit_rate` | `int` | No | `128000` | Bit rate in bps (MP3 / OGG_OPUS only). | | `speaking_rate` | `float` | No | `1.0` | Speed multiplier (0.5–1.5). | | `language` | `str` | No | — | BCP-47 language tag (e.g. `"en-US"`, `"fr-FR"`) telling the model which language the voice should speak. Auto-detected from the input text when omitted. | | `delivery_mode` | `str` | No | `"BALANCED"` | Trade-off between stability and expressiveness on `inworld-tts-2`: `"STABLE"`, `"BALANCED"`, or `"CREATIVE"`. Ignored on other models. | | `temperature` | `float` | No | `1.0` | Expressiveness (0.0–2.0). Higher = more expressive. **Ignored on `inworld-tts-2`** — use `delivery_mode` instead. | | `output_file` | `str` | No | — | Write audio to this file path. | | `play` | `bool` | No | `False` | Play audio immediately after synthesis. | **Returns:** `bytes` — raw audio bytes in the requested encoding. ### `stream(options)` Stream audio chunks over HTTP as they are generated. Lower time-to-first-audio than `generate()`. Text of any length is accepted: longer input is split at sentence boundaries and streamed as sequential requests. See [Long Text](#long-text). ```python chunks = [] async for chunk in tts.stream( text="Streaming is great for real-time playback!", voice="Ashley", ): chunks.append(chunk) audio = b"".join(chunks) ``` Parameters are the same as [`generate()`](#generateoptions), plus an optional `max_chunk_chars` that overrides the default 3,900-character chunk ceiling. See [Long Text](#long-text). **Yields:** `bytes` — audio chunks as they arrive. ### `generate_with_timestamps(options)` Same as `generate()` but also returns word- or character-level timing data. Useful for lip-sync, karaoke, and subtitle alignment. ```python result = tts.generate_with_timestamps( text="Timestamps are useful for lip sync.", voice="Ashley", timestamp_type="WORD", ) # result.audio → bytes # result.timestamps.word_alignment.words → ["Timestamps", "are", "useful", ...] # result.timestamps.word_alignment.word_start_time_seconds → [0.0, 0.42, 0.61, ...] ``` Takes all the same parameters as [`generate()`](#generateoptions), plus: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `timestamp_type` | `"WORD"` \| `"CHARACTER"` | Yes | `"WORD"` returns word timing, phonemes, and visemes. `"CHARACTER"` returns per-character timing. | **Returns:** an object with `audio: bytes` and `timestamps: TimestampInfo`. ### `stream_with_timestamps(options)` Stream audio chunks, each paired with optional timestamp data. Text of any length is accepted: longer input is split at sentence boundaries and streamed as sequential requests. See [Long Text](#long-text). ```python async for chunk in tts.stream_with_timestamps( text="Streaming with timestamps!", voice="Ashley", timestamp_type="WORD", ): # chunk.audio: bytes # chunk.timestamps: TimestampInfo | None pass ``` Takes all the same parameters as [`stream()`](#streamoptions), plus `timestamp_type` (required). **Yields:** objects with `audio: bytes` and optional `timestamps: TimestampInfo`. ### `play(audio, options)` Play audio from `bytes` or a file path. Encoding is auto-detected from magic bytes unless overridden. ```python audio = tts.generate(text="Listen to this!", voice="Ashley") tts.play(audio) # Or play from a file tts.play("output.mp3") ``` | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `audio` | `bytes` \| `str` | Yes | — | Raw audio bytes or a file path. | | `encoding` | `str` | No | auto-detected | Format hint (`"MP3"`, `"WAV"`, etc.). Inferred from extension for file paths. | ## Voice Management ### `list_voices(options)` List available voices, optionally filtered by language. ```python voices = tts.list_voices() # Filter by language en_voices = tts.list_voices(lang="EN_US") multi_lang = tts.list_voices(lang=["EN_US", "ES_ES"]) ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `lang` | `str` \| `list[str]` | No | Filter by language code(s). Returns all voices when omitted. | **Returns:** `list[VoiceInfo]` ### `get_voice(voice)` Get details for a single voice. Works with custom voices in your workspace (cloned or designed voices). ```python voice = tts.get_voice("my-custom-voice-id") # voice.voice_id, voice.display_name, voice.lang_code, ... ``` **Returns:** `VoiceInfo` ### `clone_voice(options)` Clone a voice from one or more audio recordings — as little as 3 seconds works, and longer samples (up to 30 seconds) improve similarity. ```python result = tts.clone_voice( audio_samples=["./recording.wav"], display_name="My Cloned Voice", lang="EN_US", ) print(result.voice.voice_id) # use this ID in generate() ``` | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `audio_samples` | `list[bytes \| str]` | Yes | — | Audio files as `bytes`, or file paths. WAV or MP3. | | `display_name` | `str` | No | `"Cloned Voice"` | Display name for the cloned voice. | | `lang` | `str` | No | `"EN_US"` | Language code of the recordings. | | `transcriptions` | `list[str]` | No | — | Transcriptions aligned with each audio sample. Improves clone quality. | | `description` | `str` | No | — | Voice description. | | `tags` | `list[str]` | No | — | Tags for filtering. | | `remove_background_noise` | `bool` | No | `False` | Apply noise reduction before cloning. | **Returns:** `CloneVoiceResult` — the cloned voice ID is at `result.voice.voice_id`. ### `design_voice(options)` Design a new voice from a text description — no audio recording needed. ```python result = tts.design_voice( design_prompt="A warm, friendly female voice with a slight British accent", preview_text="Hello! Welcome to our application.", number_of_samples=3, ) # Listen to previews, then publish the one you like chosen_voice = result.preview_voices[0] ``` | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `design_prompt` | `str` | Yes | — | Natural-language description of the voice (30–250 characters). | | `preview_text` | `str` | Yes | — | Text the generated voice will speak in the preview. | | `lang` | `str` | No | `"EN_US"` | Language code. | | `number_of_samples` | `int` | No | `1` | Number of preview candidates (1–3). | **Returns:** `DesignVoiceResult` — preview voices at `result.preview_voices`. ### `publish_voice(options)` Publish a designed or cloned voice preview to your library so it can be used in `generate()` and `stream()`. ```python voice = tts.publish_voice( voice=chosen_voice.voice_id, display_name="My Designed Voice", ) ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `voice` | `str` | Yes | Voice ID from `design_voice()` or `clone_voice()`. | | `display_name` | `str` | No | Display name for the published voice. | | `description` | `str` | No | Description. | | `tags` | `list[str]` | No | Tags for filtering. | **Returns:** `VoiceInfo` ### `migrate_from_elevenlabs(options)` Migrate a voice from ElevenLabs to your Inworld workspace. Fetches the voice's audio samples directly from ElevenLabs and clones them into Inworld. No ElevenLabs SDK required. ```python import os result = tts.migrate_from_elevenlabs( eleven_labs_api_key=os.environ["ELEVEN_LABS_API_KEY"], eleven_labs_voice_id="abc123", ) print(f'Migrated "{result.eleven_labs_name}" → {result.inworld_voice_id}') ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `eleven_labs_api_key` | `str` | Yes | Your ElevenLabs API key. | | `eleven_labs_voice_id` | `str` | Yes | ElevenLabs voice ID to migrate. | **Returns:** an object with `eleven_labs_voice_id`, `eleven_labs_name`, and `inworld_voice_id`. ## Configuration Create a client with `InworldTTS()`: ```python from inworld_tts import InworldTTS tts = InworldTTS() # reads INWORLD_API_KEY from env tts = InworldTTS(api_key="your_key") # or pass explicitly ``` | Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | `api_key` | `str` | — | `INWORLD_API_KEY` env var | Inworld API key. | | `base_url` | `str` | No | `https://api.inworld.ai` | Override the API base URL. | | `timeout` | `int` | No | `120` | Global HTTP timeout in seconds. | | `max_retries` | `int` | No | `2` | Retry attempts on `NetworkError` or 5xx. Uses exponential backoff (1s, 2s, 4s… capped at 16s). `0` disables retries. | | `max_concurrent_requests` | `int` | No | `4` | Max parallel chunk requests for long-text `generate()`. | | `debug` | `bool` | No | `False` | Enable debug logging. Also activated by `DEBUG=inworld-tts` env var. | `api_key` must be provided directly or through the `INWORLD_API_KEY` environment variable. If neither is set, a `MissingApiKeyError` is thrown. ## Long Text `generate()` and `generate_with_timestamps()` automatically chunk text longer than 2,000 characters and send chunks in parallel (controlled by `max_concurrent_requests`). The resulting audio is seamlessly concatenated, and timestamp offsets are merged correctly. `stream()` and `stream_with_timestamps()` accept text of any length. The streaming endpoint takes up to 4,000 characters per request, so the SDK splits longer input at sentence boundaries into chunks of at most 3,900 characters and streams them as sequential requests. The audio arrives as one continuous sequence of chunks. The 100-character margin is deliberate. The SDK splits on estimated audio-equivalent length, where a `` tag counts as roughly 12 characters per second of silence and each CJK character counts as three. That estimate can differ from the raw character count the endpoint enforces. To set your own ceiling, pass `max_chunk_chars`. The SDK does not check it against the endpoint limit, so a value above 4,000 produces requests that the API rejects. ## Error Handling The SDK exports three error classes, all extending `InworldTTSError`: ```python from inworld_tts import ( InworldTTS, InworldTTSError, ApiError, NetworkError, MissingApiKeyError, ) tts = InworldTTS() # reads INWORLD_API_KEY from env try: audio = tts.generate(text="Hello!", voice="Ashley") except MissingApiKeyError: # No API key provided pass except ApiError as err: print(f"HTTP {err.code}: {err.message}", err.details) except NetworkError as err: print(f"Network error: {err}") ``` | Error | When | |-------|------| | `MissingApiKeyError` | No `api_key` was provided and `INWORLD_API_KEY` is not set. | | `ApiError` | The API returned a 4xx or 5xx response. Includes `.code` (HTTP status) and `.details`. | | `NetworkError` | Connection failure or timeout. Automatically retried up to `max_retries` times before throwing. | ## Next Steps Create a personalized voice clone with as little as 3 seconds of audio. The checklist for expressive, fast, production-ready TTS. View the complete TTS API specification. --- ##### Integrations Source: https://docs.inworld.ai/tts/integrations > Integrate Inworld's TTS with leading voice platforms Use Inworld TTS with the voice and real-time platforms below to build voice agents and other voice experiences at scale. ## Daily (Pipecat) [Pipecat](https://docs.pipecat.ai/getting-started/introduction) is an open source Python framework for building real-time voice and multimodal AI agents that can see, hear, and speak. Developers can control how AI services, network transports, and audio processing work together in custom pipelines. These pipelines can run locally or in production infrastructure to support natural conversations with low latency. Inworld voices and text-to-speech models are supported via a built-in `InworldTTSService`, allowing you to stream high-quality audio or generate speech on demand from within your own runtime. To get started with Pipecat + Inworld, follow this [guide](https://docs.pipecat.ai/server/services/tts/inworld). ## LiveKit [LiveKit](https://livekit.io/) is an open source platform for developers building realtime agents. It makes it easy to integrate audio, video, text, data, and AI models while offering scalable realtime infrastructure built on top of WebRTC. Inworld voices and text-to-speech models are available as a plugin for LiveKit Agents, a flexible framework for building real-time conversational agents. This makes it easier for developers to create previously unimaginable, real-time voice experiences such as multiplayer games, agentic NPCs, customer-facing avatars, live training simulations, and more at an accessible price. To get started with LiveKit + Inworld, follow this [guide](https://docs.livekit.io/agents/integrations/tts/inworld/). ## NLX NLX is a no-code platform for developers and businesses to build, deploy, and manage conversational AI applications across a variety of channels. It enables the creation of sophisticated, multimodal experiences that can include chat, voice, and video. Inworld TTS is available through NLX as one of the default voice providers or you can build a custom integration. Kickstart your journey with NLX + Inworld by signing up for an NLX account, or dive right in with this how-to [guide](https://docs.nlx.ai/platform/build/integrations/text-to-speech-providers/inworld). ## Stream (Vision Agents) [Stream (Vision Agents)](https://visionagents.ai/) is Stream's [open-source framework](https://github.com/GetStream/vision-agents) that helps developers quickly build low-latency vision AI applications. Since its initial launch, the project has expanded with additional plugins, better model support, and major improvements to latency, audio, and video handling. Stream (Vision Agents) integrates Inworld's state-of-the-art TTS models directly into their platform, giving developers an out-of-the-box way to bring natural, expressive voice to their AI agents. To get started with Stream (Vision Agents) x Inworld, follow this [guide](https://github.com/GetStream/Vision-Agents/tree/main/plugins/inworld). ## Ultravox [Ultravox](https://ultravox.ai) is a real-time voice AI infrastructure layer that delivers fast, natural, and scalable voice agents. Its purpose-built inference stack powers a best-in-class speech understanding model, while developer tools including easy-to-use APIs and client-side SDKs help teams deliver production voice agents faster. Inworld voices are natively integrated with the Ultravox platform and available for use in all accounts, making it easy to create natural, conversational agents with emotionally expressive voices. To get started with Ultravox + Inworld, follow these [instructions](https://docs.ultravox.ai/voices/bring-your-own#inworld). ## Vapi [Vapi](https://vapi.ai/) is a developer platform for building advanced voice AI agents. By handling the complex infrastructure, they enable developers to focus on creating great voice experiences. Inworld's TTS is integrated with Vapi's platform, giving you access to Inworld's high-fidelity, emotionally expressive voices seamlessly on Vapi. To get started with Vapi + Inworld, follow this [guide](https://docs.vapi.ai/api-reference/assistants/create#request.body.voice) ## Voximplant [Voximplant](https://voximplant.ai/) is a serverless Voice AI orchestration platform and cloud communications stack for building real-time voice agents over the phone and the web. It combines programmable telephony (PSTN, SIP, WhatsApp), WebRTC, and client SDKs with a serverless JavaScript runtime (VoxEngine), so developers can efficiently orchestrate calls, speech services, and LLMs in one environment. Inworld's TTS is natively integrated into Voximplant's realtime speech synthesis APIs, enabling low-latency streaming of expressive Inworld voices into any Voximplant-powered call. With a single VoxEngine scenario, you can connect your agent logic to Inworld for speech generation, route calls globally, and rapidly scale from prototype to production. To get started with Voximplant + Inworld, check out this [announcement](https://voximplant.com/blog/inworld-text-to-speech-now-available-in-voximplant). --- ##### Telephony (Twilio) Source: https://docs.inworld.ai/tts/telephony-twilio > Power phone calls with Inworld TTS using Twilio ConversationRelay and LLM Router voice responses Twilio [ConversationRelay](https://www.twilio.com/docs/voice/twiml/connect/conversationrelay) handles call audio and speech-to-text, then streams user transcripts to your server over a WebSocket. Your server calls LLM Router with [voice responses](https://docs.inworld.ai/router/guides/llm-plus-tts.md) (LLM + TTS) to receive the LLM response and synthesized audio in one streaming request. Your server sends the audio back to ConversationRelay for playback. This integration relies on LLM Router's streaming voice responses. Because LLM inference and TTS synthesis happen in one call, the first sentence of audio starts playing before the LLM has finished writing the full response, resulting in improved latency. ## Prerequisites - [Node.js](https://nodejs.org/) v20 or later - [ngrok](https://ngrok.com/) account with a reserved static domain (the free tier is sufficient) - [Twilio](https://www.twilio.com/) account with a phone number that has Voice capability. - [Inworld](https://www.inworld.ai/) account with a Router API key ## Setup The steps below walk through the reference implementation in [inworld-ai/inworld-api-examples](https://github.com/inworld-ai/inworld-api-examples/tree/main/integrations/twilio-conversation-relay). ### 1. Clone the example repo ```bash git clone https://github.com/inworld-ai/inworld-api-examples.git cd inworld-api-examples/integrations/twilio-conversation-relay ``` The remaining steps run from this directory. ### 2. Get your Inworld API key Sign in to the [Inworld Portal](https://portal.inworld.ai/), open your workspace, and create an API key. The same key works for Router (LLM) and TTS because the integration uses the combined LLM + TTS endpoint. ### 3. Get a Twilio phone number In the [Twilio Console](https://console.twilio.com/), buy a phone number with Voice capability. This is the number callers will dial. ### 4. Reserve an ngrok static domain (for local development) Install ngrok and reserve a free static domain in the [ngrok dashboard](https://dashboard.ngrok.com/domains). A static domain matters here because Twilio's webhook URL needs to stay stable between restarts. Without one, every new ngrok session changes the tunnel URL and you have to update the Twilio webhook by hand. ### 5. Configure environment Copy the example env file and fill in the required variables: ```bash cp .env.example .env ``` At minimum, set: ```bash INWORLD_API_KEY=your_inworld_api_key SERVER_URL=https://your-ngrok-domain.ngrok-free.app ``` Optional variables (with sensible defaults) let you change the system prompt, LLM model, TTS voice and model, STT provider, and welcome greeting. See the repo README for the full table. ### 6. Install and run Install dependencies: ```bash npm install ``` Then start the dev server and ngrok in two separate terminals: ```bash npm run dev ``` ```bash ngrok http 3000 --url=your-ngrok-domain.ngrok-free.app ``` ### 7. Point your Twilio number at the webhook In the Twilio Console, go to **Phone Numbers** → **Manage** → **Active Numbers** → your number. Under **Voice Configuration**, set **A call comes in** to **Webhook**, enter `https://your-ngrok-domain.ngrok-free.app/voice`, and choose **HTTP POST**. Save the configuration. ngrok is only needed for local development so Twilio can reach a server running on your machine. Once you deploy the server to production, update the Twilio webhook to point at your server's public URL (for example, `https://voice.yourdomain.com/voice`) and you can drop ngrok entirely. ## How it works - An inbound call hits `/voice`, and the server returns TwiML that hands the call off to ConversationRelay. - ConversationRelay handles the call audio and runs speech-to-text, then opens a WebSocket to your server and streams user transcripts as they arrive. - For each user turn, the server calls the LLM Router API's chat completions endpoint with an `audio` block. The server receives an SSE stream of base64-encoded PCM audio chunks, with each segment's text arriving alongside its audio as a transcript. This is the [LLM + TTS](https://docs.inworld.ai/router/guides/llm-plus-tts.md) feature. - Inworld's chunking engine groups the response into text segments at natural sentence boundaries, and each segment's audio may arrive as multiple base64-encoded PCM chunks. The server assembles each segment's audio, wraps it in a WAV header, and hosts it at a short-lived HTTP URL. The server then sends a `play` message to ConversationRelay. The caller hears the first sentence before the LLM finishes generating the rest. - On an interruption (barge-in), ConversationRelay sends an `interrupt` message and the server aborts the in-flight Inworld stream via `AbortController`. Partial responses already spoken are kept in conversation history so context is preserved. The TwiML returned from `/voice` looks like this: ```xml ``` ## Test your integration Call your Twilio number. The bot should greet you and hold a conversation. ## Example implementation A complete Node.js reference implementation that bridges Twilio ConversationRelay to the LLM Router API with LLM + TTS. ## Further reading How to request combined LLM text and Inworld TTS audio in a single streaming call. Twilio's reference for configuring ConversationRelay, including voice hints, parameters, and language options. Full specification of the WebSocket messages exchanged between ConversationRelay and your server. API reference for fetching available Inworld TTS voice IDs to use in the `audio` block. --- ##### Migrate from ElevenLabs Source: https://docs.inworld.ai/tts/resources/elevenlabs-migration > Batch-migrate your existing ElevenLabs voice clones into Inworld using our open-source migration tool. ## Migrating Voice Clones from ElevenLabs To batch-migrate your ElevenLabs voice clones into Inworld, use the open-source migration tool: [github.com/inworld-ai/voice-migration-tool](https://github.com/inworld-ai/voice-migration-tool). The tool runs locally on your machine and communicates directly with the ElevenLabs and Inworld APIs; it does not proxy your data through any additional intermediary servers. Requires **Node.js** 18+ and **ffmpeg** installed on your machine. Run in your Terminal (macOS/Linux) or Command Prompt (Windows): ```bash git clone https://github.com/inworld-ai/voice-migration-tool.git && cd voice-migration-tool && npm install && npm run dev ``` Then open **http://localhost:3000** in your browser (or the localhost provided in Terminal). Enter your ElevenLabs API key, Inworld API key, and Inworld workspace name. The tool will fetch all your user-created voice clones from ElevenLabs. Only voices you created yourself are shown. ElevenLabs stock and professional voices are excluded. Select the voices you want to migrate, then click **Migrate Selected**. The tool downloads each voice's audio samples from ElevenLabs and re-clones them into your Inworld workspace. Audio samples are automatically processed to meet Inworld's requirements: - Converted to WAV format if needed - Padded to minimum 5 seconds if too short - Trimmed to 30 seconds if too long After migration, click **Preview** on any voice to generate a sample utterance and hear it synthesized with Inworld TTS. This confirms the voice clone is working correctly in your workspace. --- #### Best practices ##### TTS best practices Source: https://docs.inworld.ai/tts/best-practices/overview > Short answers to common questions about making Inworld TTS expressive, fast, and ready for production. Teams building with Inworld TTS ask these three questions most often. Start with the answers below, then follow the links for more detail. ## How do I make it as expressive as possible? 1. **Start from the right voice.** The models are designed to closely follow the voice sample. A calm, flat sample produces calm, flat speech no matter what the text says. Pick or [clone](https://docs.inworld.ai/tts/instant-voice-cloning.md) a voice whose sample has the energy and emotional range you want. 2. **Write for the ear.** Punctuation, capitalization for emphasis, and filler words shape delivery more than any parameter. Speech text is not display text. 3. **Use non-verbal sounds.** Inline tags like `[laugh]`, `[sigh]`, and `[breathe]` make speech sound more human. They work on both `inworld-tts-2` and `inworld-tts-2-flash`. 4. **Steer delivery on `inworld-tts-2`.** [Instruction tags](https://docs.inworld.ai/tts/capabilities/steering.md) like `[say excitedly with a fast pace]` guide emotion, pace, and vocal style. Use them where delivery should change. You don't need them on every sentence. 5. **Give the model context.** In [`synthesisContext`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-synthesis-context), pass the text of earlier requests from the same conversation. This helps short replies like *"Yeah."* use the right intonation. Keep it to the last few turns. Context text is not billed. The full guide: voice selection, writing for speech, non-verbals, steering, context, normalization, and LLM prompting. ## How do I make it as fast as possible? 1. **Pick the right model for your latency budget.** `inworld-tts-2-flash` delivers 20 ms time to first audio byte (P90, measured server-side) — 5× faster than `inworld-tts-2` at 100 ms. Prefer `inworld-tts-2` for the best quality and realism. Its 100 ms TTFB is already fast enough for realtime applications. Choose `inworld-tts-2-flash` only when speed matters most and you don't need [steering](https://docs.inworld.ai/tts/capabilities/steering.md) or [professional voice cloning](https://docs.inworld.ai/tts/professional-voice-cloning.md). 2. **Stream everything.** Use [WebSocket streaming](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-websocket.md) (lowest latency) or [HTTP streaming](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md) and start playback on the first chunk. If an LLM generates your text, stream its output into TTS sentence by sentence instead of waiting for the full response. 3. **Turn off text normalization.** Server-side normalization adds latency to every request. For latency-sensitive applications, set [`applyTextNormalization`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-apply-text-normalization) to `OFF` and have your LLM or application write numbers, dates, and symbols in spoken form instead. 4. **Run your backend nearby.** Time to first audio includes the network round trip between your servers and ours. Run your backend as close to Inworld as your infrastructure allows. Inworld has a default US deployment and [regional deployments](https://docs.inworld.ai/portal/regions.md) in the EU and India. If you need another region, [contact us](https://inworld.ai/contact-sales). 5. **Reuse connections.** Keep connections alive between requests to skip repeated TCP/TLS handshakes, and don't wait for acknowledgments the protocol doesn't require. The full guide: streaming setups, connection reuse, chunking, and WebSocket-specific techniques. ## How do I get ready for my production launch? 1. **Choose the right plan.** [Concurrency limits](https://docs.inworld.ai/resources/concurrency-limits.md) range from 5 concurrent generations on On-Demand to 500 on Growth. Enterprise has custom limits. Compare tiers on the [pricing page](https://inworld.ai/pricing) and make sure your plan covers your expected peak before launch. 2. **Estimate the concurrency you need.** Speech generates much faster than it plays, so one generation slot typically serves several conversations at once. We usually see at least 4× more conversations than slots. Use your traffic patterns to estimate how many slots you need. 3. **Retry limit errors.** Requests over your concurrency limit are best-effort and may be rejected with a `429` (HTTP) or `code: 8` error (WebSocket). Use [retries with backoff](https://docs.inworld.ai/resources/rate-limits.md#handling-rate-limited-requests) to handle these errors. 4. **Choose an API that fits the input.** Non-streaming HTTP accepts up to **2,000 characters per request**, streaming HTTP up to **4,000 characters per request**, and each WebSocket `send_text` message up to **2,000 characters**. For articles, chapters, and other long content, start with the [Async API](https://docs.inworld.ai/tts/synthesize-speech-async.md), which accepts up to 100,000 characters per job (10,000 on On-Demand). See [Long Text Input](https://docs.inworld.ai/tts/capabilities/long-text-input.md) for details and client-side chunking options. How concurrency is counted per protocol, and how to estimate what you need. Limits by plan and retry strategies for rate-limited requests. --- ##### Generating naturally sounding speech Source: https://docs.inworld.ai/tts/best-practices/generating-speech > How to get natural, expressive speech out of Inworld TTS — voice selection, writing for the ear, steering, context, normalization, and LLM prompting. Use this guide to generate natural, expressive speech with Inworld TTS. It covers voice selection, writing text for speech, steering delivery, and prompting an LLM to produce text for TTS. For latency techniques, see the [Latency](https://docs.inworld.ai/tts/best-practices/latency.md) guide. ## Start from the right voice Different voices suit different applications: for a meditation app, pick a steady, calm voice; for an encouraging fitness coach, pick an expressive, excited one. Choose a voice that matches the emotional range you're looking for — no amount of text engineering will make the wrong voice sound right. By default, the model is designed to closely reproduce the voice sample it was given. The delivery you hear (energy, pace, warmth) mirrors the delivery in the sample. An [instant voice clone](https://docs.inworld.ai/tts/instant-voice-cloning.md) made from a calm, flat recording will speak calmly and flatly no matter what the text says. If you need an energetic voice, clone from an energetic sample. [Professional voice cloning](https://docs.inworld.ai/tts/professional-voice-cloning.md) trains on much more audio, so a PVC voice captures a broader expressive range than any single sample can. For recording tips and sample scripts, see [Voice Cloning best practices](https://docs.inworld.ai/tts/best-practices/voice-cloning.md). To push a voice's delivery beyond its sample for a specific line or turn, use [steering](#steer-the-delivery-on-inworld-tts-2) on `inworld-tts-2`. ## Write for the ear Speech text is not display text. The way you write shapes the delivery more than any parameter: 1. **Pay attention to punctuation** — Use exclamation points (!) to make the voice more emphatic and excited. Use periods to insert natural pauses, and commas for shorter breaks. Where possible, include punctuation at the end of every sentence. 2. **Capitalize for emphasis** — Emphasize specific words by capitalizing them: "We NEED a real vacation" stresses *need*, while "We need a REAL vacation" stresses *real*. Capitalizing individual syllables works too: "AbsoLUTEly". Alternatively, wrap a word in single asterisks (`*word*`). Use single asterisks only (`*word*`). Double asterisks (`**word**`) will cause TTS to read the asterisk characters aloud instead of emphasizing the word. 3. **Vary sentence length** — Short sentences speed things up and add urgency; longer sentences slow the pace for calm, measured delivery. 4. **Add filler words for conversational speech** — Natural human conversation is full of `uh`, `um`, `well`, `like`, and `you know`. The models generate the requested text as-is, so if you want conversational imperfection, write it in: "Uh, I'm not uh too sure about that." Skip fillers for formal applications like narration or customer support. 5. **Use pause controls for precise pauses** — When punctuation isn't enough, add SSML break tags like ``. See [Pause Controls](https://docs.inworld.ai/tts/capabilities/pause-controls.md). ## Add non-verbal sounds Non-verbal tags insert organic, human sounds — a laugh, a sigh, a sharp breath — exactly where you place them: > I told him what happened, and he just [laugh] couldn't believe it! They work on **both** `inworld-tts-2` and `inworld-tts-2-flash`. The most reliable ones are `[laugh]` `[breathe]` `[clear throat]` `[sigh]` `[cough]` `[yawn]`; see the [full list of recognized sounds](https://docs.inworld.ai/tts/capabilities/steering.md#recognized-sounds). Place them inline where the sound naturally belongs — each tag produces one occurrence and doesn't change how the surrounding words are spoken. ## Steer the delivery on `inworld-tts-2` On `inworld-tts-2`, [steering](https://docs.inworld.ai/tts/capabilities/steering.md) lets you direct emotion, pacing, volume, and vocal style in natural language. Place instruction tags before the text they apply to: > [say excitedly with a high pitch and fast pace] I have great news. Your package has arrived! Two things to keep in mind: - **Don't overuse it.** The model already reproduces the character of the voice sample; steering is for the moments where the delivery should *change*. A tag stays in force until you change it or write `[reset]`, so one tag at the start covers a whole passage. Re-tagging every sentence gives the model less continuity to work with and can make delivery uneven. - **Match the instruction to the text.** `[sound sad]` on celebratory text sends contradictory signals and degrades output quality. `inworld-tts-2-flash` does not support steering — instruction tags are ignored, though [non-verbal](#add-non-verbal-sounds) tags like `[laugh]` work as usual. ## Give the model conversation context Speech doesn't happen in a vacuum: how *"Yeah."* should sound depends entirely on what came before it. Use the [`synthesisContext`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-synthesis-context) field to pass the text of previous requests from the same session or conversation, in the order they were synthesized: ```json { "text": "Yeah, that works for me.", "voiceId": "Ashley", "modelId": "inworld-tts-2", "synthesisContext": { "previousRequests": [ { "text": "Hey, are you still free to meet on Thursday?" }, { "text": "We could do the morning instead if that's easier." } ] } } ``` Context is most useful for short phrases: a few words alone rarely determine the language, meaning, or situation, and the preceding turns fill that gap. Keep it to the last few turns — a long context can outweigh the text being synthesized and lead to wrong decisions. Context text is not billed: only the `text` field counts toward your character usage. ## Speak the right language Set the [`language`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md#body-language) field for the most consistent results when generating cross-lingual audio, and [localize the voice](https://docs.inworld.ai/tts/capabilities/multilingual.md#voice-localization) for native-sounding speech in the target language. See [Language Support](https://docs.inworld.ai/tts/capabilities/multilingual.md) for details. ## Normalize complex text Text like phone numbers, dollar amounts, and dates often isn't written the way it's spoken. Inworld TTS includes an optional **normalization** step that expands these into their spoken forms before synthesis: - **Phone numbers**: "(123)456-7891" → "one two three, four five six, seven eight nine one" - **Dates**: 5/6/2025 → "may sixth twenty twenty five" - **Times**: "12:55 PM" → "twelve fifty-five PM" - **Emails**: test@example.com → "test at example dot com" - **Monetary values**: $5,342.29 → "five thousand three hundred and forty two dollars and twenty nine cents" - **Symbols**: 2+2=4 → "two plus two equals four" Toggle it with the [`applyTextNormalization`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-apply-text-normalization) parameter: - `ON` — always normalize - `OFF` — skip normalization entirely - `APPLY_TEXT_NORMALIZATION_UNSPECIFIED` (default) — TTS decides per-request Server-side normalization is available for many of the [supported languages](https://docs.inworld.ai/tts/capabilities/multilingual.md#supported-languages). It's language-specific, not locale-specific (e.g., `en-US` and `en-GB` are normalized the same way). For languages where normalization doesn't apply, expand the text yourself before sending it. Normalization adds slight latency to each TTS request. For latency-sensitive applications, expand the text in your LLM prompt or application instead and set `applyTextNormalization` to `OFF`. **With normalization on** (recommended for most cases), the service handles standard expansions. You only need to guide edge cases it may not cover: ambiguous dates (`01/02/2025` could be January 2nd or February 1st), domain-specific abbreviations (`RDS`, `k8s`, `HIPAA`), and uncommon acronyms. **With normalization off**, your application or LLM must handle *all* text expansion — no digits, no symbols, no shorthand. Use this when you need full control over how text is spoken, or when your domain has pronunciation requirements that conflict with the default expansion rules. | Raw Text | Normalization Produces | You Should Produce (Normalization Off) | |---|---|---| | `12/04/2025` | "twelve oh four twenty twenty-five" | "december fourth, twenty twenty-five" | | `(555) 123-4567` | "five five five, one two three, four five six seven" | "five five five, one two three, four five six seven" | | `$1,249.99` | "one thousand two hundred forty-nine dollars and ninety-nine cents" | "twelve hundred forty-nine dollars and ninety-nine cents" | | `3:45 PM` | "three forty-five PM" | "three forty-five PM" | | `test@example.com` | "test at example dot com" | "test at example dot com" | | `2 + 2 = 4` | "two plus two equals four" | "two plus two equals four" | ## Tune the delivery mode For `inworld-tts-2`, use the [`deliveryMode`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md#body-delivery-mode) field to control the trade-off between stability and variability. `STABLE` produces more consistent, predictable output (best when the output must closely match the input). `CREATIVE` produces more varied speech with greater emotional range (useful for creative use cases like barks or demo clips). `BALANCED` (the default) sits in between. ## Prompting your LLM When an LLM generates text that gets fed into TTS, the default output often sounds flat and unnatural. LLMs tend to produce clean, well-formatted text, but clean text isn't the same as *speakable* text: dates stay as `12/04`, acronyms aren't expanded, and there are no cues for emphasis, pauses, or emotion. This section shows what to add to your LLM system prompt so its output follows the practices above. On `inworld-tts-2`, you can go further and have the LLM embed [steering](https://docs.inworld.ai/tts/capabilities/steering.md) tags directly in its output. See [Prompting for TTS-2](https://docs.inworld.ai/tts/best-practices/prompting-for-tts-2.md) for steering-aware prompt templates. ### Emphasis **Prompt snippet:** ``` Use asterisks (*word*) to emphasize key words in your response — focus on prices, deadlines, action items, or any word the listener needs to catch. Use punctuation to convey tone: - Exclamation marks for excitement or urgency - Ellipsis (...) for trailing off, hesitation, or leaving a thought unfinished Example: "I thought it would work, but..." ``` **Before (no emphasis guidance):** > I think this is a really important point and you should consider it carefully. **After (with emphasis guidance):** > I think this is a \*really\* important point, and you should consider it \*carefully\*. ### Pronunciation For uncommon words like brand names, proper nouns, and technical terms, Inworld TTS supports [inline IPA phoneme notation](https://docs.inworld.ai/tts/capabilities/custom-pronunciation.md). You can provide a pronunciation dictionary in your system prompt that the LLM substitutes inline. **Prompt snippet:** ``` When you use any of the following words, replace them with their IPA pronunciation inline using slash notation: - "Crete" → /kriːt/ - "Yosemite" → /joʊˈsɛmɪti/ - "Nguyen" → /ŋwɪən/ - "Acai" → /ɑːsɑːˈiː/ ``` **Before (no pronunciation guidance):** > You should visit Crete for your honeymoon. **After (with IPA substitution):** > You should visit /kriːt/ for your honeymoon. Use standard English IPA phonemes only (not CMU dictionary or ARPAbet) — see [Inline custom pronunciation](https://docs.inworld.ai/tts/capabilities/custom-pronunciation.md) for finding the right phonemes. Alternatively, run a string parser over the LLM output to substitute entries from your pronunciation dictionary before passing text to TTS. This is useful when the same dictionary must apply consistently across multiple LLM providers. ### Pauses and pacing **Prompt snippet:** ``` Control pacing through punctuation and sentence structure: - Use periods to separate thoughts and create pauses - Use commas for shorter breaks within sentences - Use ellipsis (...) to create a lingering pause or beat - Use short sentences for emphasis and urgency - Use longer sentences for calm, measured delivery ``` **Before (flat pacing):** > The results are in and we exceeded our target by 40 percent so this is the best quarter we have ever had. **After (with pacing guidance):** > The results are in. We exceeded our target... by \*forty percent\*. This is the \*best\* quarter we have ever had. ### Conversational naturalness **Prompt snippet:** ``` To sound natural and conversational, include filler words where a human speaker would naturally use them: - "uh" and "um" for thinking moments - "well" and "so" for transitions - "like" and "you know" for casual emphasis Example: "So, uh, I was thinking we could, you know, try a different approach." ``` Filler words are best for casual, conversational use cases. Skip them for formal applications like news reading, professional narration, or customer support. ### Output length LLMs tend to be verbose. A detailed paragraph may read well on screen, but sounds unnatural and exhausting when spoken aloud. Keeping responses short produces better-sounding speech and reduces latency. A good default is to ask your LLM to respond in 1–2 sentences unless the user's query specifically demands a longer answer. Use sentences as your length unit, not words or characters. LLMs operate on tokens, so word and character counts are unreliable constraints. **Prompt snippet:** ``` Keep your responses to 1-2 sentences unless the user's question specifically requires a longer explanation. Prefer concise, direct answers. ``` ### Example prompt templates Copy these system prompt templates for common use cases. Each combines the techniques above. Use this template for chatbots, AI companions, virtual friends, and other informal conversational applications. ``` ## Speech Output Rules Your responses will be converted to speech using TTS. Follow these rules to produce natural, expressive spoken output: ### Expressiveness - Use *asterisks* to emphasize key words - Use exclamation marks for excitement, ellipsis for trailing off - Insert non-verbal tags where organic: [laugh], [sigh], [breathe] ### Naturalness - Include filler words (uh, um, well, like, you know) where a human would naturally pause - Vary sentence length for natural rhythm - Use contractions (don't, can't, I'm, we're) instead of formal forms ### Pronunciation - Replace uncommon proper nouns with IPA: e.g., /kriːt/ for Crete [Add your pronunciation dictionary here] ### Text Formatting - Write numbers in spoken form: "twenty-three" not "23" - Write dates in spoken form: "march fifteenth" not "3/15" - Never use markdown formatting, bullet points, or structured text - Never use emojis or special characters - Write everything as natural spoken sentences ``` Use this template for customer support agents, sales assistants, and other professional conversational applications. ``` ## Speech Output Rules Your responses will be converted to speech using TTS. Follow these rules to produce clear, professional spoken output: ### Clarity - Use *asterisks* sparingly to emphasize critical information (prices, deadlines, action items) - Use short, clear sentences for important details - Use periods to separate distinct points ### Professionalism - Do NOT use filler words (uh, um, like, you know) - Do NOT use non-verbal tags (laugh, sigh, etc.) — maintain professionalism - Maintain a warm but professional tone - Use contractions naturally (don't, we'll, you're) ### Numbers and Data - Speak account numbers digit by digit: "one two three four five six" not "123456" - Speak prices naturally: "forty-nine ninety-nine" or "forty-nine dollars and ninety-nine cents" - Speak dates fully: "january fifteenth, twenty twenty-five" not "1/15/2025" - Speak phone numbers in groups: "five five five, one two three, four five six seven" ### Pronunciation - Replace product names and brand terms with IPA where needed [Add your pronunciation dictionary here] ### Text Formatting - Never use markdown formatting, bullet points, or structured text - Never use emojis or special characters - Write everything as natural spoken sentences ``` Use this template for coding assistants, documentation readers, technical narrators, and developer-facing tools. ``` ## Speech Output Rules Your responses will be converted to speech using TTS. Follow these rules to produce accurate, well-paced technical speech: ### Technical Accuracy - Spell out acronyms on first use: "AWS, or Amazon Web Services" - For common acronyms after first use, speak them as words if pronounceable (e.g., "NASA") or spell them out if not (e.g., "A-P-I") - Speak URLs by component: "github dot com slash inworld dash AI" - Speak code identifiers in plain English: "the getUserName function" not "getUserName()" - Speak version numbers naturally: "version three point two" not "v3.2" ### Pronunciation - Replace technical proper nouns with IPA: [Add your pronunciation dictionary here, e.g.:] - "Kubernetes" → /kuːbərˈnɛtiːz/ - "Nginx" → /ˈɛndʒɪnɛks/ - "PostgreSQL" → /ˈpoʊstɡrɛsˌkjuːˈɛl/ ### Pacing - Use measured, even pacing. Avoid rushing through technical content. - Insert periods before key technical terms to create natural pauses - Keep sentences moderate length - Do NOT use filler words (uh, um, like, you know) ### Text Formatting - Write all numbers in spoken form: "forty-two" not "42" - Never use markdown formatting, bullet points, or code blocks - Never use emojis or special characters - Write everything as natural spoken sentences ``` ### Tips for iterating - **Test with the TTS Playground**: Use the [TTS Playground](https://docs.inworld.ai/tts/tts-playground.md) to quickly hear how your LLM output sounds when synthesized. Paste in sample outputs and iterate on your prompt until the speech quality meets your needs. - **Tune LLM temperature for consistency**: Lower temperatures produce more consistent output that follows your formatting rules reliably. Higher temperatures can produce more expressive text but may ignore specific instructions. Start around `0.7` and adjust based on results. - **Iterate on your pronunciation dictionary**: Start with a small set of terms and expand as you discover mispronunciations during testing. Ask an LLM to generate IPA for new terms. ## Next Steps Have your LLM embed steering tags for expressive, directed speech on inworld-tts-2. Streaming, chunking, and connection techniques for the fastest time to first audio. Best practices for producing high-quality voice clones. --- ##### Prompting for TTS-2 Source: https://docs.inworld.ai/tts/best-practices/prompting-for-tts-2 > Prompt engineering techniques that use inworld-tts-2 steering to produce expressive, directed speech from LLM output. Text generated by an LLM often sounds flat when spoken by TTS. With `inworld-tts-2`, instruct the LLM to add [steering](https://docs.inworld.ai/tts/capabilities/steering.md) tags to its output. These let the LLM direct the speech's emotion, pacing, volume, and vocal style. This page covers what is new for `inworld-tts-2`. The guidance in [Generating Naturally Sounding Speech](https://docs.inworld.ai/tts/best-practices/generating-speech.md#prompting-your-llm) still generally applies as a best practice, especially in cases where no steering instructions are applied. Steering is supported only on `inworld-tts-2`. `inworld-tts-2-flash` does not support steering and ignores instruction tags (e.g. `[say with a hint of amusement]`) instead of interpreting them, though non-verbal tags like `[laugh]` work as usual. For steering, use `inworld-tts-2`. ## Instructing the LLM to use steering The [Steering](https://docs.inworld.ai/tts/capabilities/steering.md) page documents all supported instruction tags across emotion, speed, volume, vocal style, tone, non-verbals, and free-form directions. To make your LLM use them, include a section in your system prompt that explains the tag format and lists the tags relevant to your use case. **Prompt snippet:** ``` Your responses will be spoken aloud using inworld-tts-2, which supports instruction tags — natural language directions in square brackets placed before the text they apply to. Use instruction tags to match your delivery to the content. The following are suggestions; natural language instructions can be used to describe the appropriate delivery: - Emotion: [say excitedly], [sound sad], [sound concerned], [sound terrified] - Articulation: [say with force], [articulate clearly], [say with deliberate pauses] - Intonation: [say with a falling pitch], [say with a rising pitch] - Volume: [very quiet], [very loud] - Pitch: [say in a low tone], [say in a high pitch] - Range: [say playfully], [say with no pitch variation] - Speed: [very fast], [very slow] - Vocal style: [whisper in a hushed style], [give a nasal quality] - Non-verbals: [laugh], [sigh], [clear throat], [breathe], [cough], [yawn] For maximum control, combine qualities from multiple categories in a single natural language instruction. A bare tag like [sound sad] gives the model one dimension to work with. A fuller instruction like [say sadly with deliberate pauses in a low voice and hushed style] layers mood, rhythm, pitch, and mode — producing a more nuanced and convincing performance. Place the tag at the start of the text it applies to. A tag stays in force until you change it, so it applies across as many sentences as follow it — write a new tag only when the delivery should change, and write [reset] where the delivery should return to normal. For example: [shouting] We need to leave now! [reset] Do you understand me? Non-verbal tags can also be used inline where they occur; they produce a sound and do not change the delivery. Do not apply a tag that contradicts the content of the text. Avoid combining opposing directions in the same tag — for example, [whisper in a hushed style] and [very loud] together produce unpredictable results. ``` **Before (no instruction tags):** > I have great news. Your package has arrived. **After (with instruction tags):** > [say excitedly with a high pitch and fast pace] I have great news. Your package has arrived! For the full list of supported tags and examples, see the [Steering](https://docs.inworld.ai/tts/capabilities/steering.md) page. ## Example Prompt Templates Below are complete, copyable system prompt blocks for common use cases. Each template combines steering with the text formatting guidance from [Generating Naturally Sounding Speech](https://docs.inworld.ai/tts/best-practices/generating-speech.md#prompting-your-llm). Use this template for chatbots, AI companions, virtual friends, and other informal conversational applications. ``` ## Speech Output Rules Your responses will be converted to speech using inworld-tts-2. Follow these rules to produce natural, expressive, directed spoken output: ### Instruction Tags - Open with an instruction tag that captures the emotional quality of your response; combine mood, pitch, pacing, and manner for best results: [say excitedly with a high pitch and fast pace], [say sadly with deliberate pauses in a low voice and hushed style], [sound concerned with a measured pace and low tone] - For intimate or private moments, combine volume and manner: [quietly with a warm and gentle tone] - Insert non-verbal tags where organic: [laugh], [sigh], [breathe] - Place tags at the start of the text they apply to. A tag stays in force until you change it — write [reset] where delivery should return to normal ### Emphasis - Capitalize full words for stress: "I told you NOT to do that" - Capitalize syllables for nuance: "AbsoLUTEly" - Use sparingly for maximum effect ### Naturalness - Include filler words (uh, um, well, like, you know) where a human would naturally pause - Vary sentence length for natural rhythm - Use contractions (don't, can't, I'm, we're) instead of formal forms ### Text Formatting - Write numbers in spoken form: "twenty-three" not "23" - Write dates in spoken form: "march fifteenth" not "3/15" - Never use markdown formatting, bullet points, or structured text - Never use emojis or special characters - Write everything as natural spoken sentences ``` Use this template for customer support agents, sales assistants, and other professional conversational applications. ``` ## Speech Output Rules Your responses will be converted to speech using inworld-tts-2. Follow these rules to produce clear, professional, directed spoken output: ### Instruction Tags - When acknowledging a customer's problem, combine concern with pacing: [sound concerned with a measured pace and low tone] - When delivering sensitive information, combine volume and manner: [quietly with a calm and steady tone] - For time-sensitive alerts, combine speed and manner: [speak quickly with a clear and direct manner] - When combining qualities, keep the tone professional and measured - Do NOT use non-verbal tags (laugh, sigh, etc.) — maintain professionalism - Place tags at the start of the text they apply to. A tag stays in force until you change it — write [reset] where delivery should return to normal ### Emphasis - Capitalize key words to draw attention to critical information: "Your order will arrive by FRIDAY" or "This offer expires TONIGHT" - Use sparingly ### Professionalism - Do NOT use filler words (uh, um, like, you know) - Maintain a warm but professional tone - Use contractions naturally (don't, we'll, you're) ### Numbers and Data - Speak account numbers digit by digit: "one two three four five six" - Speak prices naturally: "forty-nine ninety-nine" - Speak dates fully: "january fifteenth, twenty twenty-five" ### Text Formatting - Never use markdown formatting, bullet points, or structured text - Never use emojis or special characters - Write everything as natural spoken sentences ``` Use this template for coding assistants, documentation readers, technical narrators, and developer-facing tools. ``` ## Speech Output Rules Your responses will be converted to speech using inworld-tts-2. Follow these rules to produce accurate, well-paced technical speech: ### Instruction Tags - For urgent alerts, combine speed and manner: [very fast with a sharp and urgent tone] - For critical steps, combine pace and articulation: [very slow with deliberate pauses and clear articulation] - When flagging errors or risks, combine concern with pacing: [sound concerned with a measured pace and low tone] - Do NOT use non-verbal tags — maintain a focused, technical delivery - Place tags at the start of the text they apply to. A tag stays in force until you change it — write [reset] where delivery should return to normal ### Emphasis - Capitalize key technical terms or required actions: "you MUST run this as root" ### Technical Accuracy - Speak URLs by component: "github dot com slash inworld dash AI" - Speak code identifiers in plain English: "the getUserName function" - Speak version numbers naturally: "version three point two" ### Pacing - Use measured, even pacing. Avoid rushing through technical content. - Use periods to separate distinct steps or key terms - Do NOT use filler words (uh, um, like, you know) ### Text Formatting - Write all numbers in spoken form: "forty-two" not "42" - Never use markdown formatting, bullet points, or code blocks - Write everything as natural spoken sentences ``` ## Tips for Iterating - **Test with the TTS Playground**: Use the [TTS Playground](https://docs.inworld.ai/tts/tts-playground.md) to hear how your LLM output sounds when synthesized. Paste in sample outputs with instruction tags and iterate until the speech quality meets your needs. - **Check for tag/content mismatches**: The LLM should not add an instruction tag that contradicts the content. A `[sound sad]` tag on celebratory text will reduce output quality. Check for these mismatches during testing. - **Avoid conflicting instructions**: Instruct the LLM not to combine opposing directions in the same tag. Pairing `[whisper in a hushed style]` with `[very loud]` produces unpredictable results. One clear instruction per tag is the rule. ## Next Steps Full reference for all instruction tags, free-form instructions, non-verbals, and best practices. Add precise pauses to your speech with SSML break tags. Voice selection, writing for the ear, and LLM prompting techniques that apply to all Inworld TTS models. --- ##### Latency Source: https://docs.inworld.ai/tts/best-practices/latency > Best practices for minimizing latency in realtime TTS applications Use these techniques to reduce latency in realtime applications: 1. **Pick the fastest model** - `inworld-tts-2-flash` delivers 20 ms time to first audio byte (P90, measured server-side) — 5× faster than `inworld-tts-2` at 100 ms. If your use case doesn't need [steering](https://docs.inworld.ai/tts/capabilities/steering.md) or [professional voice cloning](https://docs.inworld.ai/tts/professional-voice-cloning.md), Flash is the fastest (and cheapest) choice. 2. **Stream TTS output** - You can start playback as soon as the first chunk arrives. The user does not have to wait for the entire generation, which may take time for long output. Inworld's [websocket streaming](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-websocket.md) should be the lowest-latency option, but [streaming over HTTP](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md) will also be superior to a [non-streaming setup](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md). 3. **Chunk streaming LLM output into TTS** - For the fastest time to first audio, consider breaking streaming LLM output into sentence chunks and sending them one by one to TTS. The Inworld Agent Runtime provides [built-in tools](https://docs.inworld.ai/node/runtime-reference/classes/graph_dsl_nodes_text_chunking_node.TextChunkingNode.md) to handle this in a performant manner. If the text comes from an LLM, [voice responses](https://docs.inworld.ai/router/guides/llm-plus-tts.md) do the chunking for you: one request to LLM Router returns the LLM's answer as streamed audio. 4. **Turn off text normalization** - Server-side [text normalization](https://docs.inworld.ai/tts/best-practices/generating-speech.md#normalize-complex-text) adds slight latency to each request. For latency-sensitive applications, set [`applyTextNormalization`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream.md#body-apply-text-normalization) to `OFF` and have your LLM or application write numbers, dates, and symbols in spoken form instead. 5. **Minimize network distance** - Time to first audio includes the network round trip between your servers and ours, so run your backend as close to Inworld as your infrastructure allows. Besides the default US deployment, Inworld runs [regional deployments](https://docs.inworld.ai/portal/regions.md) in the EU (`api.eu.inworld.ai`) and India (`api.in.inworld.ai`) that remove the intercontinental round trip for region-local users. If you need a region we don't cover yet, [drop us a note](https://inworld.ai/contact-sales). 6. **Use JWT authentication to stream directly to the client** - For applications like mobile apps or browser-based experiences, use [JWT authentication](https://docs.inworld.ai/portal/authentication.md) to stream TTS directly to the client rather than proxying through your server and adding extra latency. 7. **Reuse connections with keep-alive** - The first request to the API incurs a TCP and TLS handshake. Use `Connection: keep-alive` (and persistent sessions in Python) to reuse the established connection on subsequent requests. See our [low-latency Python](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/python/example_tts_low_latency_http.py) and [JavaScript](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/js/example_tts_low_latency_http.js) examples for this technique in practice. 8. **Don't wait for the context acknowledgment (WebSocket)** - When using the [WebSocket API](https://docs.inworld.ai/tts/synthesize-speech-websocket.md), messages are processed in order per connection. Send each message as soon as it is ready instead of waiting for the `contextCreated` acknowledgment — waiting adds a full network round trip before the first audio chunk. See our [low-latency WebSocket examples](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/python/example_tts_low_latency_ws.py) for this pattern in practice. 9. **Use ASYNC timestamp transport (WebSocket)** - If you request [word timestamps](https://docs.inworld.ai/tts/capabilities/timestamps.md), set `timestampTransportStrategy: "ASYNC"` so audio chunks are delivered as soon as they are ready, with timestamps following separately. The default `SYNC` mode holds each audio chunk until its timestamps are computed, which delays time-to-first-audio. ## Next Steps See these guides for more TTS best practices: Learn best practices for producing high-quality voice clones. Voice selection, writing for the ear, steering, and LLM prompting for natural speech. Explore Python and JavaScript code examples for TTS integration. --- ##### Voice cloning best practices Source: https://docs.inworld.ai/tts/best-practices/voice-cloning > Learn best practices for producing high-quality voice clones Use these practices to create high-quality voice clones. For the setup steps, see the [Instant Voice Cloning](https://docs.inworld.ai/tts/instant-voice-cloning.md) or [Professional Voice Cloning](https://docs.inworld.ai/tts/professional-voice-cloning.md) guide. Inworld offers instant and professional voice cloning in [Inworld Portal](https://platform.inworld.ai). This guide starts with practices for all voice clones, then covers each type separately. ## General Best Practices 1. **Capture the full range of expression** - Make sure your script and delivery cover the emotions and expressiveness you want the voice to capture. The more variety you include, the better the model will be at recreating those feelings. If the audio is flat, the resulting voice will usually sound monotone as well. Below are some scripts you can use that we've found work well: - *Are you ready to save big? Get set for the sale of the century! Deals and discounts like never before! You won't want to miss this.* - *Every challenge we face is an opportunity in disguise. Wouldn't you agree? So cheer up! It'll all be okay.* - *How have you been? It's been way too long since we last caught up. By the way, I heard about your recent promotion. Congratulations! I'm so excited for you!* 2. **Speak clearly and consistently** - Pronounce each word carefully and avoid filler sounds like sighs or coughs. Try not to have unnaturally long pauses in the middle of your recording, as this can affect the flow of the cloned voice. 3. **Minimize noise** - Record in a quiet environment and keep a reasonable distance from the microphone to reduce echo, plosives, and device noise. The audio shouldn't include sound effects or background music. After recording, listen back to ensure your audio is clean and free of any unwanted sounds. ## Best Practices for Instant Voice Cloning 1. **Use the full prompt length** - Clones work from as little as 3 seconds of audio, but speaker similarity improves with longer samples. Use up to the 30-second limit where possible. 2. **Use high-quality audio** - Record with at least a 22 kHz sample rate and 16-bit depth. 3. **Vary emotion and delivery** - Combine a few short clips that show different expressions into your final clip; use short pauses or crossfades between clips to avoid abrupt cuts. 4. **Use clean audio** - Avoid artifacts, background noise, and non-speech sounds. 5. **Normalize volume** - Keep levels fairly consistent with normal voice variation; avoid clipping due to very high dB. 6. **Avoid mid-word cuts** - Don’t use samples that break in the middle of words. Instant voice cloning may not perform well for less common voices, such as children's voices or unique accents. For those use cases, we recommend professional voice cloning. ## Best Practices for Professional Voice Cloning 1. **Follow the optimal recording specifications** - For the best voice quality, we recommend recording audio with the following specifications: - Audio Format: .wav  - Sampling Frequency: 48 kHz - Bit Rate: 24 bits - Codec: Linear PCM (uncompressed) - Channel(s): 1 (mono) - Loudness Level: -23LUFS ±0.5 LU (compliant with ITU-R BS.1770-3) - Peak Values Level (Max): -5 dBFS using True Peak value (compliant with ITU-R BS.1770-3) - Noise Floor Level (Max): -60dB 2. **Maintain consistent voice delivery** - Keep your voice consistent throughout all recordings. It’s fine to reflect natural variation in speech (such as hesitations, questions, or exclamations), but avoid major changes in accent or style between samples. 3. **Provide ample, high-quality audio** - The minimum required audio is 10 minutes total across all uploaded files, but we recommend providing more for the best results. For best results, include a natural delivery recording and recordings of the emotions or delivery styles you want the cloned voice to capture. They can be in the same file. More clean, high-quality audio generally produces a higher-quality clone. ## Automation via API If you need to clone multiple voices (for example, to support a batch of creators or a pipeline workflow), you can automate voice cloning via the API. - **API reference**: [Clone a voice](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/clone-voice.md) - **Python example**: [example_voice_clone.py](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/python/example_voice_clone.py) - **JavaScript example**: [example_voice_clone.js](https://github.com/inworld-ai/inworld-api-examples/blob/main/tts/js/example_voice_clone.js) Voice cloning has lower rate limits than regular speech synthesis. For details, see [Rate limits](https://docs.inworld.ai/resources/rate-limits.md). --- ##### Voice design best practices Source: https://docs.inworld.ai/tts/best-practices/voice-design > Learn best practices for designing high-quality voices from text descriptions Use these practices to generate high-quality voices with Voice Design. For the setup steps, see the [Voice Design guide](https://docs.inworld.ai/tts/voice-design.md). ## Voice Description Best Practices Your description tells the model what kind of voice to generate. Use these practices to write better descriptions: 1. **Be specific in your description** - Vague descriptions like "a fun voice" may produce less consistent results. Include details about age, gender, language (if not English), accent, pitch, pace, timbre, tone, and emotional quality. We generally recommend structuring your description in this order: *Distinctive Qualities → Gender → Language / Accent → Age → Tone → Delivery Style → Pacing → Additional Qualities → Audio Quality* For example: > *"A soothing, calming female voice with soft American accent, 30-45 years old. Gentle, flowing delivery with natural pauses and smooth transitions. Warm, peaceful tone that creates relaxation without sounding robotic. Perfect broadcast quality audio."* **Freeform** is AI-assisted: your description is turned into a structured voice profile before the voice is generated. If you want more control, click **Review structured profile** to see and edit that profile first, or switch to **Structured** to write it yourself. Over the API, send it as `designPrompt` with `designPromptMode: DESIGN_PROMPT_MODE_VERBATIM` to use it directly — see [Design a voice](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/design-voice.md). 2. **Be specific with age** - If more general terms like "young" and "old" are not producing the desired voice, use more specific age ranges like "mid-20s to early 30s" or "late 60s to early 70s". - For child voices, try specifying exact ages (e.g., "8-10 years old") and emphasize "natural" and "age-appropriate" to avoid over-cutesy results. - For elderly voices, include both the age range and specific texture descriptors ("gravelly," "weathered") along with pacing cues ("slower, deliberate"). 3. **For regional accents, specify the city or region** - For regional accents, always include the specific city or region. For example, write "Boston accent" rather than "Northeast accent." 4. **Describe vocal texture in the middle** - Place descriptions of the vocal texture and timbre (e.g., "raspy," "breathy," "nasally") in the middle of your voice description, never at the end. Use modifiers like "slight," "subtle," or "natural" to prevent over-exaggeration. 5. **End with audio quality** - For the clearest audio quality, include the phrase "Perfect broadcast quality audio.” at the end of your description. This can be especially helpful if the voice includes descriptions like "gravelly", "breathy", or "scratchy" that may be misinterpreted as audio degradation. 6. **Avoid conflicting descriptors** - Don't use conflicting descriptors (e.g., "fast-paced" with "slow, deliberate"), as that may confuse the model. 7. **Experiment with multiple generations** - Each generation produces slightly different results. You may need several attempts to get the voice you want, especially for less common voices such as children, elderly speakers, or specific regional accents. ## Voice Script Best Practices The script shapes the voice that gets generated, as the model will tailor the voice to suit the content it's speaking. If writing your own script, the following best practices will help ensure the best results. 1. **Match the script to the voice** - The model will tailor the voice to the script. Write a script that matches your voice and desired use case. For example, if you're designing a customer support voice, use a script that sounds like a customer support conversation. For accented voices, use words and phrasing typical of that accent. For example, for a British voice, use words like "brilliant," "proper," or "spot on." 2. **Aim for 5-30 seconds** - Aim for a script that generates 5-30 seconds of audio (50-400 characters in English). This gives the voice enough generated audio to use as a reference in future synthesis. 3. **Match the desired language** - Make sure the script is in the desired language (e.g., write a Chinese script if you want the voice to speak Chinese). ## Next Steps Follow the step-by-step guide to design your first voice. Learn best practices for synthesizing high-quality speech with your designed voices. Clone an existing voice with as little as 3 seconds of audio. --- #### Resources ##### TTS release notes Source: https://docs.inworld.ai/release-notes/tts > Product updates and improvements to TTS ## OpenAI-compatible speech endpoint Realtime TTS now serves `POST /v1/audio/speech` in OpenAI's format. Apps built on OpenAI's text-to-speech can switch by pointing the SDK at Inworld and choosing an Inworld model and voice. See [OpenAI Compatibility](https://docs.inworld.ai/tts/openai-compatibility.md). * **Works with the OpenAI SDKs:** The official Python and Node.js SDKs work unchanged, including streaming responses. * **Formats:** `mp3`, `opus`, `flac`, `wav`, and `pcm`, plus server-sent events with `stream_format: "sse"`. * **One base URL:** `https://api.inworld.ai/v1` also serves the [Inworld Router](https://docs.inworld.ai/router/openai-compatibility.md), so one client configuration covers LLM and TTS. ## Realtime TTS-2 Flash Launched Realtime TTS-2 Flash (`inworld-tts-2-flash`), the fastest member of the TTS-2 family — see [Models](https://docs.inworld.ai/tts/tts-models.md): * **Our lowest latency:** 20 ms time to first audio (server-side P90 TTFB, excluding network latency) — 5× faster than `inworld-tts-2` at 100 ms, making it the best choice for latency-critical real-time agents. * **Our lowest cost:** The most cost-efficient model per character, ideal for high-volume workloads. * **Full TTS-2 language coverage:** The same 200+ [languages and locales](https://docs.inworld.ai/tts/capabilities/multilingual.md) as `inworld-tts-2`, plus instant voice cloning and [timestamp alignment](https://docs.inworld.ai/tts/capabilities/timestamps.md). [Steering](https://docs.inworld.ai/tts/capabilities/steering.md) instructions and [Professional Voice Cloning](https://docs.inworld.ai/tts/professional-voice-cloning.md) are supported on `inworld-tts-2` only — use it when you need directed, contextually aware delivery. Non-verbal tags like `[laugh]` work on both models. ## Steering instructions now persist Steering on `inworld-tts-2` follows one rule: **a `[tag]` applies from where you write it until you change it.** See the [Steering](https://docs.inworld.ai/tts/capabilities/steering.md) guide. * **`[reset]`:** New reserved tag that ends a styled passage and returns the voice to its own character for the rest of the text. `[shouting] We need to leave now! [reset] Do you understand me?` shouts only the first sentence. * **Instructions survive pauses:** A `` no longer clears the active instruction. A pause is a pause and never changes delivery. **Behavior change.** An inline `[tag]` previously affected only the text immediately after it and delivery could revert on its own partway through longer text. A tag now stays in force until you change it. If you relied on an instruction wearing off — for example `[shout] Hi. Normal text.` expecting the second sentence unstyled — add `[reset]` where normal delivery should resume. Requests that use no inline tags are unaffected, as are `inworld-tts-1.5-max` and `inworld-tts-1.5-mini`. ## Realtime TTS-2 Launched [Realtime TTS-2](https://inworld.ai/blog/realtime-tts-2) (`inworld-tts-2`), our most powerful and expressive TTS model: * **Natural Language Steering:** Direct any voice with bracketed instructions like `[say excitedly]`, `[whisper in a hushed style]`, or free-form directions like `[speak as if barely holding back rage]`. Covers articulation, intonation, volume, pitch, range, speed, vocal style, and non-verbals (`[laugh]`, `[sigh]`, etc.). See the [Steering](https://docs.inworld.ai/tts/capabilities/steering.md) guide. * **Stronger Multilingual Support:** Production-quality synthesis across 15 languages, plus experimental support for 90+ additional languages. See [Languages](https://docs.inworld.ai/tts/capabilities/multilingual.md). * **Cross-Lingual Voice Synthesis:** Reuse the same voice across multiple languages. For best results, specify the [`language`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md#body-language) field. * **Voice Localization:** Localize your voice for the most consistent, native-sounding speech in a target language. See [Voice Localization](https://docs.inworld.ai/tts/capabilities/multilingual.md#voice-localization). * **Delivery Mode:** New [`deliveryMode`](https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech.md#body-delivery-mode) field (`STABLE`, `BALANCED`, `CREATIVE`) controls the trade-off between consistency and emotional range. * **Updated Voice Design:** Released an updated version of Voice Design with improved generations. See [Voice Design](https://docs.inworld.ai/tts/voice-design.md). ## Inworld TTS 1.5 Launched [Inworld TTS 1.5](https://inworld.ai/blog/introducing-inworld-tts-1-5), our newest generation of realtime TTS models featuring: * **Two New Models:** Our flagship model `inworld-tts-1.5-max` is ideal for most use cases, with the best balance of quality and speed. For use cases where latency is the top priority, we also offer `inworld-tts-1.5-mini`. * **Latency Improvements:** Our new TTS-1.5 models achieve P90 latency for first audio chunk delivery under 250ms for our Max model and under 130ms for our Mini model, a 4x improvement compared to TTS-1. * **More Expressive and More Stable:** TTS-1.5 is 30% more expressive than prior generations and demonstrates a 40% reduction in word error rates. * **Additional Languages:** We've added support for additional languages, including Hindi, Arabic, and Hebrew, bringing total languages supported to 15. ## Updates to Inworld TTS Released an upgraded version of the Inworld TTS models with higher overall quality. * **Speech Quality:** Clearer, more natural speech with smoother pacing and more accurate pronunciation. * **Voice Similarity:** Cloned voices sound closer to the originals, preserving each voice’s unique style. * **Non-English Languages:** More consistent, reliable output across supported non-English languages. * **Inline custom pronunciation:** New support for inline IPA, giving you control over exact word pronunciations. See [Inline custom pronunciation](https://docs.inworld.ai/tts/capabilities/custom-pronunciation.md) for details. --- ### STT #### STT overview Source: https://docs.inworld.ai/stt/overview > High-accuracy, low-latency multilingual speech-to-text for live voice agents. Inworld STT transcribes speech in 30 languages with high accuracy and low latency for live voice agents. Alongside the transcript, it returns Voice Profile signals: age, pitch, emotion, vocal style, and accent. You can tune turn detection or control turns yourself. Use synchronous transcription for complete audio files, and bidirectional WebSocket streaming for live audio. Both run the `inworld/inworld-stt-1` model. Follow the steps to make your first API call. Transcribe a file or your microphone in the Portal. Find code examples for common use cases. **Using AI to code?** Give your assistant the docs index at `https://docs.inworld.ai/llms.txt`. For live search, [add the MCP server](https://docs.inworld.ai/developer-tools/vibe-coding). **Prefer the terminal?** Install the [Inworld CLI](https://docs.inworld.ai/developer-tools/inworld-cli.md) with `npm install -g @inworld/cli`. Use it to transcribe audio and create API keys. AI agents can use it too. ## Your first request Set `INWORLD_API_KEY` to the Base64 credential from the [Portal](https://platform.inworld.ai), then send a 16 kHz, 16-bit mono WAV file: ```python Python import requests import os import base64 with open("input.wav", "rb") as f: audio_content = base64.b64encode(f.read()).decode("utf-8") response = requests.post( "https://api.inworld.ai/stt/v1/transcribe", headers={ "Authorization": f"Basic {os.getenv('INWORLD_API_KEY')}", "Content-Type": "application/json", }, json={ "transcribeConfig": { "modelId": "inworld/inworld-stt-1", "language": "en", "audioEncoding": "LINEAR16", }, "audioData": {"content": audio_content}, }, ) response.raise_for_status() print(response.json()["transcription"]["transcript"]) ``` ```javascript JavaScript import fs from "fs"; const audioContent = fs.readFileSync("input.wav").toString("base64"); const response = await fetch("https://api.inworld.ai/stt/v1/transcribe", { method: "POST", headers: { Authorization: `Basic ${process.env.INWORLD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ transcribeConfig: { modelId: "inworld/inworld-stt-1", language: "en", audioEncoding: "LINEAR16", }, audioData: { content: audioContent }, }), }); const result = await response.json(); console.log(result.transcription.transcript); ``` The [quickstart](https://docs.inworld.ai/stt/quickstart.md) explains this request step by step, adds Voice Profile, and shows how to stream with WebSocket. ## Explore Learn how to use the Portal playground. Every field on the synchronous and WebSocket endpoints. Encodings, sample rates, and file-size limits per endpoint. The 30 supported languages and how language hints work. Automatic end-of-turn detection, or client-controlled turns. Age, pitch, emotion, vocal style, and accent alongside the transcript. Label who said what. Experimental. For a spoken conversation, use the [Realtime API](https://docs.inworld.ai/realtime/overview.md). It runs STT, an LLM, and TTS in one session. For pricing, see [Billing](https://docs.inworld.ai/portal/billing.md#stt). --- #### STT quickstart Source: https://docs.inworld.ai/stt/quickstart > Make your first Inworld STT API request Send an audio file to the STT API and receive a transcript. This quickstart uses Inworld STT (`inworld/inworld-stt-1`), which also offers Voice Profile (age, pitch, emotion, vocal style, accent) and automatic or manual turn-taking. ## Make your first STT API request Create an [Inworld account](https://platform.inworld.ai/signup). In [Inworld Portal](https://platform.inworld.ai/), open **Manage** > [**API Keys**](https://platform.inworld.ai/api-keys) and click **Generate new key**. Choose **Standard**, click **Generate**, then copy **Basic (Base64)**. Set your API key as an environment variable. ```shell macOS and Linux export INWORLD_API_KEY='your-base64-api-key-here' ``` ```shell Windows setx INWORLD_API_KEY "your-base64-api-key-here" ``` The STT API accepts audio in several formats (e.g. MP3, OGG_OPUS, FLAC, LINEAR16). In this quickstart, audio bytes are sent in the request payload as a base64-encoded string — base64 is the transport encoding, not the audio format. To skip base64 and its one-third size overhead, send the file as [multipart](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe.md#sending-raw-audio) or, when streaming, as [binary frames](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe-stream-websocket.md#send-audio-as-binary-frames). Requirements vary by use case: | **Use case** | **Format** | **Notes** | | :--- | :--- | :--- | | File upload (sync) | LINEAR16, MP3, OGG_OPUS, FLAC, AUTO_DETECT | Sample rate can be auto-detected from file headers when possible | | Streaming | LINEAR16 (PCM) | Other encodings are not supported for streaming to minimize latency and preserve quality | **Recommended settings:** - Sample rate: 16,000 Hz (STT performs best at this rate; lower sample rates like 8 kHz contain fewer data points, reducing accuracy) - Bit depth: 16-bit (for LINEAR16) - Channels: Mono (1 channel) For file uploads (MP3, FLAC, OGG_OPUS, WAV), `sampleRateHertz` is optional — the API can auto-detect it from the file header. Sync transcription accepts audio files up to **~16 MB**. The actual duration depends on the encoding (e.g., ~18 minutes of MP3 or ~8 minutes of 16 kHz 16-bit WAV). For larger files, split them into chunks or use the WebSocket streaming endpoint. Audio is sent as a JSON payload with base64-encoded audio content. The API returns the complete transcript when processing is complete (and optionally Voice Profile, when returned by the API). Create a new file `inworld_stt_quickstart.py` or `inworld_stt_quickstart.js` and use the code below. The Inworld model (`inworld/inworld-stt-1`) provides transcription plus optional Voice Profile (age, pitch, emotion, vocal style, accent) and configurable turn-taking for streaming. ```python Python import requests import os import base64 # Sync endpoint URL = "https://api.inworld.ai/stt/v1/transcribe" # Use a 16-bit PCM WAV file (16 kHz, mono) with open("input.wav", "rb") as f: audio_content = base64.b64encode(f.read()).decode("utf-8") payload = { "transcribeConfig": { "modelId": "inworld/inworld-stt-1", "language": "en", "audioEncoding": "LINEAR16", "voiceProfileConfig": { "enableVoiceProfile": True, }, }, "audioData": {"content": audio_content}, } headers = { "Authorization": f"Basic {os.getenv('INWORLD_API_KEY')}", "Content-Type": "application/json", } response = requests.post(URL, headers=headers, json=payload) response.raise_for_status() result = response.json() print("Transcript:", result["transcription"]["transcript"]) # Voice Profile (when returned by the API) if "voiceProfile" in result and result["voiceProfile"]: vp = result["voiceProfile"] if vp.get("age"): print("Age:", vp["age"].get("label"), vp["age"].get("confidence")) if vp.get("pitch"): print("Pitch:", vp["pitch"].get("label"), vp["pitch"].get("confidence")) ``` ```javascript JavaScript import fs from "fs"; import fetch from "node-fetch"; const URL = "https://api.inworld.ai/stt/v1/transcribe"; // Use a 16-bit PCM WAV file (16 kHz, mono) const audioContent = fs.readFileSync("input.wav").toString("base64"); const payload = { transcribeConfig: { modelId: "inworld/inworld-stt-1", language: "en", audioEncoding: "LINEAR16", voiceProfileConfig: { enableVoiceProfile: true, } }, audioData: { content: audioContent } }; const response = await fetch(URL, { method: "POST", headers: { Authorization: `Basic ${process.env.INWORLD_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify(payload) }); const result = await response.json(); console.log("Transcript:", result.transcription.transcript); if (result.voiceProfile) { const vp = result.voiceProfile; if (vp.age) console.log("Age:", vp.age.label, vp.age.confidence); if (vp.pitch) console.log("Pitch:", vp.pitch.label, vp.pitch.confidence); } ``` The response includes the transcript and usage fields, plus optional `voiceProfile` when available. **Response (sync)** | Field | Description | | :--- | :--- | | `transcription.transcript` | The transcribed text | | `transcription.isFinal` | Whether the result is finalized | | `transcription.wordTimestamps` | Per-word timing data (when available) | | `usage` | Usage metrics for billing | | `voiceProfile` | (When returned) Age, pitch, emotion, vocalStyle, accent with `label` and `confidence` | **transcribeConfig** | Field | Type | Required | Description | | :--- | :--- | :--- | :--- | | `modelId` | string | Yes | STT model ID. Use `inworld/inworld-stt-1` for WebSocket and HTTP | | `language` | string | No | ISO 639 language code (e.g. `en`, `ja`). BCP-47 codes like `en-US` are also accepted and converted to the base language. If omitted, the model may auto-detect. See [Language Support](https://docs.inworld.ai/stt/languages.md) for the full list | | `audioEncoding` | string | Yes | One of: `LINEAR16`, `MP3`, `OGG_OPUS`, `FLAC`, `AUTO_DETECT`. For streaming, use `LINEAR16` only | | `sampleRateHertz` | integer | No | Sample rate in Hz. Default 16000. Can be omitted for formats with headers (MP3, FLAC, OGG_OPUS, WAV) | | `numberOfChannels` | integer | No | Channel count. Default 1 | | `prompts` | string[] | No | Custom vocabulary / key terms (names, jargon, acronyms) that bias recognition toward those terms. A soft bias, not a hard keyword lock. Max 100 terms per request; letters, digits, spaces, and `. , ' ( ) : ! ? ; -` only — a term with any other character rejects the whole request. Supported across models. The Realtime API equivalent is `audio.input.transcription.prompts` | | `voiceProfileConfig` | object | No | Voice Profile configuration. See below | **voiceProfileConfig** | Field | Type | Required | Description | | :--- | :--- | :--- | :--- | | `enableVoiceProfile` | bool | Yes | Set to `true` to enable Voice Profile analysis | | `topN` | integer | No | Number of top labels per category to return. Default: 10 | **audioData** | Field | Type | Required | Description | | :--- | :--- | :--- | :--- | | `content` | string | Yes | Base64-encoded audio bytes | ```shell Python pip install requests # if needed python inworld_stt_quickstart.py ``` ```shell JavaScript npm install node-fetch # if needed node inworld_stt_quickstart.js ``` **Example output:** ``` Transcript: Hey, I just wanted to check in on the delivery status for my order. ``` ## Streaming (WebSocket) For real-time microphone or live audio, follow the [WebSocket integration guide](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe-stream-websocket.md) for authentication, browser capture, message examples, and runnable Node.js/Python samples: 1. **First message** must contain `transcribeConfig` (same fields as above, including `voiceProfileConfig` to enable Voice Profile). 2. **Later messages** send `audioChunk` with base64-encoded LINEAR16 (PCM) audio only — or, if the first message added `"audioProtocol": "BINARY"`, the raw PCM bytes as binary WebSocket messages. 3. **Turn and stream end:** - To finalize a speaker turn and continue sending audio, send `endTurn`. - When done, flush the last audio chunk and send `closeStream` once, without a preceding `endTurn`. Keep reading final transcripts and usage until the server closes the stream. **Example first WebSocket message:** ```json { "transcribeConfig": { "modelId": "inworld/inworld-stt-1", "audioEncoding": "LINEAR16" } } ``` Interim transcripts replace the previous interim for the current turn; they are not deltas. Append each final once and clear the interim. Responses stream back as Transcription (interim and final), optional `voiceProfile`, speech events (`speechStarted` when voice activity is detected, `speechStopped` when silence is detected after speech), and finally Usage when the stream is closed. Every server message arrives wrapped in a `result` envelope (errors arrive as `{"error": {"code": 3, "message": "invalid transcribe config"}}`): ```json { "result": { "transcription": { "transcript": "Open the door and let me in, please.", "isFinal": true, "wordTimestamps": [], "voiceProfile": null, "silenceDurationMs": 0 } } } ``` **Streaming endpoint (WebSocket):** `wss://api.inworld.ai/stt/v1/transcribe:streamBidirectional` ## Next Steps Learn about supported models, audio formats, and endpoints. View the complete API specification. --- #### STT Playground Source: https://docs.inworld.ai/stt/stt-playground > Record live or upload audio to transcribe with Inworld STT Try Inworld STT in the STT Playground before writing code. Test language hints and custom vocabulary, and watch Voice Profile analyze the speaker in real time. STT Playground with model, language, custom vocabulary and speaker controls STT Playground with model, language, custom vocabulary and speaker controls ## Get Started In [Portal](https://platform.inworld.ai/), select **Speech-to-Text** from the left-hand side panel. Open **Model** in the right-hand panel to select `inworld/inworld-stt-1`. See [Supported models](https://docs.inworld.ai/stt/overview.md) for its endpoints and language coverage. Press the mic button to transcribe live from your microphone, or use the upload button to transcribe an audio file. The status bar shows **Ready** when the playground can accept audio and **Listening…** while recording. The transcript appears in real time as you speak. When you're done, use the copy button to grab the text, or **Integrate** to view the equivalent API request. ## Advanced Features To control transcription, try these settings: 1. **Language hint** — The **Language** dropdown defaults to **Auto-detect**, which recognizes the spoken language automatically. Select a specific language to give the model a hint when you know the audio's language in advance — this generally improves accuracy, especially for short utterances. See [Language Support](https://docs.inworld.ai/stt/languages.md) for how hints behave. 2. **Custom vocabulary** — To bias recognition toward names, jargon, or acronyms, enter them in **Custom vocabulary** and press Enter. Terms appear as chips, with the active term count below the input. This is a soft bias rather than a hard keyword lock — test it on the terms the baseline actually misses. Up to 100 terms, using letters, digits, spaces, and `. , ' ( ) : ! ? ; -` only — a term with any other character (such as `&`, `_`, or a curly quote) makes the request fail. The equivalent API field is [`prompts`](https://docs.inworld.ai/stt/quickstart.md#make-your-first-stt-api-request). 3. **Speaker diarization** — Turn on the **Speaker diarization** toggle to label who said what in multi-speaker audio. The transcript prefixes each turn with a speaker label (`[Speaker 1]`, `[Speaker 2]`, …). The equivalent API fields are `enableSpeakerDiarization` together with `includeWordTimestamps` — speaker labels are returned on the word timestamps, so both must be enabled. See [Speaker Diarization](https://docs.inworld.ai/stt/speaker-diarization.md) for how labels are assigned and its current experimental status on `inworld/inworld-stt-1`. 4. **Voice Profile** — The panel on the right analyzes the speaker alongside transcription, showing **Age**, **Accent**, **Voice Pitch**, **Vocal Style**, and **Emotion**, each with a confidence score. It populates after transcription begins. See [Voice Profiles](https://docs.inworld.ai/stt/voice-profiles.md) for the full category reference. ## From Playground to Code Click **Integrate** at the top of the playground to generate an API request matching your current playground configuration: model, language hint, vocabulary, speaker diarization, and Voice Profile settings. ## Next Steps Ready to build? Here's where to go next. Make your first STT API call in minutes. Analyze speaker characteristics alongside transcription. Language coverage and hinting behavior. --- #### Capabilities ##### Audio formats and endpoints Source: https://docs.inworld.ai/stt/audio-formats > Audio and video formats, sample rates, file size limits, and endpoints for the STT API. ## Supported audio formats | **Format** | **Sync API** | **WebSocket Streaming** | | :--- | :--- | :--- | | `LINEAR16` (PCM) | | | | `MP3` | | | | `OGG_OPUS` | | | | `FLAC` | | | | `AUTO_DETECT` | | | Use a 16,000 Hz sample rate, 16-bit depth, and mono audio as defaults. For container formats (MP3, FLAC, OGG_OPUS, WAV), `sampleRateHertz` is optional. The API reads the sample rate from the file header. `.m4a` files (audio-only MP4, typically AAC) work on the synchronous and [asynchronous](https://docs.inworld.ai/stt/async-transcription.md) endpoints — send `AUTO_DETECT`. Do not send `MP4` for an audio-only file: that encoding expects a video container, and the request fails on both endpoints. ## Video files You can send a video file to transcribe its audio. The API extracts the audio track and discards the video. You are billed for the audio duration, as with any other recording. | **Format** | **Sync API** | **WebSocket Streaming** | | :--- | :--- | :--- | | `MP4` — also covers `.m4v` and `.mov` | | | | `WEBM` | | | | `MXF` | | | These formats identify the container. The API reads the audio codec from the file: AAC, AC-3, E-AC-3, ALAC, Opus, MP3, or uncompressed PCM. `AUTO_DETECT` works for all of them. You can specify the container to get an earlier, clearer error if the file doesn't match. `.mov` and `.m4v` are the same container as `.mp4`, so send `MP4` for those. `.mkv` is accepted as `WEBM`, which is the same Matroska container. Video files often exceed the synchronous endpoint's **~16 MB** limit. A few minutes of 1080p video can exceed it even when the audio track is small. Use [asynchronous transcription](https://docs.inworld.ai/stt/async-transcription.md) for video. It accepts files up to 512 MB. Use `audioUri` to point to a file in cloud storage instead of uploading it. Sync transcription accepts audio files up to **~16 MB**. How much audio fits depends on the encoding: for example, ~18 minutes of MP3 or ~8 minutes of 16 kHz 16-bit WAV. For larger files, split them into chunks or use the [WebSocket streaming endpoint](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe-stream-websocket.md). STT performs best with 16 kHz audio. Lower sample rates, such as 8 kHz telephony audio, give the model less data and reduce transcription accuracy. Upsampling this audio doesn't improve quality. It fills in values between existing samples without adding new information. ## Endpoints | **Endpoint** | **Method** | **Description** | | :--- | :--- | :--- | | [`/stt/v1/transcribe`](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe.md) | POST | Send complete audio, receive full transcript | | [`/stt/v1/transcribe:streamBidirectional`](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe-stream-websocket.md) | WebSocket | Stream audio in real time, receive transcription chunks as they become available | --- ##### Language support Source: https://docs.inworld.ai/stt/languages > Language support for the Inworld first-party STT model. The Inworld STT model (`inworld/inworld-stt-1`) supports **30 languages** for speech recognition. ## Supported languages | Language | Code | | --- | --- | | Arabic | `ar` | | Cantonese | `yue` | | Chinese | `zh` | | Czech | `cs` | | Danish | `da` | | Dutch | `nl` | | English | `en` | | Filipino | `fil` | | Finnish | `fi` | | French | `fr` | | German | `de` | | Greek | `el` | | Hindi | `hi` | | Hungarian | `hu` | | Indonesian | `id` | | Italian | `it` | | Japanese | `ja` | | Korean | `ko` | | Macedonian | `mk` | | Malay | `ms` | | Persian | `fa` | | Polish | `pl` | | Portuguese | `pt` | | Romanian | `ro` | | Russian | `ru` | | Spanish | `es` | | Swedish | `sv` | | Thai | `th` | | Turkish | `tr` | | Vietnamese | `vi` | ## Specifying a language The `language` field is a **language hint**: it tells the model which language to prefer, but the model may not follow it. The model detects the spoken language from the audio. You can switch languages during a conversation without changing the hint. The field accepts [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language codes (e.g., `en`, `ja`) matching the codes listed in the table above. BCP-47 codes (e.g., `en-US`, `ja-JP`) are also accepted and will be automatically converted to the base ISO 639 language code — for example, `en-US` becomes `en`. Regional variants do not affect recognition behavior. If you know the audio's primary language, a hint generally improves transcription accuracy. This is especially useful for short utterances where auto-detection may lack context. Setting `language` also constrains the **output script** for English, Chinese, Cantonese, Japanese, Korean, Russian, and Hindi. For example, `en` keeps the transcript in Latin script (a name spoken in another language is romanized rather than written in its native script), while `ja` allows Japanese script. This applies only when you set the hint: to let auto-detection follow the spoken language as it changes mid-stream, leave `language` empty. ## Next steps Make your first STT API call and get a transcript. View the complete API specification. --- ##### Turn detection Source: https://docs.inworld.ai/stt/turn-detection > Detect end-of-turn automatically or control turn boundaries manually with the Inworld STT streaming API. Turn detection identifies when a speaker has finished talking, so a voice agent knows when to respond. The STT streaming API detects end-of-turn automatically. You can tune its sensitivity or take full manual control. Turn detection is available on the [WebSocket streaming endpoint](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe-stream-websocket.md). Sync (file upload) transcription processes complete audio files, so turn detection does not apply. ## How it works With `inworld/inworld-stt-1` streaming, turn detection runs by default — no configuration required: 1. As you stream audio, the server returns interim (partial) transcription results. 2. When the server detects end-of-turn (for example, a sustained pause), it finalizes the transcript for that turn (`isFinal: true`). 3. Speech after the turn boundary starts a new transcript. A pause can split the transcript into two final results. The server waits for the configured minimum silence before checking for turn completion. It then ends the turn when the model is sufficiently confident, or when the maximum silence threshold is reached. The server also emits voice-activity events you can use to drive application behavior (e.g., interrupt playback when the user starts speaking): | **Event** | **Meaning** | | :--- | :--- | | `speechStarted` | Voice activity detected in the audio stream | | `speechStopped` | Silence detected after speech has stopped | ## Tuning automatic turn detection Adjust sensitivity via `transcribeConfig` in the first WebSocket message: | **Field** | **Type** | **Default** | **Description** | | :--- | :--- | :--- | :--- | | `endOfTurnConfidenceThreshold` | float | `0.4` | Confidence required to declare end-of-turn. Higher values reduce false positives (fewer premature turn splits) at the cost of slower turn detection. Range: 0.0–1.0 | | `inworldSttV1Config.minEndOfTurnSilenceWhenConfident` | integer (ms) | — | Minimum trailing silence before checking for turn completion. Range: 20–5000 | | `inworldSttV1Config.maxTurnSilence` | integer (ms) | `300` | Trailing silence that ends the turn even if confidence is insufficient, once the minimum silence has been reached. Range: 20–5000 | | `inworldSttV1Config.vadThreshold` | float | `0.15` | Voice activity detection threshold. Range: 0.0–1.0 | Server defaults can change. Set explicit values when tuning behavior; the values below are example settings. For application idle-stop behavior, maintain a client-side timer: `inactivityTimeoutSeconds` is not applied by `inworld/inworld-stt-1`. ```json { "transcribeConfig": { "modelId": "inworld/inworld-stt-1", "audioEncoding": "LINEAR16", "endOfTurnConfidenceThreshold": 0.7, "inworldSttV1Config": { "minEndOfTurnSilenceWhenConfident": 300, "maxTurnSilence": 1200 } } } ``` Keep `minEndOfTurnSilenceWhenConfident` less than or equal to `maxTurnSilence`. The minimum is checked first, so setting it higher than the maximum delays the maximum-silence fallback too. Set both fields when tuning how long the server waits through pauses. Silence thresholds apply to the streamed audio and are evaluated as audio is processed. They do not guarantee transcript delivery or response latency. ## Manual turn control To hand turn control fully to the client, disable server-side voice activity detection by setting `vadThreshold` to `0`: ```json { "transcribeConfig": { "modelId": "inworld/inworld-stt-1", "audioEncoding": "LINEAR16", "inworldSttV1Config": { "vadThreshold": 0 } } } ``` With VAD disabled, the server no longer splits turns automatically. Signal turn boundaries yourself: - Send an `endTurn` message at the end of each speaker turn to finalize the transcript. - When done sending audio, flush the final audio chunk and send `closeStream` once. It finalizes pending audio, so do not send `endTurn` immediately before it. Keep receiving final results and usage until the server closes the stream. With manual turn control, a single turn has a maximum length (currently around 30 seconds; subject to change). Send `endTurn` regularly at natural turn boundaries rather than relying on the limit. ## Choosing a mode | **Mode** | **When to use** | | :--- | :--- | | Automatic (default) | Voice agents and live transcription where the server should decide when the speaker is done | | Automatic, tuned | Environments with background noise, slow speakers, or domain-specific pacing — adjust thresholds to reduce premature or delayed turn splits | | Manual (`vadThreshold: 0`) | Push-to-talk UIs, client-side VAD, or applications with their own turn-taking logic | ## Next steps Full message and configuration schema for the streaming endpoint. Make your first STT API call and get a transcript. --- ##### Asynchronous transcription Source: https://docs.inworld.ai/stt/async-transcription > Submit a long recording as a job and collect the transcript when it finishes Synchronous transcription returns a transcript in one request. Use it when the recording fits in the request body and you can wait for processing. For recordings too large or too long for that approach, use asynchronous transcription: submit the recording, receive a job, and collect the transcript when it finishes. Use it for recordings measured in minutes or hours — meetings, interviews, podcasts, call archives — and for video files, whose audio track it extracts. For live audio, use [streaming transcription](https://docs.inworld.ai/stt/quickstart.md) instead; for a short clip, the synchronous endpoint is simpler. ## How it works `POST /stt/v1/transcribe:async` returns immediately with an **operation** — a handle naming the job, not the transcript. `GET /lro/v1alpha/{operation name}` reports whether the job has finished. Poll every few seconds; a long recording takes a while. A finished operation carries a link to the transcript. The link is signed and needs no credentials. ## Sending the audio Three ways, and the right one depends mostly on size. | Way | Field | Use it when | | :--- | :--- | :--- | | Inline | `audioData.content` | The recording is small. Audio is base64-encoded, which makes the request about a third larger than the file. | | Multipart | `multipart/form-data` | The recording is large. The file is streamed rather than held whole. | | By URL | `audioUri` | The file already lives somewhere reachable, such as cloud storage. | ### Inline ```bash curl -X POST https://api.inworld.ai/stt/v1/transcribe:async \ -H "Authorization: Basic $INWORLD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "transcribeConfig": { "modelId": "inworld/inworld-stt-1", "audioEncoding": "LINEAR16" }, "audioData": { "content": "" } }' ``` ### Multipart Send the config first and the file second. The upload is streamed as it arrives, so a config that comes after the audio is read too late. ```bash curl -X POST https://api.inworld.ai/stt/v1/transcribe:async \ -H "Authorization: Basic $INWORLD_API_KEY" \ -F 'transcribeConfig={"modelId":"inworld/inworld-stt-1","audioEncoding":"LINEAR16"};type=application/json' \ -F "file=@recording.wav" ``` ### By URL ```json { "transcribeConfig": { "modelId": "inworld/inworld-stt-1", "audioEncoding": "LINEAR16" }, "audioUri": "https://storage.googleapis.com/your-bucket/recording.wav?..." } ``` The URL must use `https` and serve the file directly without your credentials. It can be public or include its own authorization, such as a signed cloud-storage URL. **Redirects are refused**, so use the URL that serves the file. Audio and video files are fetched the same way. The submit request waits for the fetch: an unreachable URL fails the submit, not the job, and submitting a long recording takes as long as fetching it. Most convenient sharing links redirect — file-sharing services, shortened URLs, and console URLs such as `storage.cloud.google.com`. Use the direct serving URL, for example `https://storage.googleapis.com//`. ## Configuration `transcribeConfig` takes the same fields as synchronous transcription, with two differences. **Every audio encoding is accepted**, including the compressed formats streaming rejects — `MP3`, `FLAC`, `OGG_OPUS` — because the audio is a stored file rather than a live stream. `AUTO_DETECT` works where the file carries headers. **Video files work too.** Send an `MP4` (also `.m4v` and `.mov`), `WEBM` or `MXF` and its audio track is extracted and transcribed; the video is discarded and you are billed for the audio duration. Use async for video: a few minutes of 1080p exceeds the synchronous endpoint's ~16 MB cap even when the audio is small. An async job accepts up to 512 MB. With `audioUri`, Inworld fetches the file from its URL, so you do not upload it in the submit request. The codec inside the container is read from the file — see [audio formats](https://docs.inworld.ai/stt/audio-formats.md) for the full list. **Live-session fields are ignored**: `inactivityTimeoutSeconds` and `endOfTurnConfidenceThreshold` mean nothing to a recording that is already complete. **Custom vocabulary works here too.** Pass `prompts` exactly as in synchronous transcription, with the same limits: at most 100 terms, each using only letters, digits, spaces, and `. , ' ( ) : ! ? ; -` — a term with any other character (such as `&` or `@`) rejects the submit with `INVALID_ARGUMENT`. See [best practices](https://docs.inworld.ai/stt/best-practices.md) for how to choose terms. [Speaker diarization](https://docs.inworld.ai/stt/speaker-diarization.md), [voice profiles](https://docs.inworld.ai/stt/voice-profiles.md) and word timestamps all work as they do synchronously. Their output lands in the transcript document rather than in the operation — see below. ## Reading the result While the job runs, the operation reports `done: false`. When it finishes: ```json { "name": "workspaces/my-workspace/sttTranscriptionJobs/6f1c.../operations/1790...", "done": true, "response": { "resultUri": "https://storage.googleapis.com/...?X-Goog-Signature=...", "expireTime": "2026-10-01T12:00:00Z", "language": "English", "usage": { "transcribedAudioMs": 7200000 } } } ``` A job that failed carries an `error` instead of a `response`, with a code and a message. Either way the operation is `done`, so polling ends. `resultUri` points at the transcript document: ```json { "jobId": "6f1c...", "transcript": "Full text of the recording...", "language": "English", "segments": [ { "endTimeMs": 7200000, "transcript": "Full text of the recording..." } ], "usage": { "transcribedAudioMs": 7200000, "modelId": "inworld/inworld-stt-1" } } ``` ### Word timestamps and speaker labels Asking for `includeWordTimestamps` adds a `wordTimestamps` array to each segment, and `enableSpeakerDiarization` adds a `speaker` to each word in it: ```json { "segments": [ { "endTimeMs": 25861, "transcript": "Furthermore, our universal fit phone cases...", "wordTimestamps": [ { "word": "Furthermore", "startTimeMs": 60, "endTimeMs": 870, "speaker": 0 }, { "word": "our", "startTimeMs": 1240, "endTimeMs": 1420, "speaker": 0 } ] } ] } ``` Speaker identifiers are small integers starting at `0`, scoped to the one job: they mark "same speaker in this recording", not a person you can recognise across jobs. There is no segment-level speaker field — group words by their `speaker` value to build per-speaker segments. [Voice profiles](https://docs.inworld.ai/stt/voice-profiles.md) arrive the same way. Asking for `voiceProfileConfig.enableVoiceProfile` adds a `voiceProfile` object to each segment. The profile has the same categories and `{ label, confidence }` shape as a synchronous response. `language` is the detected language's name, not a BCP-47 tag. The millisecond fields arrive as JSON numbers. A field with a value of zero is omitted rather than sent. Examples include `startTimeMs` for the first segment or first word. `segments` currently contains a single entry spanning the whole recording. The field is a list so that finer segmentation can be added without changing the document's shape. Do not rely on there being exactly one entry. ## Limits | Limit | Value | | :--- | :--- | | Audio per job | 512 MB | | Jobs in flight per account | Depends on your plan | | Result retention | 7 days from completion, after which the transcript is deleted and its link stops working | | Submitted audio retention | 7 days, after which the recording is deleted | | `audioUri` fetch timeout | 5 minutes, and the submit waits for it | Submitting while you are already at your in-flight limit is refused with `RESOURCE_EXHAUSTED`, **before** the audio is transferred. Wait for a job to finish and submit again. The link is signed once, when the job completes. Polling the operation again returns the same link rather than a fresh one. The seven days run from completion, however late you collect the result. Download the transcript and keep your own copy if you need it after that; neither it nor the submitted audio is a permanent store. ## Errors | What you see | What it means | | :--- | :--- | | `401` / `403` | The API key is missing, wrong, or belongs to a different environment. | | `RESOURCE_EXHAUSTED` | Either the in-flight job limit, or the request rate limit. The message says which. | | `INVALID_ARGUMENT` | A malformed request — check `audioEncoding`, `modelId`, and the `audioUri` rules above. | | `FAILED_PRECONDITION` | The `audioUri` could not be fetched. The request was well formed; the fetch was not. It is answered on the submit, since that is when the fetch happens. | --- ##### Voice profiles Source: https://docs.inworld.ai/stt/voice-profiles > Analyze speaker vocal characteristics alongside transcription using Voice Profile on the Inworld STT API. Voice Profile analyzes vocal characteristics of the speaker alongside transcription. It returns structured classification data for **Age**, **Emotion**, **Pitch**, **Vocal Style**, and **Accent**, each with confidence scores ranging from 0.0 to 1.0. Voice Profile uses a dedicated speech-analysis model that runs alongside transcription. It is not tied to any particular transcription model and is available with every STT model on the Inworld STT API. By understanding _who_ is speaking and _how_ they are speaking, applications can adapt responses, adjust tone, route conversations, or trigger context-sensitive behaviors in real time. ## Use cases - **Voice agents and NPCs** — Adapt responses based on the speaker's detected emotion or vocal style (e.g., respond empathetically to a sad tone). - **Accessibility** — Detect age category or vocal style to adjust UI, pacing, or interaction complexity. - **Content moderation** — Flag unusual vocal patterns (shouting, crying) for escalation or review. - **Analytics and insights** — Aggregate emotion and vocal style data across sessions for user experience analysis. - **Localization** — Use accent detection to dynamically select language models or localized content. ## How it works Voice Profile analysis runs when enabled via `voiceProfileConfig` in your `transcribeConfig` request (HTTP or WebSocket). Set `enableVoiceProfile` to `true` to activate the feature. Optionally use `topN` to control how many top labels per category are returned. ## Language support Voice Profile is trained on 18 languages: | Language | Code | | --- | --- | | Arabic | `ar` | | Cantonese | `yue` | | Chinese (Mandarin) | `zh` | | Dutch | `nl` | | English | `en` | | French | `fr` | | German | `de` | | Hebrew | `he` | | Hindi | `hi` | | Italian | `it` | | Japanese | `ja` | | Korean | `ko` | | Polish | `pl` | | Portuguese | `pt` | | Russian | `ru` | | Spanish | `es` | | Turkish | `tr` | | Ukrainian | `uk` | Classification quality can vary by language. ## Classification categories ### Age Estimates the speaker's age category. Returns an array of labels sorted by descending confidence. | **Label** | **Description** | | :--- | :--- | | `young` | Young adult / teenager | | `adult` | Adult speaker | | `kid` | Child speaker | | `old` | Elderly speaker | | `unclear` | Age could not be determined | ### Emotion Detects emotional tone in the speaker's voice. Returns an array of labels sorted by descending confidence. | **Label** | **Description** | | :--- | :--- | | `tender` | Soft, gentle, caring tone | | `sad` | Sorrowful or melancholy tone | | `calm` | Relaxed, even-tempered delivery | | `neutral` | No strong emotional signal | | `happy` | Cheerful, upbeat tone | | `angry` | Frustrated, aggressive tone | | `fearful` | Anxious or frightened tone | | `surprised` | Startled or astonished tone | | `disgusted` | Revulsion or strong disapproval | | `unclear` | Emotion could not be determined | ### Pitch Classifies the speaker's vocal pitch. Pitch shifts during a conversation can serve as a real-time emotional signal. Rising pitch can correlate with increasing stress, excitement, or urgency. Falling pitch may indicate that the speaker is becoming more withdrawn, tired, or deflated. Returns an array of labels sorted by descending confidence. | **Label** | **Description** | | :--- | :--- | | `low` | Low-pitched voice | | `medium` | Medium-pitched voice | | `high` | High-pitched voice | ### Vocal Style Identifies the speaker's manner of delivery. Returns an array of labels sorted by descending confidence. | **Label** | **Description** | | :--- | :--- | | `whispering` | Hushed, breathy delivery | | `normal` | Standard conversational speech | | `singing` | Melodic or musical delivery | | `mumbling` | Unclear, low-articulation speech | | `crying` | Speech accompanied by crying | | `laughing` | Speech accompanied by laughter | | `shouting` | Loud, raised-voice delivery | | `monotone` | Flat, unvaried pitch delivery | | `unclear` | Vocal style could not be determined | ### Accent Detects the speaker's accent or regional dialect using BCP-47 locale codes. Returns an array of labels sorted by descending confidence. | **Label** | **Region** | | :--- | :--- | | `en-US` | American English | | `en-GB` | British English | | `en-AU` | Australian English | | `zh-CN` | Mandarin Chinese | | `fr-FR` | French (France) | | `es-ES` | Spanish (Spain) | | `es-419` | Spanish (Latin America) | | `es-MX` | Spanish (Mexico) | | `ar-EG` | Arabic (Egypt) | Additional accent locales may be returned beyond those listed above. The model supports a broad range of BCP-47 codes. ## Configuration The STT API accepts both camelCase and snake_case field names (e.g., `transcribeConfig` / `transcribe_config`, `voiceProfileConfig` / `voice_profile_config`). The examples below use camelCase. Set `voiceProfileConfig` inside `transcribeConfig`: ```json { "transcribeConfig": { "modelId": "", "language": "en", "audioEncoding": "MP3", "voiceProfileConfig": { "enableVoiceProfile": true, "topN": 5 } } } ``` Use `inworld/inworld-stt-1` as the `modelId` — it supports Voice Profiles on both the synchronous HTTP and WebSocket streaming endpoints. ### WebSocket (Streaming) Include `voiceProfileConfig` in the first WebSocket message: ```json { "transcribeConfig": { "modelId": "", "audioEncoding": "LINEAR16", "voiceProfileConfig": { "enableVoiceProfile": true, "topN": 5 } } } ``` ### Configuration parameters | **Field** | **Type** | **Default** | **Description** | | :--- | :--- | :--- | :--- | | `enable_voice_profile` / `enableVoiceProfile` | bool | — | **Required.** Set to `true` to enable Voice Profile analysis for the request or stream. | | `top_n` / `topN` | integer | `10` | Number of top labels from each classification category to return. | ## Response structure The `voiceProfile` object is returned alongside `transcription` and `usage` in both sync and streaming responses. Each category is an array of `{ label, confidence }` objects, sorted by descending confidence. The JSON below shows the **normalized** response shape, using camelCase throughout. Raw API payloads may use `snake_case` for the same fields (for example `vocal_style`, `transcribed_audio_ms`, `model_id`). In your own docs and client code, use either the raw or normalized shape consistently within each example. ### Example response (sync, normalized shape) ```json { "transcription": { "transcript": "Hey, I just wanted to check in on the delivery status.", "isFinal": true }, "voiceProfile": { "age": [ { "label": "young", "confidence": 0.78 } ], "emotion": [ { "label": "tender", "confidence": 0.97 }, { "label": "sad", "confidence": 0.03 } ], "pitch": [ { "label": "medium", "confidence": 0.85 } ], "vocalStyle": [ { "label": "whispering", "confidence": 0.97 }, { "label": "normal", "confidence": 0.03 } ], "accent": [ { "label": "en-US", "confidence": 0.48 } ] }, "usage": { "transcribedAudioMs": 3200, "modelId": "inworld/inworld-stt-1" } } ``` ### Response fields | **Field** | **Type** | **Description** | | :--- | :--- | :--- | | `voiceProfile.age` | ClassLabel[] | Array of detected age categories, sorted by descending confidence. | | `voiceProfile.emotion` | ClassLabel[] | Array of detected emotions, sorted by descending confidence. | | `voiceProfile.pitch` | ClassLabel[] | Array of detected pitch levels, sorted by descending confidence. Pitch drift across a conversation can signal changes in emotional state. | | `voiceProfile.vocalStyle` | ClassLabel[] | Array of detected vocal styles, sorted by descending confidence. | | `voiceProfile.accent` | ClassLabel[] | Array of detected accents as BCP-47 locale codes, sorted by descending confidence. | Each `ClassLabel` object contains: - **label** (string) — The predicted class name - **confidence** (float) — Score from 0.0 to 1.0 ## Best practices - **Start with the default `topN` (10)** — This returns up to 10 labels per category, sorted by descending confidence. Lower `topN` if you only need the most confident predictions; raise it if you need broader signal. - **Use emotion and vocal style together** — Combining both categories gives a richer picture. A "tender" emotion with "whispering" vocal style tells a different story than "tender" with "normal" style. - **Handle missing fields gracefully** — Voice Profile fields may be absent if the model cannot make a confident classification or if the audio quality is insufficient. Always check for presence before accessing. - **Accent is probabilistic** — Accent detection returns the most likely locale, not a definitive answer. Use it as a signal rather than a hard routing decision. - **Test with representative audio** — Classification accuracy depends on audio quality, background noise, and speech duration. Test with samples that reflect your production environment. --- ##### Speaker diarization Source: https://docs.inworld.ai/stt/speaker-diarization > Label each transcribed word with the speaker who said it. Speaker diarization labels each transcribed word with a speaker identifier, so you can tell who said what in calls, meetings, interviews, and other multi-speaker audio. Speaker diarization for `inworld/inworld-stt-1` is **Experimental** — speaker attribution quality is still improving and speakers may occasionally be misattributed. ## Enabling diarization Diarization is available on the [WebSocket streaming endpoint](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe-stream-websocket.md) and on [asynchronous transcription](https://docs.inworld.ai/stt/async-transcription.md). Set both fields in `transcribeConfig`: ```json { "transcribeConfig": { "modelId": "inworld/inworld-stt-1", "audioEncoding": "LINEAR16", "sampleRateHertz": 16000, "enableSpeakerDiarization": true, "includeWordTimestamps": true } } ``` `includeWordTimestamps` goes together with `enableSpeakerDiarization` — speaker labels attach to word timestamps, not to the turn-level transcript. ## Reading speaker labels Each word in `wordTimestamps` carries a speaker identifier: ```json { "result": { "transcription": { "transcript": "Hi, I'm calling about my account balance.", "isFinal": true, "wordTimestamps": [ { "word": "Hi,", "startTimeMs": 97, "endTimeMs": 194, "speaker": 0 }, { "word": "I'm", "startTimeMs": 698, "endTimeMs": 779, "speaker": 0 } ] } } } ``` An asynchronous job returns the same `wordTimestamps` array, inside each entry of the transcript document's `segments`. Speaker labels exist only on words — there is no segment-level `speaker` field. A `confidence` field on word timestamps is not currently populated: asynchronous results omit it entirely, and the synchronous and streaming endpoints return `0`. Do not build on it. Speaker identifiers are small integers starting at `0`. They apply only to one stream or job: the same identifier means the same speaker within that session, not across sessions. To build per-speaker segments, group consecutive words that share a `speaker` value. Words may occasionally arrive without a label; treat those as unattributed rather than assuming a speaker. ## Next steps Full message and configuration schema for the streaming endpoint. Detect when a speaker has finished talking. Diarize a long recording as a job instead of a live stream. --- #### Best practices ##### STT best practices Source: https://docs.inworld.ai/stt/best-practices > Get the best transcripts from the STT API: audio choices, turn-taking, speech events, Voice Profile, language hints, and custom vocabulary. - **Audio** — Use MP3/OGG_OPUS for file uploads to reduce size; use LINEAR16 for streaming (required) and when you need highest quality. - **Streaming** — With manual turn-taking, send `endTurn` at turn boundaries while continuing the session. When done, send `closeStream` once to finalize pending audio and receive usage; do not precede it with another `endTurn`. See the [WebSocket integration guide](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe-stream-websocket.md) for the complete sequence. - **Speech events** — Listen for `speechStarted` and `speechStopped` events in the streaming response to detect when a speaker begins and stops talking. Use these to build custom turn-taking logic or visualize voice activity. - **Voice Profile** — Set `voiceProfileConfig.enableVoiceProfile` to `true` and optionally adjust `topN` (default: 10) to control how many labels per category are returned. - **Language hint** — If you know the audio's language, set `language` (e.g. `en`, `ja`) for cleaner output; the hint also constrains the output script (see [Language Support](https://docs.inworld.ai/stt/languages.md)). Leave it empty to auto-detect or when speakers switch languages. - **Custom vocabulary** — Pass domain-specific terms (names, jargon, acronyms) in `prompts` to bias recognition toward them. The bias does not guarantee a match, so test terms the baseline mishears. Keep the list short and targeted. The cap is 100 terms per request, and each term gets a weaker bias as the list grows. A long list is likely to be ignored; a few misheard terms work better than a full glossary. - **Custom vocabulary characters** — Terms may contain letters and digits in any script, spaces, and `. , ' ( ) : ! ? ; -` (straight apostrophes and hyphens). A term with any other character — `AT&T`, `user_name`, curly quotes, en/em dashes — rejects the **whole request** with a 400 naming the character, so sanitize terms before sending. The model only ever outputs letters, digits, and that same basic punctuation. A spelling like `In@world` cannot appear in a transcript even if it were accepted. Well-known names such as `AT&T` are generally transcribed correctly without prompting. - Test with sample audio and your target language before production. --- ##### STT errors and troubleshooting Source: https://docs.inworld.ai/stt/troubleshooting > How the STT API reports errors on the synchronous and WebSocket endpoints, the common gRPC status codes, and what to check when transcription fails. ## Error format Errors follow the standard [gRPC status](https://grpc.io/docs/guides/status-codes/) format. **Authentication error** ```json { "code": 16, "message": "Unauthenticated: invalid or missing API key.", "details": [] } ``` **Invalid request** ```json { "code": 3, "message": "Unsupported audio encoding.", "details": [] } ``` **Common gRPC status codes** | **Code** | **Name** | **Description** | | :--- | :--- | :--- | | `3` | `INVALID_ARGUMENT` | Invalid or missing request field (encoding, model ID, audio data) | | `8` | `RESOURCE_EXHAUSTED` | Too many concurrent requests (rate limit) | | `16` | `UNAUTHENTICATED` | Invalid or missing API key | **Streaming (WebSocket) errors** On the [streaming endpoint](https://docs.inworld.ai/api-reference/sttAPI/speechtotext/transcribe-stream-websocket.md), errors arrive as a frame with a top-level `error` key using the same status format: ```json { "error": { "code": 3, "message": "invalid transcribe config: unsupported audio encoding" } } ``` A malformed first message currently closes the socket with WebSocket close code `1005` and no error frame, for example when the config is not wrapped in `transcribeConfig`. If the connection closes silently without transcripts, check your first message's shape. ## Troubleshooting | **Issue** | **What to check** | | :--- | :--- | | No transcript | API key, audio encoding matches request, valid audio file | | `UNAUTHENTICATED` | `INWORLD_API_KEY` set correctly and not expired in Portal | | `INVALID_ARGUMENT` | `audioEncoding` matches the actual format (LINEAR16 for raw PCM, MP3 for MP3, etc.) | | Poor quality | Use 16 kHz sample rate (8 kHz telephony audio has fewer data points and will produce lower-quality results); ensure clear speech | | Large file failures | Split or compress (e.g. MP3/OGG_OPUS); respect upload size limits | | No Voice Profile | Ensure `voiceProfileConfig.enableVoiceProfile` is set to `true` in your request | For more help, see the [Inworld Discord community](https://discord.gg/inworld). --- ### Realtime API #### Realtime API overview Source: https://docs.inworld.ai/realtime/overview > Build low-latency voice agents over WebSocket or WebRTC, with turn detection and interruptions handled for you. The Realtime API connects STT, the LLM you choose, and TTS in one spoken conversation. It uses one WebSocket or WebRTC connection and handles turn detection and interruptions. It follows the OpenAI Realtime protocol, so you can [migrate an OpenAI Realtime client](https://docs.inworld.ai/realtime/openai-migration.md) with few changes. Inworld [extensions](https://docs.inworld.ai/realtime/provider-data.md) add back-channel responses, fillers, and memory. A Node.js server and a browser client, streaming audio both ways. Voice using the browser's built-in support, with automatic echo cancellation. Talk to a voice agent in the Portal. Complete Node.js and browser projects. Complete Python projects. Every event, for WebSocket and WebRTC. **Using AI to code?** Give your assistant the docs index at `https://docs.inworld.ai/llms.txt`. For live search, [add the MCP server](https://docs.inworld.ai/developer-tools/vibe-coding). **Prefer the terminal?** Install the [Inworld CLI](https://docs.inworld.ai/developer-tools/inworld-cli.md) with `npm install -g @inworld/cli`. Use it to create and manage API keys for the Realtime API. AI agents can use it too. ## Your first session Connect and set up a session, then stream audio or text and handle the responses: ```text wss://api.inworld.ai/api/v1/realtime/session?key=&protocol=realtime ``` 1. Connect with `Authorization: Basic ` from a server, or a [session token](https://docs.inworld.ai/portal/session-tokens.md) from a browser. You receive `session.created`. 2. Send `session.update` with your instructions, model, voice, and tools. 3. Stream audio with `input_audio_buffer.append`, or text with `conversation.item.create`. 4. Handle `response.output_*` events until `response.done`. ## Explore Choose the STT model, the LLM or router, the voice, and turn detection. Let the agent call your functions mid-conversation. Conversation items, turn detection events, interruptions, and errors. Steering, back-channel responses, and fillers that keep the agent feeling present. Put the agent on a phone number. For voice output without speech input, use [Voice responses](https://docs.inworld.ai/router/guides/llm-plus-tts.md). Generate and speak an answer in one HTTP request. --- #### WebSocket quickstart Source: https://docs.inworld.ai/realtime/quickstart-websocket Build a browser-based voice agent that streams audio to the Inworld Realtime API using WebSocket. Use WebSocket for server-side and proxied connections where you can set custom headers. For lower-latency voice using the browser's built-in support, see the [WebRTC Quickstart](https://docs.inworld.ai/realtime/quickstart-webrtc.md). ## Get Started Create an [Inworld account](https://platform.inworld.ai/signup). In [Inworld Portal](https://platform.inworld.ai/), open **Manage** > **API Keys** and click **Generate new key**. Choose the key type and permissions, click **Generate**, then copy **Basic (Base64)**. API Keys page with Basic (Base64), UI-masked credentials and a hidden email API Keys page with Basic (Base64), UI-masked credentials and a hidden email Set your API key as an environment variable. ```shell macOS and Linux export INWORLD_API_KEY='your-base64-api-key-here' ``` ```shell Windows setx INWORLD_API_KEY "your-base64-api-key-here" ``` Create `server.js`. It proxies WebSocket events between the browser and Inworld, configures the voice session, and triggers an initial greeting. ```javascript server.js import { readFileSync } from 'fs'; import { createServer } from 'http'; import { WebSocketServer, WebSocket } from 'ws'; const html = readFileSync('index.html'); const server = createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); }); const wss = new WebSocketServer({ server, path: '/ws' }); const SESSION_CFG = JSON.stringify({ type: 'session.update', session: { instructions: 'You are a friendly voice assistant. Keep responses brief.', } }); const GREET = JSON.stringify({ type: 'conversation.item.create', item: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Greet the user' }] } }); wss.on('connection', (browser) => { let setup = 0; const api = new WebSocket( `wss://api.inworld.ai/api/v1/realtime/session?key=voice-${Date.now()}&protocol=realtime`, { headers: { Authorization: `Basic ${process.env.INWORLD_API_KEY}` } } ); api.on('message', (raw) => { if (setup < 2) { const t = JSON.parse(raw.toString()).type; if (t === 'session.created') { api.send(SESSION_CFG); setup = 1; } else if (t === 'session.updated' && setup === 1) { api.send(GREET); api.send('{"type":"response.create"}'); setup = 2; } } if (browser.readyState === WebSocket.OPEN) browser.send(raw.toString()); }); browser.on('message', (msg) => { if (api.readyState === WebSocket.OPEN) api.send(msg.toString()); }); browser.on('close', () => api.close()); api.on('close', () => { if (browser.readyState === WebSocket.OPEN) browser.close(); }); api.on('error', (e) => console.error('API error:', e.message)); }); let port = 3000; server.on('error', (e) => { if (e.code === 'EADDRINUSE') { console.warn(`Port ${port} in use, trying ${++port}…`); server.listen(port); } else throw e; }); server.listen(port, () => console.log(`Open http://localhost:${port}`)); ``` This greeting asks the LLM to write it, so the wording varies from session to session. For a fixed greeting, replace `GREET` and the `response.create` with a single [`response.speak`](https://docs.inworld.ai/realtime/usage/using-realtime-models.md#speak-exact-text-without-the-llm) event, such as `{"type":"response.speak","text":"Hi! How can I help?"}`. It skips the LLM, so it costs less and starts without waiting for the LLM's first token, but the text is spoken verbatim. Create `index.html` in the same directory. It captures microphone audio, plays agent audio, and displays transcripts that fade after each turn. ```html index.html Voice Agent ``` ```bash npm init -y && npm pkg set type=module npm install ws node server.js ``` Open [http://localhost:3000](http://localhost:3000) and click **Start Conversation**. The agent greets you with audio. ## How It Works | Component | Role | | --- | --- | | **Browser** | Captures mic audio (PCM16, 24 kHz), plays agent audio | | **Server** | Proxies events between browser and Inworld, holds the API key server-side | | **Inworld Realtime API** | Handles speech-to-text, LLM processing, and text-to-speech in one WebSocket session | Key events used: - `input_audio_buffer.append` — streams mic audio to Inworld - `response.output_audio.delta` — agent audio chunks for playback - `input_audio_buffer.speech_started` — triggers interruption (stops agent playback) ## Next Steps Full connection details, session config, and event handling. Configure the key elements of your voice agent. --- #### WebRTC quickstart Source: https://docs.inworld.ai/realtime/quickstart-webrtc Build a browser-based voice agent that streams audio to the Inworld Realtime API over WebRTC. The browser handles audio directly, so you do not need manual PCM encoding or base64 conversion. WebRTC is ideal for browser voice apps with low latency. For server-side integrations, see the [WebSocket Quickstart](https://docs.inworld.ai/realtime/quickstart-websocket.md). ## Get Started Create an [Inworld account](https://platform.inworld.ai/signup). In [Inworld Portal](https://platform.inworld.ai/), open **Manage** > **API Keys** and click **Generate new key**. Choose the key type and permissions, click **Generate**, then copy **Basic (Base64)**. API Keys page with Basic (Base64), UI-masked credentials and a hidden email API Keys page with Basic (Base64), UI-masked credentials and a hidden email Create a `.env` file: ```shell .env INWORLD_API_KEY=your-base64-api-key-here ``` Create `server.js`. It serves the page and a `/api/config` endpoint that fetches ICE servers and creates a session token. Save the [server-side mint helper](https://docs.inworld.ai/portal/session-tokens.md#server-side-mint-example) as `mint-session-token.mjs` in the same directory. The endpoint returns the token, never the API key. This is a local-development server, bound to loopback. Before deploying a token endpoint, add your application's authentication, authorization, CSRF protection, and per-user rate limits as described in the [backend token pattern](https://docs.inworld.ai/portal/ephemeral-tokens.md#connect-a-client-through-your-backend). ```javascript server.js import 'dotenv/config'; import { readFileSync } from 'fs'; import { createServer } from 'http'; import { mintSessionToken } from './mint-session-token.mjs'; const html = readFileSync('index.html'); const API_KEY = process.env.INWORLD_API_KEY || ''; const PROXY = 'https://api.inworld.ai'; let port = 3000; const server = createServer(async (req, res) => { const allowedHosts = [`localhost:${port}`, `127.0.0.1:${port}`]; if (!allowedHosts.includes(req.headers.host) || (req.headers.origin && req.headers.origin !== `http://${req.headers.host}`)) { res.writeHead(403).end(); return; } if (req.url === '/api/config') { res.setHeader('Cache-Control', 'no-store'); if (req.method !== 'GET') { res.writeHead(405, { Allow: 'GET' }).end(); return; } let ice = []; try { const r = await fetch(`${PROXY}/v1/realtime/ice-servers`, { headers: { Authorization: `Basic ${API_KEY}` }, }); if (r.ok) ice = (await r.json()).ice_servers || []; } catch {} try { const { token, expirationTime } = await mintSessionToken(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ token, expirationTime, ice_servers: ice, url: `${PROXY}/v1/realtime/calls` })); } catch { res.writeHead(502, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Could not create a Realtime session' })); } return; } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); }); server.on('error', (e) => { if (e.code === 'EADDRINUSE') { console.warn(`Port ${port} in use, trying ${++port}…`); server.listen(port, '127.0.0.1'); } else throw e; }); server.listen(port, '127.0.0.1', () => console.log(`Open http://localhost:${port}`)); ``` Create `index.html` in the same directory. It connects via WebRTC, streams mic audio automatically, and plays agent audio through an RTP track. ```html index.html WebRTC Voice Agent ``` ```bash npm init -y && npm pkg set type=module npm install dotenv node server.js ``` Open [http://localhost:3000](http://localhost:3000) and click **Start Conversation**. The agent greets you with audio. ## Option 2: Using OpenAI Agents SDK If you're building a more advanced voice agent with features like agent handoffs, tool calling, and guardrails, you can use the [OpenAI Agents SDK](https://github.com/openai/openai-agents-js) with Inworld's WebRTC proxy. We provide a ready-to-run playground based on OpenAI's realtime agents demo. ```bash git clone https://github.com/inworld-ai/experimental-oai-realtime-agents-playground.git cd experimental-oai-realtime-agents-playground npm install ``` If you are unable to access this repository, please contact **support@inworld.ai** for access. Open `.env` and set `OPENAI_API_KEY` to your **Inworld** API key (the same Base64 credentials from [Inworld Portal](https://platform.inworld.ai/)): ```shell .env OPENAI_API_KEY=your-inworld-base64-api-key-here ``` Despite the variable name `OPENAI_API_KEY`, this must be your **Inworld** API key — not an OpenAI key. The SDK uses this variable name by convention, but the playground routes all traffic through the Inworld WebRTC proxy. ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000). Select a scenario from the **Scenario** dropdown and start talking. The playground includes two agentic patterns: - **Chat-Supervisor** — A realtime chat agent handles basic conversation while a more capable text model (e.g. `gpt-4.1`) handles tool calls and complex responses. - **Sequential Handoff** — Specialized agents transfer the user between them to handle specific intents (e.g. authentication → returns → sales). For full details on customizing agents, see the playground's README. --- ## How It Works | Component | Role | | --- | --- | | **Browser** | Captures mic audio via WebRTC, plays agent audio from RTP track | | **Node.js server** | Serves the page and `/api/config` (ICE servers + session token) | | **WebRTC proxy** | Bridges WebRTC ↔ WebSocket, transcodes OPUS ↔ PCM16 | | **Inworld Realtime API** | Handles speech-to-text, LLM processing, and text-to-speech | Key differences from WebSocket: - Audio flows via **RTP tracks** (no base64 encoding) - Events flow via **DataChannel** (same JSON schema) - Browser handles **OPUS codec** natively ## Next Steps Full connection details, session config, and SDK integration. VAD configuration, audio formats, and conversation flow. Migrate from OpenAI Realtime API to Inworld. --- #### Connect ##### WebSocket Source: https://docs.inworld.ai/realtime/connect/websocket > Connect to the Realtime API over WebSocket, set up sessions, and stream audio and text events. Use WebSocket to connect to the Realtime API and exchange audio and text events. For low-latency voice using the browser's built-in support, see [WebRTC](https://docs.inworld.ai/realtime/connect/webrtc.md). ## Endpoint ``` wss://api.inworld.ai/api/v1/realtime/session?key=&protocol=realtime ``` | Parameter | Required | Description | | --- | --- | --- | | `key` | Yes | Session ID from your app | | `protocol` | Yes | `realtime` | ## Authentication | Environment | Header | Notes | | --- | --- | --- | | **Server-side (Node.js)** | `Authorization: Basic ` | Your API key from [Inworld Portal](https://platform.inworld.ai/) | | **Client-side (browser)** | `Authorization: Bearer ` | Mint a [session token](https://docs.inworld.ai/portal/session-tokens.md) on your backend. See the [JWT sample app](https://github.com/inworld-ai/inworld-nodejs-jwt-sample-app) for a complete example | ## Flow 1. Connect → receive `session.created` 2. Send `session.update` (instructions, audio config, tools) 3. Stream audio (`input_audio_buffer.append`) or text (`conversation.item.create`) 4. `response.create` → handle `response.output_*` until `response.done` ## Session Config Use `session.update` to change your system prompt, voice, model, or tools during a conversation. It accepts partial updates. ```javascript ws.send(JSON.stringify({ type: 'session.update', session: { type: 'realtime', model: 'openai/gpt-4o-mini', instructions: 'You are a concise concierge.', output_modalities: ['audio', 'text'], audio: { input: { turn_detection: { type: 'semantic_vad', eagerness: 'medium', create_response: true, interrupt_response: true } }, output: { voice: 'Clive', model: 'inworld-tts-2', speed: 1.0 } }, tools: [{ type: 'function', name: 'get_weather', description: 'Fetch weather for a location', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'] } }], providerData: { stt: { voice_profile: true, language_hints: ['en-US', 'es-MX'], end_of_turn_confidence_threshold: 0.7, min_end_of_turn_silence: 200, max_turn_silence: 5000, vad_threshold: 0.5 }, tts: { segmenter_strategy: 'sentence', steering_handling: 'emit_once', language: 'en-US', delivery_mode: 'CREATIVE', conversational: false }, memory: { enabled: true, turn_interval: 5, max_facts: 50 }, backchannel: { enabled: true }, responsiveness: { enabled: true } } } })); ``` `providerData` adds Inworld options to the OpenAI-compatible session format, including STT tuning, TTS segmentation and steering, and automatic memory. You can change most fields during a conversation with a partial `session.update`. A few fields are read only when the session opens and ignored afterwards, notably `providerData.tts.conversational` and `providerData.tts.user_turn_mode`. See [Inworld Realtime API Extensions](https://docs.inworld.ai/realtime/provider-data.md) for each field and whether it can change during a session. ## Audio The default audio format is base64-encoded mono PCM16 at 24 kHz. For telephony, the API also accepts `audio/pcmu` and `audio/pcma` (G.711 μ-law / A-law) at 8 kHz. Use `audio/float32` for pipelines that produce 32-bit float samples. Set the input and output formats with `audio.input.format` and `audio.output.format` in `session.update`. See [Audio input formats](https://docs.inworld.ai/realtime/usage/using-realtime-models.md#audio-input-formats) for the full list. We recommend 60-100ms chunks. ```javascript ws.send(JSON.stringify({ type: 'input_audio_buffer.append', audio: base64PcmChunk })); ``` Use `input_audio_buffer.clear` to discard unwanted audio. ## Text To send text from your client, use `conversation.item.create`. ```javascript ws.send(JSON.stringify({ type: 'conversation.item.create', item: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Can you summarize the notes I sent?' }] } })); ``` ## Events WebSocket events carry speech-to-speech conversations. Your client sends events to the API and handles events from the server. - **Session:** `session.created`, `session.updated` - **Conversation:** `conversation.item.added/done/retrieved/deleted/truncated`, transcription deltas/completions - **Responses:** `response.speak.acknowledged` (after a `response.speak`), `response.created`, `response.output_item.added/done`, `response.output_text.delta/done`, `response.output_audio.delta/done`, `response.done` - **Audio/VAD:** `input_audio_buffer.speech_started`, `input_audio_buffer.speech_stopped`, `input_audio_buffer.turn_suggestion`, `input_audio_buffer.turn_suggestion_revoked`, `input_audio_buffer.timeout_triggered`, `response.output_audio_transcript.delta` - **Back-channel:** `response.backchannel.audio.delta`, `response.backchannel.audio.done`, `response.backchannel.skipped` - **Errors:** `error` See the [API reference](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-websocket.md) for all events and their schemas. ## Node.js websocket server example This server-side Node.js example uses the `ws` library with Basic auth. ```javascript import WebSocket from 'ws'; const sessionId = 'your-session-id'; const credentials = process.env.INWORLD_API_KEY; const ws = new WebSocket(`wss://api.inworld.ai/api/v1/realtime/session?key=${sessionId}&protocol=realtime`, { headers: { Authorization: `Basic ${credentials}` } }); ws.on('open', () => { console.log('WebSocket connected'); }); ws.on('message', (buffer) => { const message = JSON.parse(buffer.toString()); switch (message.type) { case 'session.created': console.log('Session created:', message.session.id); updateSession(); break; case 'session.updated': console.log('Session updated'); sendMessage('Hello!'); break; case 'conversation.item.added': console.log('Conversation item added:', message.item.id); break; case 'conversation.item.done': console.log('Conversation item done'); createResponse(); break; case 'input_audio_buffer.speech_started': console.log('Speech started at', message.audio_start_ms, 'ms'); break; case 'input_audio_buffer.speech_stopped': console.log('Speech stopped at', message.audio_end_ms, 'ms'); break; case 'conversation.item.input_audio_transcription.delta': console.log('Transcription delta:', message.delta); break; case 'conversation.item.input_audio_transcription.completed': console.log('Transcription complete:', message.transcript); break; case 'response.created': console.log('Response created:', message.response.id); break; case 'response.output_item.added': console.log('Output item added:', message.item.id); break; case 'response.output_text.delta': console.log('Text delta:', message.delta); break; case 'response.output_audio.delta': // Decode and play audio chunk const audioBuffer = Buffer.from(message.delta, 'base64'); playAudio(audioBuffer); break; case 'response.output_audio_transcript.delta': console.log('Audio transcript delta:', message.delta); break; case 'response.done': console.log('Response complete, status:', message.response.status); break; case 'error': console.error('Error:', message.error.message, message.error.code); break; } }); function updateSession() { ws.send(JSON.stringify({ type: 'session.update', session: { type: 'realtime', output_modalities: ['text', 'audio'], instructions: 'You are a helpful AI assistant.', audio: { input: { turn_detection: { type: 'semantic_vad', eagerness: 'medium', create_response: true, interrupt_response: true } }, output: { voice: 'Clive' } } } })); } function sendMessage(text) { ws.send(JSON.stringify({ type: 'conversation.item.create', item: { type: 'message', role: 'user', content: [{ type: 'input_text', text }] } })); } function createResponse() { ws.send(JSON.stringify({ type: 'response.create', response: { output_modalities: ['text', 'audio'] } })); } function cancelResponse() { ws.send(JSON.stringify({ type: 'response.cancel' })); } function sendAudioChunk(audioChunk) { ws.send(JSON.stringify({ type: 'input_audio_buffer.append', audio: audioChunk // base64-encoded audio data })); } function clearAudioBuffer() { ws.send(JSON.stringify({ type: 'input_audio_buffer.clear' })); } ``` See the [API reference](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-websocket.md) for full schemas. --- ##### WebRTC Source: https://docs.inworld.ai/realtime/connect/webrtc > Connect to the Realtime API over WebRTC for low-latency audio, with JSON events over a data channel. Use WebRTC for low-latency voice through the browser's built-in support. A proxy connects your peer connection to the same realtime service used by [WebSocket](https://docs.inworld.ai/realtime/connect/websocket.md). It converts audio between OPUS and PCM16 and forwards events transparently. ## Endpoint ``` https://api.inworld.ai ``` | Endpoint | Method | Description | | --- | --- | --- | | `/v1/realtime/calls` | POST | SDP offer/answer exchange | | `/v1/realtime/ice-servers` | GET | STUN/TURN server configuration | ## Authentication Browser clients use a [session token](https://docs.inworld.ai/portal/session-tokens.md) created on your backend. Send it as a Bearer credential. The proxy forwards it to the realtime service. ``` Authorization: Bearer ``` Server-side clients can use their complete Base64 API credential: ```text Authorization: Basic ``` Keep the API key server-side (see [Security best practices](https://docs.inworld.ai/portal/auth-security.md)). Your backend returns a session token to the browser, never the API key (see examples below). ## Flow 1. Fetch config from your server (session token + ICE servers) 2. Create `RTCPeerConnection` with ICE servers 3. Create data channel `oai-events` + add microphone track 4. Create SDP offer → POST it with optional initial session config to `/v1/realtime/calls` → set SDP answer 5. Data channel opens → send any later `session.update` patches → start conversation Audio flows via RTP tracks (no manual encode/decode). Events flow via data channel using the same JSON schema as [WebSocket](https://docs.inworld.ai/realtime/connect/websocket.md). ## Session config You can configure the session during signaling or through the data channel: - **During signaling:** include the session object in a JSON or multipart `/v1/realtime/calls` request. The proxy sends it upstream as an initial `session.update` before returning the SDP answer. It requires a successful send but does not wait for `session.updated`. - **After connection:** send `session.update` through the data channel. Partial updates patch the initial configuration. For example, a JSON signaling request can opt into OpenAI-style tool continuation. `SESSION_TOKEN` is the token returned by your backend: ```javascript const response = await fetch(`${PROXY}/v1/realtime/calls`, { method: 'POST', headers: { Authorization: `Bearer ${SESSION_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ sdp: offer.sdp, session: { type: 'realtime', providerData: { auto_tool_response: false } } }) }); const { sdp } = await response.json(); await pc.setRemoteDescription({ type: 'answer', sdp }); ``` You can also send the same session fields later as a WebSocket-compatible `session.update` through the data channel. See [Configuring Models](https://docs.inworld.ai/realtime/usage/using-realtime-models.md) for the full breakdown of STT, LLM, and TTS configuration. ```javascript dc.send(JSON.stringify({ type: 'session.update', session: { type: 'realtime', model: 'openai/gpt-4o-mini', instructions: 'You are a concise concierge.', output_modalities: ['audio', 'text'], audio: { input: { turn_detection: { type: 'semantic_vad', eagerness: 'medium', create_response: true, interrupt_response: true } }, output: { voice: 'Clive', model: 'inworld-tts-2', speed: 1.0 } }, tools: [{ type: 'function', name: 'get_weather', description: 'Fetch weather for a location', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'] } }], providerData: { stt: { voice_profile: true, language_hints: ['en-US', 'es-MX'], end_of_turn_confidence_threshold: 0.7, min_end_of_turn_silence: 200, max_turn_silence: 5000, vad_threshold: 0.5 }, tts: { segmenter_strategy: 'sentence', steering_handling: 'emit_once', language: 'en-US', delivery_mode: 'CREATIVE', conversational: false }, memory: { enabled: true, turn_interval: 5, max_facts: 50 }, backchannel: { enabled: true }, responsiveness: { enabled: true } } } })); ``` `providerData` carries Inworld-specific extensions to the OpenAI-compatible session shape — STT tuning, TTS segmentation/steering, automatic memory, and more. Most `providerData` fields are hot-swappable via partial `session.update`, but a few are read only at session open and ignored afterwards (notably `providerData.tts.conversational` and `providerData.tts.user_turn_mode`). See [Inworld Realtime API Extensions](https://docs.inworld.ai/realtime/provider-data.md) for the field-by-field reference. ## Audio Unlike WebSocket (manual base64 PCM), WebRTC handles audio natively: - **Input**: browser captures mic and sends OPUS over RTP automatically - **Output**: proxy sends AI audio back as an RTP track — attach to `