pipedog is an unofficial community port of Pipecat, not affiliated with Daily.
pipedog
Text-to-speech

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 sends cancel without dropping the connection.
  • BlandHttpTTSService: one request per text to /v2/tts. Voice agents should use the WebSocket service.

Installation

bun add @pipedog/bland
import { BlandHttpTTSService, BlandTTSService } from "@pipedog/bland/tts";

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

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.

SettingTypeDefaultDescription
voicestring"29158307-9893-4149-8a75-bc9ce313d64e"Voice UUID. The voice sets the model.
expressivenessnumber | nullnull0.0 to 1.0. Higher values vary the intonation more. Calibrated for BTTS_V3.
stabilitynumber | nullnull0.0 to 1.0. Higher values keep delivery more consistent.
autoFormattingboolean | nullnullRewrite numbers into the form the voice reads best, such as phone numbers digit by digit.

Usage

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:

// 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

EventDescription
on_connectedThe session is ready.
on_disconnectedThe WebSocket closed.
on_connection_errorThe 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

  • 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

  • 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.
RuntimeWorksWhat it needs
Bun✅ yesNothing.
Node.js✅ yesNothing.
Deno✅ yesNothing.
Cloudflare Workers✅ yesNothing.

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).

On this page