Skip to main content

Pipecat Gandr TTS

Official Gandr text-to-speech integration for Pipecat, the framework for building voice and multimodal conversational agents.

Note: This integration is maintained by Gandr. As the provider of the TTS service, we keep it current with Pipecat releases and with our own API.

Table of Contents


Why Gandr

Audio streams back as it is generated. The first bytes arrive while the rest of the utterance is still rendering, so a pipeline can start playback instead of waiting on a finished clip.

WER 1.982%, against a 2.171% human baseline. One whisper-large-v3 scorer transcribed everything, including the human baseline, so the two numbers are comparable. n=1,088, zero render errors.

Pipecat compatibility

Tested with Pipecat v1.7.0 on Python 3.12.11 (2026-08-07): clean virtual environment, pip install exit 0, imports, 11/11 unit tests pass, service constructs.

Built against the WebsocketTTSService base class and the audio-context API, so it supports pipecat-ai from 0.0.108 up to (but not including) 2.0.0. The declared floor is 0.0.108 because that is what it was written against; the tested figure above crosses a major version and about a year of API drift, which is the number worth trusting.

Not claimed: no live socket has been opened against the service in that test. It covers install, import, unit tests and construction. Rendering audio costs money, so it is left to whoever runs it.

Installation

uv add pipecat-gandr

or with pip:

pip install pipecat-gandr

From source

git clone https://github.com/Gandr-AI/gandr-pipecat.git
cd gandr-pipecat
pip install -e .

Quick start

1. Get an API key

Keys are gnd_… strings. See gandr.ai/docs.

2. Basic usage

import os

from pipecat_gandr import GandrTTSService

tts = GandrTTSService(
    api_key=os.getenv("GANDR_API_KEY"),
    params=GandrTTSService.InputParams(
        voice_id="gandr-mia",
        language="en",
        sample_rate=24000,
    ),
)

3. In a pipeline

pipeline = Pipeline(
    [
        transport.input(),
        stt,
        context_aggregator.user(),
        llm,
        tts,
        transport.output(),
        context_aggregator.assistant(),
    ]
)

A complete runnable pipeline is in examples/foundational/gandr_tts_basic.py.

Configuration

GandrTTSService.InputParams:

Parameter Type Default Range / options Description
voice_id str "gandr-mia" a stock id, or a gnd: clone id Voice for synthesis
language str "en" ISO language code Language of the input text
sample_rate int 24000 8000, 16000, 22050, 24000 Output rate. Takes priority over a sample_rate passed to the constructor
speed float None 0.6 to 1.5 Playback speed, pitch preserving
volume float None 0.5 to 2.0 Output gain, soft-ceiling mastered
temperature float None , Expression control. Omit and the API chooses
cfg_weight float None , Expression control. Omit and nothing is sent
seed int None , Accepted and forwarded; renders are currently non-deterministic regardless of value
voice_wav_b64 str None base64 WAV Reference audio for a cloned voice
voice_consent bool None True Confirms the speaker agreed to the clone; required with voice_wav_b64
word_timestamps bool None on/off Word-level timing: the utterance tail then carries per-word offsets, fed into Pipecat's word-timestamp machinery for highlighting

Constructor arguments beyond api_key and params:

Argument Default Description
url wss://tts.gandr.ai/ws Streaming endpoint
text_aggregation_mode None How Pipecat aggregates text before synthesis
utterance_timeout_s 30.0 How long to wait for an utterance's closing frame
busy_retry_s 0.5 Wait before retrying after the server answers busy
max_attempts 3 Attempts per utterance, retries included
reconnect_on_interruption True See Interruptions

On sample_rate: 24000 is the API's default output rate and the right choice for almost every agent, including telephony. If your transport needs narrowband, let the transport resample, Pipecat does it for free, rather than asking the server for a narrowband stream.

Changing voice mid-session

Voice, language and expression controls are read per utterance, so a TTSUpdateSettingsFrame takes effect on the next thing the bot says:

from pipecat.frames.frames import TTSUpdateSettingsFrame

await task.queue_frame(TTSUpdateSettingsFrame(settings={"voice": "gandr-leo"}))

Cloned voices

A cloned voice is registered per connection. Pass the reference audio once and the service attaches it to the first utterance on each connection, and re-attaches it automatically if the connection is reopened:

tts = GandrTTSService(
    api_key=os.getenv("GANDR_API_KEY"),
    params=GandrTTSService.InputParams(
        voice_id="gnd:your-clone-id",
        voice_wav_b64=reference_wav_base64,
        voice_consent=True,  # the speaker agreed to be cloned
    ),
)

If the server asks for a voice and none was configured, the service raises a clear error naming voice_wav_b64 rather than going silent. Reference audio needs voice_consent=True, your confirmation that the speaker agreed; without it the API refuses the clone and the error says so.

Voices

Stock voices: gandr-mia, gandr-ava, gandr-jenny, gandr-dane, gandr-leo, gandr-lewis. Cloned voices are gnd: identifiers.

How the integration works

The service holds one WebSocket to wss://tts.gandr.ai/ws for the life of the pipeline. Each utterance is a JSON message; the server answers with binary frames of raw PCM16LE mono audio as it renders, then a JSON frame that closes the utterance.

The connection carries many utterances but renders one at a time, so the service serialises sends: run_tts queues an utterance and a single sender task delivers it, waits for the closing frame, and only then sends the next. That is what keeps the server's busy backpressure from ever becoming the normal case. If it happens anyway, the send is retried after busy_retry_s.

Audio is delivered through Pipecat's audio-context API, so frames stay bound to the turn that requested them and are dropped cleanly when that turn is cancelled.

Interruptions

The wire protocol has no cancel message. When the user barges in, audio already rendering is discarded client-side, but it would still occupy the connection, and the next turn's first byte would queue behind audio nobody is listening to.

So by default (reconnect_on_interruption=True) the service reopens the connection on interruption. Set it to False to keep the connection and accept that the next turn waits for the interrupted render to drain.

Long text

The API caps a single request's transcript at 2000 characters. Pipecat normally hands over a sentence at a time, so this rarely comes up; when it does, the service splits the text on the cleanest boundary it can find, sentence end first, then a word boundary, and sends the pieces back to back on the same connection under the same turn. Nothing is dropped, and only the last piece closes the turn.

Errors

Failures surface as Pipecat ErrorFrames through push_error, and the turn is always closed with a TTSStoppedFrame so the pipeline never hangs waiting for audio that is not coming. Connection loss is handled by reopening and, where the utterance had not yet reached the wire, resending it.

Metrics

can_generate_metrics() is True.

Time to first byte is stopped on the first audio byte off the wire, so what your dashboard shows is the same event the service itself measures. Usage metrics are reported per request from the full text of the turn.

The server reports its own timings on the frame that closes each utterance. This service neither consumes nor logs them, so the only latency it ever reports is the one it measured itself.

Pricing

  • $10 a month for one million tokens (one token is one character), resets monthly.
  • $150 per stream per month.

See gandr.ai/pricing.

Environment variables

GANDR_API_KEY=your_gandr_api_key_here
OPENAI_API_KEY=your_openai_key_here      # if using with an LLM
DEEPGRAM_API_KEY=your_deepgram_key_here  # if using with STT

Examples

uv add "pipecat-ai[deepgram,openai,silero,local]"
python examples/foundational/gandr_tts_basic.py

Tests

pytest tests

The transcript tests import pipecat_gandr._text directly and need neither Pipecat nor a network, so they run anywhere.

Requirements

  • Python >= 3.11
  • pipecat-ai >= 0.0.108, < 2.0.0
  • websockets >= 15.0.1, < 16.0
  • pydantic >= 2.0
  • loguru >= 0.7.3
  • python-dotenv >= 1.1.1

License

MIT. See LICENSE.

Support

Metadata

Release files for pipecat-gandr 0.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pipecat-gandr 0.1.5
File Size Uploaded Provenance
pipecat_gandr-0.1.5.tar.gz 199.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pipecat-gandr 0.1.5
File Interpreter ABI Platform
pipecat_gandr-0.1.5-py3-none-any.whl Python 3 none any Details

Total release size: 216.9 kB

Release files / pipecat_gandr-0.1.5.tar.gz

Download URL pipecat_gandr-0.1.5.tar.gz
Size 199.5 kB
Tags Source
SHA-256 checksum
How to use checksums
d23cceda258aca4a5d0bf7381a7fa7b11a121eb6cfe0aa7704a0f4a5e56846cd
BLAKE2b-256 checksum
How to use checksums
105cab7f15045f00546d2e06ae0209ee66e180c2a405b02fcb50e437c828ee8b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / pipecat_gandr-0.1.5-py3-none-any.whl

Download URL pipecat_gandr-0.1.5-py3-none-any.whl
Size 17.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ebd4f4bf2e416f5753a4f770b3933340f2d4e622b2673681b0b544fed846218e
BLAKE2b-256 checksum
How to use checksums
274c6c8d68478cfe0eef88c4843455c4976448d89f53896c0821cf23a68b6e27
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for pipecat-gandr 0.1.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page