# AssemblyAI

> Streaming text-to-speech with per-word timings over AssemblyAI's Streaming TTS WebSocket API.

Source: https://polvallverdu.github.io/pipedog/docs/services/tts/assemblyai/



## Overview [#overview]

`AssemblyAITTSService` streams text to AssemblyAI's Streaming TTS WebSocket API over one session
and plays the audio as it arrives, with per-word timings for word-level `TTSTextFrame`s. It is a
port of Pipecat's `AssemblyAITTSService`. AssemblyAI's speech-to-text services, in the same
package, are on the [AssemblyAI speech-to-text](https://polvallverdu.github.io/pipedog/docs/services/stt/assemblyai.md) page.

<Cards>
  <Card title="Upstream page" href="https://docs.pipecat.ai/api-reference/server/services/tts/assemblyai">
    Pipecat's documentation for the same service.
  </Card>

  <Card title="Source" href="https://github.com/polvallverdu/pipedog/tree/main/packages/assemblyai/src">
    `packages/assemblyai/src/tts.ts`.
  </Card>
</Cards>

## Installation [#installation]

```bash
bun add @pipedog/assemblyai
```

## Prerequisites [#prerequisites]

* An [AssemblyAI](https://www.assemblyai.com) account and API key, passed as `apiKey` (the
  examples read `ASSEMBLYAI_API_KEY`). A temporary token opens a single session, so it can't carry
  the service's reconnects and session renewals.

## AssemblyAITTSService [#assemblyaittsservice]

### Configuration [#configuration]

| Option | Type | Required | Description |
|---|---|---|---|
| `apiKey` | `string` | yes | AssemblyAI API key. A temporary token opens a single session, so it can't carry the service's reconnects and session renewals. |
| `url` | `string \| undefined` | no | Streaming TTS WebSocket URL. Use `wss://tts.eu.assemblyai.com/v1/ws` to process text and audio in the EU. A cloned voice works only on the host of the zone it was created in. |
| `sampleRate` | `number \| null \| undefined` | no | Output sample rate in Hz. If null, uses the pipeline's output rate. |
| `settings` | `AssemblyAITTSSettings \| null \| undefined` | no | Runtime-updatable settings. |
| `name` | `string \| null \| undefined` | no | Custom name; defaults to `ClassName#count`. |
| `taskManager` | `BaseTaskManager \| null \| undefined` | no | Task manager for handling tasks. |
| `enableDirectMode` | `boolean \| undefined` | no | Process frames immediately instead of using internal queues. |
| `metrics` | `FrameProcessorMetrics \| null \| undefined` | no | Optional metrics collector for this processor. |
| `reconnectBackoffMinWait` | `number \| undefined` | no | Minimum time, in seconds, to wait between reconnection attempts. |
| `reconnectBackoffMaxWait` | `number \| undefined` | no | Maximum time, in seconds, to wait between reconnection attempts. |
| `reconnectOnError` | `boolean \| undefined` | no | Whether to automatically reconnect on connection errors. |
| `wsCloseTimeout` | `number \| undefined` | no | Maximum time, in seconds, to wait for the peer to acknowledge the websocket closing handshake before dropping the connection. Applied to connections opened through `_websocketConnect()`. Increase it for peers that need longer to complete a graceful close. |
| `wsSendTimeout` | `number \| null \| undefined` | no | Maximum time, in seconds, a single send may wait on a peer that has stopped reading before the connection is dropped and the send throws `ConnectionClosedError`. Applied to connections opened through `_websocketConnect()`. Pass `null` to wait indefinitely. Defaults to `WS_SEND_TIMEOUT`. |
| `textAggregationMode` | `TextAggregationMode \| null \| undefined` | no | How to aggregate incoming text before synthesis. `TextAggregationMode.SENTENCE` (default) buffers until sentence boundaries, `TextAggregationMode.TOKEN` streams tokens directly for lower latency. |
| `pushTextFrames` | `boolean \| undefined` | no | Whether to push TextFrames and LLMFullResponseEndFrames. |
| `pushStopFrames` | `boolean \| undefined` | no | Whether to automatically push TTSStoppedFrames. |
| `pushStartFrame` | `boolean \| undefined` | no | Whether to automatically create audio contexts and push TTSStartedFrames. When true, the base class handles `createAudioContext` and yields `TTSStartedFrame` before each synthesis call, so `runTts` implementations do not need to. |
| `stopFrameTimeoutS` | `number \| undefined` | no | Idle time before pushing TTSStoppedFrame when pushStopFrames is true. |
| `pushSilenceAfterStop` | `boolean \| undefined` | no | Whether to push silence audio after TTSStoppedFrame. |
| `silenceTimeS` | `number \| undefined` | no | Duration of silence to push when pushSilenceAfterStop is true. |
| `pauseFrameProcessing` | `boolean \| undefined` | no | Whether to hold incoming frames after a turn until its audio contexts end. Needed by services whose audio doesn't carry its request's context ID. |
| `maxConsecutiveZeroAudioContexts` | `number \| undefined` | no | How many consecutive TTS contexts may complete without producing any audio before the service is reported unable to do its job. Catches a provider that accepts requests and stays silent — an unknown voice ID, say — which no error ever surfaces. Every silent context reports an error the service can carry on from, whatever the limit is; on reaching the limit the service reports a permanent error instead, stops being given work, and the pipeline worker applies its `ProcessorUnusablePolicy`. Set to 0 to report silent contexts without ever writing the service off. |
| `appendTrailingSpace` | `boolean \| undefined` | no | Whether to append a trailing space to text before sending to TTS. This helps prevent some TTS services from vocalizing trailing punctuation (e.g., "dot"). Only applied in sentence aggregation mode; when streaming tokens, the incoming text's own whitespace is preserved. |
| `skipTextTypes` | `string[] \| null \| undefined` | no | List of text types that should not be spoken. |
| `textTransforms` | `[string, TextTransform][] \| null \| undefined` | no | A list of TextTransforms to transform text just before sending it to TTS. Each TextTransform takes the aggregated text and its type, and returns the transformed text. To register, provide a list of tuples of `[textType \| "*", transformFunction]`. |
| `transportDestination` | `string \| null \| undefined` | no | Destination for generated audio frames. |
| `reuseContextIdWithinTurn` | `boolean \| undefined` | no | Whether the service should reuse context IDs within the same turn. |
| `audioCache` | `TTSAudioCacheStore \| null \| undefined` | no | Where to keep synthesized audio for reuse (e.g. a shared `TTSAudioCache`). Text whose audio is in the cache is served from it, with no request to the provider. Lines get there through `warmAudioCache()`, and also from live synthesis when `cacheSynthesizedAudio` allows it. Only services that yield their audio from `runTts` (HTTP and streaming-RPC services, see `supportsAudioCache`) use it, and only in sentence aggregation mode. Defaults to null (no cache). pipedog only. |
| `cacheSynthesizedAudio` | `boolean \| ((text: string) => boolean) \| undefined` | no | Whether audio synthesized live is stored in `audioCache`, or a function that decides per text. Defaults to false: only warmed lines are stored, since a generated reply can repeat what the user said. pipedog only. |
| `audioCacheNamespace` | `string \| undefined` | no | Extra part of the cache key, for constructor options that change the audio but are not in `settings`. pipedog only. |

The default URL is `wss://tts.us.assemblyai.com/v1/ws`. Use `wss://tts.eu.assemblyai.com/v1/ws`
to process text and audio in the EU. A cloned voice works only on the host of the zone it was
created in.

### Settings [#settings]

Runtime-updatable settings, passed as `settings: new AssemblyAITTSService.Settings({...})`
(`AssemblyAITTSSettings`). Settings you leave out keep their defaults. The session is configured
when it connects, so changing any of them reconnects.

| Setting    | Type               | Default                                           | Description                                                                                                                                                                                    |
| ---------- | ------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `voice`    | `string \| null`   | `"49c443a4-8a1d-5f7e-b70b-98bdbd9823c3"` (Audrey) | Sent as `voice_id`: any voice by its ID, an AssemblyAI voice by its library slug such as `aai-jane`, or a cloned voice by the slug the dashboard shows. `null` leaves the voice to the server. |
| `language` | `Language \| null` | `null`                                            | The language the session speaks: German, English, Spanish, French, Italian or Portuguese. Unset, the voice's own language.                                                                     |
| `model`    | `string \| null`   | `null`                                            | The model, such as `assemblyai-tts-1`. Unset, the language's default model.                                                                                                                    |

### Usage [#usage]

```ts title="tts.ts"
import { AssemblyAITTSService } from "@pipedog/assemblyai/tts";
import { TTSUpdateSettingsFrame } from "@pipedog/core/frames/frames";
import { Language } from "@pipedog/core/transcriptions/language";

// Speaks in Audrey, the default voice.
const tts = new AssemblyAITTSService({ apiKey: process.env.ASSEMBLYAI_API_KEY ?? "" });
```

Pick a voice and a region:

```ts
// A library voice by its slug, processed in the EU.
const eu = new AssemblyAITTSService({
  apiKey: process.env.ASSEMBLYAI_API_KEY ?? "",
  url: "wss://tts.eu.assemblyai.com/v1/ws",
  settings: new AssemblyAITTSService.Settings({ voice: "aai-jane", language: Language.EN }),
});
```

Change settings mid-call with a `TTSUpdateSettingsFrame`:

```ts
// The session is configured at connect time, so a new voice reconnects.
const update = new TTSUpdateSettingsFrame({
  delta: new AssemblyAITTSService.Settings({ voice: "987db62e-a2c2-569a-9c4d-82460cdea1bd" }),
});
```

### Notes [#notes]

* Each sentence is sent as its own request (a `Generate`, then a `Flush`). With
  `TextAggregationMode.TOKEN`, the turn's text streams as it is generated and is flushed when the
  turn ends. Text longer than the session's limit is split across `Generate` messages.
* Interruptions send `Cancel`, which stops synthesis in flight and keeps the session open.
* The service keeps an idle session open with a `KeepAlive` every 30 seconds, replaces a session
  between requests before its one-hour limit, and waits as long as an error asks before
  reconnecting.
* `sampleRate` left out uses the pipeline's output rate.

## Runtimes [#runtimes]

| Runtime            | Works | What it needs |
| ------------------ | ----- | ------------- |
| Bun                | ✅ yes | Nothing.      |
| Node.js            | ✅ yes | Nothing.      |
| Deno               | ✅ yes | Nothing.      |
| Cloudflare Workers | ✅ yes | Nothing.      |

Generated from `pipecat.runtimes` and `pipecat.compat` in `packages/assemblyai/package.json`: ✅ means the compat smoke test passes on that runtime.

The service uses the global `WebSocket` (through core's `websocketConnect`, which sends the
`Authorization` header on the handshake on Cloudflare Workers too). No AssemblyAI SDK is needed.
