---
url: 'https://adk.nht.io/assembly/batteries-tts.md'
description: >-
  The full option and exception surface for the TTS (text-to-speech) batteries —
  transformers.js and OS-native — side by side.
---

# TTS batteries

## LLM summary — TTS batteries

* Aggregate barrel `@nhtio/adk/batteries/tts` re-exports the `BaseTtsAdapterOptions` base, `SynthesizeOptions`, `RawAudioLike`, and `TtsSynthesisResult` (an alias for `GeneratedMediaOutput`), plus the `TransformersJsTtsAdapter` and its options/exceptions. It **omits** the native engine entirely, exactly like isolation's `child_process` (which is node-only).
* Deep subpaths: `@nhtio/adk/batteries/tts/transformers_js` (environment-neutral), `@nhtio/adk/batteries/tts/native` (node-only, no top-level re-export). The shared base is exported from `@nhtio/adk/batteries/tts/_shared`.
* The shared base `BaseTtsAdapterOptions { voice?, rate? }` is deliberately **model-LESS**: no `model` field. The native engine has no model, so each model-backed engine adds its own required `model`. This is the inverse of the specialists domain (specialists turn a modality INTO text; TTS turns text INTO audio).
* Shared contract: `synthesize(text, opts?) → Promise<GeneratedMediaOutput>` — the shared descriptor is generic (`{ kind: MediaKind; mimeType: string; bytes: Uint8Array; filename?: string }`); both TTS adapters specifically resolve it to `kind: 'audio'`, `mimeType: 'audio/wav'`, `filename: 'speech.wav'`. Plus static+instance `isAvailable()`, `preload()`, and `reset()` (and `dispose()` for tjs). Returns a SINGLE result, no `n` array.
* Both engines map `voice`/`rate`. `voice` → `say -v` / `espeak-ng -v` / PowerShell `SelectVoice` / tjs `speaker_embeddings` (when string). `rate` is a MULTIPLIER (1 = normal) → `round(175*rate)` wpm clamped 80–500 (say/espeak), `-10..10` (PowerShell), `speed` (tjs). The tjs adapter validates `rate` as a **positive** number (0/negative throw); the native adapter accepts any number.
* `TransformersJsTtsAdapter` (tjs): requires `model`. Runs `pipeline('text-to-speech')`. Works with any transformers.js `text-to-speech` model — MMS-VITS and SpeechT5 (SpeechT5 needs `speakerEmbeddings`) are the verified examples. Options: `model`, `pipeline?`, `createPipeline?`, `device?`, `dtype?`, `speakerEmbeddings?: Float32Array|string|URL`, `numInferenceSteps?`, `modelSource?`, `onInitProgress?`, `isAvailable?`, plus `BatteryLifecycleHooks`. Per-call adds `speakerEmbeddings`/`numInferenceSteps`. Verified against **@huggingface/transformers 4.2.0**.
* `NativeTtsAdapter` (native): zero-config valid. Shells out to macOS `say` / Linux `espeak-ng` / win32 PowerShell `System.Speech`, reads back WAV. Options: `platform?`, `command?`, `extraArgs?` (ignored on win32), `wordsPerMinute?`, `pitch?` (espeak only), `timeoutMs?` (default 60000), `executor?`, `fs?`, `tmpdir?`, `randomName?`, `isAvailable?`. SECURITY: PowerShell interpolation is neutralized by `psSingleQuotedLiteral`.
* 6 exceptions across 2 engines: `E_INVALID_TRANSFORMERS_JS_TTS_OPTIONS` (529), `E_TRANSFORMERS_JS_TTS_ENGINE_ERROR` (502). `E_INVALID_NATIVE_TTS_OPTIONS` (529), `E_NATIVE_TTS_UNSUPPORTED_PLATFORM` (529), `E_NATIVE_TTS_ENGINE_ERROR` (502), `E_NATIVE_TTS_TIMEOUT` (504).

ADK includes two Text-to-Speech (TTS) engines for synthesizing audio from text: an OS-native CLI engine and a model-backed transformers.js engine. The shared contract takes text and returns a `GeneratedMediaOutput` descriptor — the same generic shape the generation domain uses (`{ kind: MediaKind; mimeType: string; bytes: Uint8Array; filename?: string }`); both TTS adapters resolve it specifically to `kind: 'audio'`, `mimeType: 'audio/wav'`, `filename: 'speech.wav'`. Unlike the generation and embeddings domains, the shared option base is explicitly **model-less**: the native engine has no model concept, so any model-backed engine extends the base to add its own required `model`.

## Barrels, side by side

| | Transformers.js | Native |
| --- | --- | --- |
| Subpath | `@nhtio/adk/batteries/tts/transformers_js` (or `@nhtio/adk/batteries/tts`) | `@nhtio/adk/batteries/tts/native` |
| Adapter | `TransformersJsTtsAdapter` | `NativeTtsAdapter` |
| Options type | `TransformersJsTtsAdapterOptions` | `NativeTtsAdapterOptions` |
| Validator | `validateTransformersJsTtsOptions` | `validateNativeTtsOptions` |
| Transport | on-device, `@huggingface/transformers` peer | OS shell (`say`, `espeak-ng`, PowerShell) |
| Environment | Environment-neutral (Browser / Node) | **Node-only** |

> **Note:** The aggregate barrel `@nhtio/adk/batteries/tts` re-exports the transformers.js engine but **omits** the native engine entirely. Because the native engine relies on `node:child_process` and `node:fs`, exporting it from the top-level would break browser bundlers. Import it from its deep subpath.

## Shared options (`BaseTtsAdapterOptions`)

Both engines accept and normalize these shared options, either in the constructor or as per-call overrides:

* `voice?: string`: The voice identifier. Mapped to `say -v <voice>`, `espeak-ng -v <voice>`, PowerShell `SelectVoice`, or a transformers.js `speaker_embeddings` URL/string.
* `rate?: number`: A multiplier where `1` is engine-normal. Mapped per-engine: `say`/`espeak-ng` compute `round(175 * rate)` words-per-minute (clamped 80–500), PowerShell maps it to a `-10..10` band, and transformers.js applies it as `speed`. The transformers.js adapter validates `rate` as a **positive** number (zero or negative throws `E_INVALID_TRANSFORMERS_JS_TTS_OPTIONS`); the native adapter accepts any number.

## Transformers.js (`TransformersJsTtsAdapter`)

The transformers.js engine requires a `model` and works with any transformers.js `text-to-speech` model — MMS-VITS (`'Xenova/mms-tts-eng'`) and SpeechT5 (`'Xenova/speecht5_tts'`, which additionally needs `speakerEmbeddings`) are the verified examples. It shares the same experimental-ish weight as the generation and specialist tjs engines: it downloads hundreds of megabytes of weights and can take significant CPU time to synthesize audio. Run it behind [`forkIsolated`](/batteries/isolation/node) for anything beyond a throwaway script. Pin **`@huggingface/transformers` 4.2.0 as the verified minimum**.

### Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `model` | `string` | **Required** | The TTS model id (e.g. `Xenova/mms-tts-eng`). |
| `speakerEmbeddings` | `Float32Array\|string\|URL` | `undefined` | Required by SpeechT5-family models. Ignored by MMS-VITS. |
| `numInferenceSteps` | `number` | `undefined` | Denoising step count for models that support it. |
| `pipeline` | `TransformersJsTtsPipeline` | `undefined` | Pre-built pipeline to bypass lazy creation. |
| `createPipeline` | `Function` | `pipeline('text-to-speech')` | Override the pipeline factory. |
| `device` | `string` | Environment default | Inference device (`'auto'`, `'webgpu'`, `'wasm'`, `'cpu'`, etc). |
| `dtype` | `string` | Environment default | Quantization/precision dtype. |
| `modelSource` | `Function` | HF default | Custom resolver for serving weights (OPFS, bundled, etc). |
| `onInitProgress` | `Function` | `undefined` | Callback for model download/compile progress. |

*(Also accepts the shared `voice` and `rate`, plus the `BatteryLifecycleHooks` common to all on-device ADK batteries).*

Per-call overrides (`TransformersJsSynthesizeOptions`) accept `voice`, `rate`, `speakerEmbeddings`, and `numInferenceSteps`.

## Native (`NativeTtsAdapter`)

The native engine is zero-config valid (`new NativeTtsAdapter()`). It auto-detects the platform and shells out to macOS `say`, Linux `espeak-ng`, or Windows PowerShell `System.Speech`. It uses lazy `node:*` imports so tests remain hermetic. PowerShell input is secured via `psSingleQuotedLiteral` to prevent script injection.

### Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `platform` | `'darwin'\|'linux'\|'win32'` | `process.platform` | Override auto-detected platform. |
| `command` | `string` | Platform default | Executable-only override (e.g. `/usr/bin/say`). |
| `wordsPerMinute` | `number` | Computed from `rate` | Direct WPM override for darwin/linux. |
| `pitch` | `number` | `undefined` | Pitch override (espeak-ng only). |
| `extraArgs` | `string[]` | `[]` | Extra CLI flags for darwin/linux. Ignored on win32. |
| `timeoutMs` | `number` | `60000` | Process timeout in milliseconds. |
| `executor` | `TtsBinaryExecutor` | `child_process` wrapper | Injectable CLI executor seam. |
| `fs` / `tmpdir` / `randomName` | `TtsFsLike` / `Function` / `Function` | `node:fs/promises` / `node:os` / `crypto` | Injectable filesystem seams. |
| `isAvailable` | `() => boolean` | `NativeTtsAdapter.isAvailable` | Injectable availability probe (overrides the default platform check). |

*(Also accepts the shared `voice` and `rate`).*

Per-call overrides (`NativeSynthesizeOptions`) accept `voice`, `rate`, and `pitch`.

## Exceptions

| Code | Engine | Status | Fatal? | Description |
| --- | --- | --- | --- | --- |
| `E_INVALID_TRANSFORMERS_JS_TTS_OPTIONS` | Transformers.js | 529 | Yes | Adapter options fail schema validation. |
| `E_TRANSFORMERS_JS_TTS_ENGINE_ERROR` | Transformers.js | 502 | No | Model load fails or pipeline throws. |
| `E_INVALID_NATIVE_TTS_OPTIONS` | Native | 529 | Yes | Adapter options fail schema validation. |
| `E_NATIVE_TTS_UNSUPPORTED_PLATFORM` | Native | 529 | Yes | Platform is not darwin, linux, or win32. |
| `E_NATIVE_TTS_ENGINE_ERROR` | Native | 502 | No | Binary fails, empty output, or invalid RIFF/WAVE magic. |
| `E_NATIVE_TTS_TIMEOUT` | Native | 504 | No | Binary aborted for exceeding `timeoutMs`. |

## Wiring into an agent

Since TTS generates media from text, it is typically exposed to the LLM via a BYO Tool. The pattern
is identical to the [image-generation recipe](/batteries/generation/recipes) — `synthesize()` returns
a `GeneratedMediaOutput`, which the handler persists with `ctx.storeMediaBytes` and wraps in
`Media.toolGenerated`:

```typescript
import { Tool, Media } from '@nhtio/adk'
import { validator } from '@nhtio/validation'
import { NativeTtsAdapter } from '@nhtio/adk/batteries/tts/native'
import type { DispatchContext } from '@nhtio/adk/types'

const tts = new NativeTtsAdapter()

const speak = (ctx: DispatchContext) =>
  new Tool({
    name: 'synthesize_speech',
    description: 'Speaks text aloud, returning a WAV audio clip.',
    inputSchema: validator.object({
      text: validator.string().description('What to say').required(),
    }),
    async handler({ text }) {
      let output
      try {
        output = await tts.synthesize(text) // a single GeneratedMediaOutput, not an array
      } catch (err) {
        return `Error (SYNTHESIS_FAILED): ${err instanceof Error ? err.message : String(err)}`
      }
      const reader = await ctx.storeMediaBytes(output.filename ?? 'speech.wav', output.bytes)
      return Media.toolGenerated({
        kind: output.kind,
        mimeType: output.mimeType,
        filename: output.filename ?? 'speech.wav',
        reader,
      })
    },
  })
```

*(For browser builds, swap `NativeTtsAdapter` for the environment-neutral `TransformersJsTtsAdapter`
from `@nhtio/adk/batteries/tts/transformers_js` — the handler body is unchanged since both return
`GeneratedMediaOutput`.)*

## Where to go next

* [Generation batteries](/assembly/batteries-generation) — the parallel domain for visual media generation.
* [Specialist batteries](/assembly/batteries-specialists) — the inverse domain (modality INTO text).
