# Bland

> Text-to-speech with Bland: BlandTTSService streams LLM tokens over a realtime WebSocket, and BlandHttpTTSService posts whole texts.

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



## Overview [#overview]

* **`BlandTTSService`**: one WebSocket for the whole conversation. LLM tokens go to Bland as they arrive (Bland chooses its own synthesis boundaries), audio comes back as binary frames, and a barge-in sends `cancel` without dropping the connection.
* **`BlandHttpTTSService`**: one request per text to `/v2/tts`. Voice agents should use the WebSocket service.

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

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

  <Card title="Bland docs" href="https://docs.bland.ai/api-v2/post/tts-ws">
    The realtime WebSocket reference.
  </Card>
</Cards>

## Installation [#installation]

```bash
bun add @pipedog/bland
```

```ts
import { BlandHttpTTSService, BlandTTSService } from "@pipedog/bland/tts";
```

## Prerequisites [#prerequisites]

* A Bland account and an API key.
* Pass the key as `apiKey`. The examples read it from `BLAND_API_KEY`. An empty key throws.

## BlandTTSService [#blandttsservice]

Connects to `wss://api.bland.ai/v2/tts/ws` and waits for the session's `ready` message, so a session Bland refuses fails at connect, not on the first turn.

### Configuration [#configuration]

| Option | Type | Required | Description |
|---|---|---|---|
| `apiKey` | `string` | yes | Bland API key for authentication. |
| `url` | `string \| undefined` | no | WebSocket URL for the Bland realtime TTS API. Defaults to `wss://api.bland.ai/v2/tts/ws`. |
| `textAggregationMode` | `TextAggregationMode \| null \| undefined` | no | How to aggregate incoming text before sending. Defaults to `TextAggregationMode.TOKEN`, streaming LLM tokens straight to Bland for the lowest latency. |
| `settings` | `BlandTTSSettings \| 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`. |
| `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. |
| `sampleRate` | `number \| null \| undefined` | no | Output sample rate for generated audio. |
| `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. |

### Settings [#settings]

Runtime-updatable settings, passed as `settings: new BlandTTSService.Settings({...})`
(`BlandTTSSettings`). Any setting you leave out keeps the default below. A change to `voice`, `expressiveness`, `stability` or `autoFormatting` opens a new session, because the session's `init` message fixes them.

| Setting          | Type              | Default                                  | Description                                                                               |
| ---------------- | ----------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------- |
| `voice`          | `string`          | `"29158307-9893-4149-8a75-bc9ce313d64e"` | Voice UUID. The voice sets the model.                                                     |
| `expressiveness` | `number \| null`  | `null`                                   | 0.0 to 1.0. Higher values vary the intonation more. Calibrated for `BTTS_V3`.             |
| `stability`      | `number \| null`  | `null`                                   | 0.0 to 1.0. Higher values keep delivery more consistent.                                  |
| `autoFormatting` | `boolean \| null` | `null`                                   | Rewrite numbers into the form the voice reads best, such as phone numbers digit by digit. |

### Usage [#usage]

```ts title="tts.ts"
import { BlandHttpTTSService, BlandTTSService } from "@pipedog/bland/tts";
import { TTSUpdateSettingsFrame } from "@pipedog/core/frames/frames";

const tts = new BlandTTSService({
  apiKey: process.env.BLAND_API_KEY ?? "",
  settings: new BlandTTSService.Settings({
    voice: "29158307-9893-4149-8a75-bc9ce313d64e",
    expressiveness: 0.6,
    stability: 0.5,
  }),
});
```

Change settings mid-call with a `TTSUpdateSettingsFrame`:

```ts
// Opens a new session, since the voice and controls are part of the init message.
const update = new TTSUpdateSettingsFrame({
  delta: new BlandTTSService.Settings({ stability: 0.8 }),
});
```

### Event handlers [#event-handlers]

| Event                 | Description            |
| --------------------- | ---------------------- |
| `on_connected`        | The session is ready.  |
| `on_disconnected`     | The WebSocket closed.  |
| `on_connection_error` | The connection failed. |

## BlandHttpTTSService [#blandhttpttsservice]

Posts to `https://api.bland.ai/v2/tts` and streams the raw PCM response into frames.

### Configuration [#configuration-1]

| Option | Type | Required | Description |
|---|---|---|---|
| `apiKey` | `string` | yes | Bland API key for authentication. |
| `baseUrl` | `string \| undefined` | no | Base URL for the Bland API. Defaults to `https://api.bland.ai/v2`. |
| `fetch` | `{ (input: RequestInfo \| URL, init?: RequestInit): Promise<Response>; (input: string \| URL \| Request, init?: RequestInit): Promise<Response>; } \| null \| undefined` | no | `fetch` used for HTTP requests. Defaults to the global `fetch`. DIVERGENCE: upstream takes an optional shared `aiohttp_session`, and creates and closes one itself when none is given. |
| `settings` | `BlandTTSSettings \| 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. |
| `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. |
| `sampleRate` | `number \| null \| undefined` | no | Output sample rate for generated audio. |
| `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. |

It takes the same settings as the WebSocket service.

### Usage [#usage-1]

```ts
const httpTts = new BlandHttpTTSService({
  apiKey: process.env.BLAND_API_KEY ?? "",
  settings: new BlandHttpTTSService.Settings({ autoFormatting: true }),
});
```

## Notes [#notes]

* `textAggregationMode` defaults to `TextAggregationMode.TOKEN` for the WebSocket service. Pass `TextAggregationMode.SENTENCE` to send whole sentences, with a trailing space added.
* Bland renders 8000, 16000, 24000, 44100 and 48000 Hz. For another pipeline rate the service asks for 48000 Hz, tags its audio frames with that rate and logs a warning, and the output transport resamples.
* Bland ends a session after 60 seconds without a client message. The service opens a new one without reporting an error, and a turn that was half sent continues on the new session as a new turn.
* At shutdown the service sends `close` and waits up to five seconds for `done`, so Bland settles the usage.
* A turn that Bland refuses or fails is reported once as an error frame, and its audio context is closed.

## Runtime notes [#runtime-notes]

* The WebSocket opens through the core `websocketConnect`, with the key in an `Authorization` header (through `fetch` on Cloudflare Workers). The HTTP service uses the global `fetch`; pass `fetch` to use another one (upstream takes an optional aiohttp session, and creates and closes one itself when none is given). Both work on Bun, Node, Deno and Cloudflare Workers.
* Tested against local servers that follow Bland's documented message flow, not against Bland itself. Bland's HTTP reference doesn't list `auto_formatting`; the service sends it, as upstream does.

| 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/bland/package.json`: ✅ means the compat smoke test passes on that runtime.
