TTS batteries
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/ttsre-exports the transformers.js engine but omits the native engine entirely. Because the native engine relies onnode:child_processandnode: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 tosay -v <voice>,espeak-ng -v <voice>, PowerShellSelectVoice, or a transformers.jsspeaker_embeddingsURL/string.rate?: number: A multiplier where1is engine-normal. Mapped per-engine:say/espeak-ngcomputeround(175 * rate)words-per-minute (clamped 80–500), PowerShell maps it to a-10..10band, and transformers.js applies it asspeed. The transformers.js adapter validatesrateas a positive number (zero or negative throwsE_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 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 — synthesize() returns a GeneratedMediaOutput, which the handler persists with ctx.storeMediaBytes and wraps in Media.toolGenerated:
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 — the parallel domain for visual media generation.
- Specialist batteries — the inverse domain (modality INTO text).