> ## Documentation Index
> Fetch the complete documentation index at: https://docs.inworld.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Script synthesis

> Generate dialogue or a monologue in one request, with combined or separate utterance audio

A script is an ordered list of utterances, each with text and a stored voice. One request can produce a conversation between speakers or a monologue with one voice. TTS-2 keeps a bounded audio history within the script to help later utterances follow earlier turns naturally. Separate requests do not share this history.

Scripts work with the [unary](https://docs.inworld.ai/tts/synthesize-speech.md), [HTTP streaming](https://docs.inworld.ai/tts/synthesize-speech-streaming.md), [async](https://docs.inworld.ai/tts/synthesize-speech-async.md), and [batch](https://docs.inworld.ai/tts/synthesize-speech-batch.md) APIs. This request shape does not apply to WebSocket or OpenAI-compatible synthesis. Async and batch remain preview APIs.

## Build a script

Send `script` instead of the top-level `text`, `voiceId`, or `voiceDesign`. Use `modelId: "inworld-tts-2"`. Every utterance needs its own `text` and `voiceConfig.voiceId`; inline voice design is not supported inside a script.

```bash
curl --request POST 'https://api.inworld.ai/tts/v1/voice' \
  --header "Authorization: Basic $INWORLD_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "modelId": "inworld-tts-2",
    "language": "en-US",
    "audioConfig": {"audioEncoding": "MP3", "speakingRate": 1.0},
    "timestampType": "WORD",
    "outputMode": "PER_UTTERANCE",
    "script": {
      "voices": [
        {"voiceId": "Dennis", "speakingRate": 0.9, "deliveryMode": "STABLE"}
      ],
      "utterances": [
        {"text": "Welcome to the observatory.", "voiceConfig": {"voiceId": "Dennis"}},
        {"text": "The telescope is ready for tonight.", "voiceConfig": {"voiceId": "Ashley"}},
        {"text": "Let us look at the stars.", "voiceConfig": {"voiceId": "Dennis", "speakingRate": 1.1}}
      ]
    }
  }'
```

`script.voices` is an optional list of defaults keyed by unique `voiceId`. It does not declare speakers or replace `voiceConfig.voiceId` on each utterance. For `speakingRate` and `deliveryMode`, an utterance override takes precedence over its voice defaults, which take precedence over the request settings. In this example, Dennis's first turn uses rate `0.9`, Ashley inherits `1.0`, and Dennis's last turn overrides the rate to `1.1` while retaining `STABLE` delivery.

Scripts accept **1–100 utterances**. The endpoint's text limit applies to the sum of all utterance texts, measured in UTF-16 code units: 2,000 for unary and 4,000 for HTTP streaming; async and batch use their existing [job limits](https://docs.inworld.ai/tts/synthesize-speech-async.md#limits). Splitting text into more utterances does not increase the limit. Usage counts the text across the script once.

Audio encoding, sample rate, language, model, and timestamp type are shared request settings. Per-utterance controls are `voiceId`, `speakingRate` (0.5–1.5), and `deliveryMode`.

## Request fields



<ParamField body="script" type="object">
  Ordered speech utterances for TTS-2. Replaces the request's top-level `text`, `voiceId`, and `voiceDesign`. Bounded audio history carries between utterances within this request. See [script synthesis](https://docs.inworld.ai/tts/capabilities/script-synthesis.md) for examples and output formats.

  <Expandable title="script fields">
    <ParamField body="utterances" type="object[]" required>
      Between 1 and 100 utterances, generated in array order. The endpoint's text limit applies to the sum of their texts, measured in UTF-16 code units.

      <Expandable title="utterance fields">
        <ParamField body="text" type="string" required>
          Text to synthesize for this utterance.
        </ParamField>
        <ParamField body="voiceConfig" type="object" required>
          Stored voice and optional delivery overrides for this utterance.

          <Expandable title="voiceConfig fields">
            <ParamField body="voiceId" type="string" required>
  Stored voice to use for this utterance, or the key for an entry in `script.voices`. Required even when voice defaults are supplied. Inline voice design is not supported inside scripts.
</ParamField>

<ParamField body="speakingRate" type="number">
  Speaking rate from `0.5` to `1.5`. An utterance's value overrides its matching `script.voices` entry, which overrides `audioConfig.speakingRate` on the request.
</ParamField>

<ParamField body="deliveryMode" type="enum">
  Delivery mode: `STABLE`, `BALANCED`, or `CREATIVE`. An utterance's value overrides its matching `script.voices` entry, which overrides the request's `deliveryMode`. Omit this field to inherit. An explicit `DELIVERY_MODE_UNSPECIFIED` selects `BALANCED` behavior.
</ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>
    <ParamField body="voices" type="object[]">
      Optional defaults keyed by unique `voiceId`. Each utterance still requires its own `voiceConfig.voiceId`.

      <Expandable title="voice default fields">
        <ParamField body="voiceId" type="string" required>
  Stored voice to use for this utterance, or the key for an entry in `script.voices`. Required even when voice defaults are supplied. Inline voice design is not supported inside scripts.
</ParamField>

<ParamField body="speakingRate" type="number">
  Speaking rate from `0.5` to `1.5`. An utterance's value overrides its matching `script.voices` entry, which overrides `audioConfig.speakingRate` on the request.
</ParamField>

<ParamField body="deliveryMode" type="enum">
  Delivery mode: `STABLE`, `BALANCED`, or `CREATIVE`. An utterance's value overrides its matching `script.voices` entry, which overrides the request's `deliveryMode`. Omit this field to inherit. An explicit `DELIVERY_MODE_UNSPECIFIED` selects `BALANCED` behavior.
</ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="outputMode" type="enum">
  `COMBINED` (default) returns one recording. `PER_UTTERANCE` returns separate utterance recordings and requires `script`. Both modes retain the same generation history. Async and batch per-utterance results require ZIP packaging.
</ParamField>

## Choose the output

`outputMode` controls whether the service returns one combined recording or separate recordings. Omitting it selects `COMBINED`.

| Endpoint | `COMBINED` | `PER_UTTERANCE` |
| --- | --- | --- |
| Unary | Top-level `audioContent` and optional `timestampInfo` | Complete clips in `scriptResult.utterances[]` |
| HTTP streaming | Top-level audio and timestamps on one timeline | Top-level audio and timestamps tagged with `scriptResult.utteranceIndex` |
| Async | Existing single-recording result | `scriptResult.utterances[]` in the ZIP results document |
| Batch | Existing single-recording result per item | `scriptResult.utterances[]` on each script item in the ZIP results document |

`scriptResult` is omitted for combined output. With separate recordings, timestamps are relative to the start of **each utterance's audio**; combined output uses one cumulative timeline. Separate output requires a script and does not also produce a combined recording.

## Unary response

For `PER_UTTERANCE`, decode each entry's `audioContent` as its own complete recording. Indexes are zero-based positions in the request's `script.utterances` array. The following excerpt omits usage and timestamp details, and uses illustrative base64 placeholders:

```json
{
  "scriptResult": {
    "utterances": [
      {"utteranceIndex": 0, "audioContent": "<base64 recording 0>"},
      {"utteranceIndex": 1, "audioContent": "<base64 recording 1>"},
      {"utteranceIndex": 2, "audioContent": "<base64 recording 2>"}
    ]
  }
}
```

When requested, alignment belongs to each entry's `timestampInfo`. Top-level `audioContent` is empty; a top-level timestamp object may contain empty alignment arrays and is not the script's alignment. Read the top-level `usage` for the whole request.

## HTTP streaming response

Send the same body to `/tts/v1/voice:stream`. The endpoint returns newline-delimited JSON, with each response inside `result`. In `PER_UTTERANCE` mode, audio stays in `result.audioContent`; the wrapper supplies the index:

```json
{"result":{"audioContent":"<first chunk of utterance 0>","scriptResult":{"utteranceIndex":0}}}
{"result":{"audioContent":"<next chunk of utterance 0>","scriptResult":{"utteranceIndex":0}}}
{"result":{"audioContent":"<first chunk of utterance 1>","scriptResult":{"utteranceIndex":1}}}
```

The excerpt omits timestamps and usage. Group audio and timestamp updates by index. Base64-decode each audio chunk separately before concatenating its bytes. MP3 chunks for one index form one recording; follow the [audio format rules](https://docs.inworld.ai/tts/capabilities/generating-audio.md#supported-formats) for other encodings.

Indexes arrive in order. An increment means all audio and timestamp updates for the previous utterance have finished. Successful end-of-stream completes the last utterance; an error or disconnected stream does not. There is no separate utterance-completed event. An utterance with no audio still emits its index, so do not require an audio chunk on every response. Timestamp-only updates also belong to their indicated index.

## Async results

Async and batch use the same packaging rules:

| Results | Packaging |
| --- | --- |
| Hosted, `COMBINED` | `FILES` by default; `ZIP` optional |
| Hosted, any `PER_UTTERANCE` output | Explicit `ZIP` required |
| Delivered to `outputConfig.resultsUri` | `ZIP` required and selected by default, regardless of output mode |

For `/tts/v1/voice:synthesizeAsync` with `PER_UTTERANCE`, add `"outputConfig": {"packaging": "ZIP"}` to the script request. See [results delivery](https://docs.inworld.ai/tts/job-results-delivery.md#packaging) for destination requirements.

Poll the returned operation as described in [async synthesis](https://docs.inworld.ai/tts/synthesize-speech-async.md). On success, its `response.resultsUri` points to the ZIP and `response.resultsPath` names the results document inside it. The operation does not inline the utterance list.

For the three-turn example, the results document contains the following structure. Other result fields are omitted:

```json
{
  "scriptResult": {
    "utterances": [
      {"utteranceIndex": 0, "audioPath": "utterances/0000/audio.mp3", "timestampsPath": "utterances/0000/timestamps.json"},
      {"utteranceIndex": 1, "audioPath": "utterances/0001/audio.mp3", "timestampsPath": "utterances/0001/timestamps.json"},
      {"utteranceIndex": 2, "audioPath": "utterances/0002/audio.mp3", "timestampsPath": "utterances/0002/timestamps.json"}
    ]
  }
}
```

Paths are relative to the results document inside the archive. Follow the manifest's paths instead of constructing filenames. Each audio file is independently playable. Timestamp files contain a JSON array of alignment updates and are present only when timestamps were requested.

## Batch results

Place each script request inside an item's `request`, and set `outputConfig` **once at the batch root**:

```json
{
  "outputConfig": {"packaging": "ZIP"},
  "items": [
    {
      "customId": "observatory-dialogue",
      "request": {
        "modelId": "inworld-tts-2",
        "audioConfig": {"audioEncoding": "MP3"},
        "outputMode": "PER_UTTERANCE",
        "script": {
          "utterances": [
            {"text": "Welcome to the observatory.", "voiceConfig": {"voiceId": "Dennis"}},
            {"text": "The telescope is ready for tonight.", "voiceConfig": {"voiceId": "Ashley"}}
          ]
        }
      }
    },
    {
      "customId": "closing-narration",
      "request": {"modelId": "inworld-tts-2", "voiceId": "Dennis", "text": "Thank you for visiting."}
    }
  ]
}
```

A batch can mix scripts and ordinary single-voice requests. Hosted combined batches can use `FILES`; any `PER_UTTERANCE` item or delivery to your storage requires ZIP. Each script is generated independently; audio history is not shared between batch items.

After the [batch operation](https://docs.inworld.ai/tts/synthesize-speech-batch.md) completes, download `response.resultsUri` and open the file named by `response.resultsPath` inside the ZIP. Each successful script item has its own wrapper, while combined items retain their existing shape. This results excerpt corresponds to the request above:

```json
{
  "completedItems": 2,
  "failedItems": 0,
  "results": [
    {
      "customId": "observatory-dialogue",
      "scriptResult": {
        "utterances": [
          {"utteranceIndex": 0, "audioPath": "items/0/utterances/0000/audio.mp3"},
          {"utteranceIndex": 1, "audioPath": "items/0/utterances/0001/audio.mp3"}
        ]
      }
    },
    {"customId": "closing-narration", "audioPath": "items/1.mp3"}
  ]
}
```

With timestamps enabled on a script item, each utterance also has a `timestampsPath`. Inspect item errors and the success/failure counts: a completed batch operation does not mean every item succeeded. A failed script item does not return a successful prefix of its utterances.
