Bland
Text-to-speech with Bland: BlandTTSService streams LLM tokens over a realtime WebSocket, and BlandHttpTTSService posts whole texts.
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 sendscancelwithout dropping the connection.BlandHttpTTSService: one request per text to/v2/tts. Voice agents should use the WebSocket service.
Upstream page
Pipecat's documentation for the same service.
Source
packages/bland/src/tts.ts.
Bland docs
The realtime WebSocket reference.
Installation
bun add @pipedog/blandimport { BlandHttpTTSService, BlandTTSService } from "@pipedog/bland/tts";Prerequisites
- A Bland account and an API key.
- Pass the key as
apiKey. The examples read it fromBLAND_API_KEY. An empty key throws.
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
Prop
Type
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
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:
// 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 | Description |
|---|---|
on_connected | The session is ready. |
on_disconnected | The WebSocket closed. |
on_connection_error | The connection failed. |
BlandHttpTTSService
Posts to https://api.bland.ai/v2/tts and streams the raw PCM response into frames.
Configuration
Prop
Type
It takes the same settings as the WebSocket service.
Usage
const httpTts = new BlandHttpTTSService({
apiKey: process.env.BLAND_API_KEY ?? "",
settings: new BlandHttpTTSService.Settings({ autoFormatting: true }),
});Notes
textAggregationModedefaults toTextAggregationMode.TOKENfor the WebSocket service. PassTextAggregationMode.SENTENCEto 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
closeand waits up to five seconds fordone, 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
- The WebSocket opens through the core
websocketConnect, with the key in anAuthorizationheader (throughfetchon Cloudflare Workers). The HTTP service uses the globalfetch; passfetchto 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.
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).