Pipecat

Add Speechify TTS with word timestamps to a Pipecat voice pipeline via Pipecat's SpeechifyHttpTTSService.

Overview

Pipecat ships Speechify TTS in its core package as SpeechifyHttpTTSService, first released in pipecat-ai 1.8.0. It is the TTS stage of a Pipecat pipeline: for each sentence the LLM writes, it calls POST /v1/audio/stream/with-timestamps, reads the Server-Sent Events stream, and pushes PCM audio frames and word timestamps downstream as they arrive.

Pipecat maintains the service. Its Speechify reference lists every argument.

Prerequisites

  • Speechify API key (platform.speechify.ai/api-keys)
  • Python 3.11 or later
  • For the full agent: a Deepgram key for STT and an OpenAI key for the LLM (this guide’s stack, swap freely)

Install

python3 -m venv .venv
source .venv/bin/activate
pip install "pipecat-ai[speechify,deepgram,openai,silero,webrtc,runner]>=1.8.0" python-dotenv

pipecat-ai versions before 1.8.0 have no Speechify service. For the TTS smoke test alone, pip install "pipecat-ai[speechify]>=1.8.0" is enough.

Configure

Put credentials in .env:

SPEECHIFY_API_KEY=your_speechify_api_key
DEEPGRAM_API_KEY=your_deepgram_api_key
OPENAI_API_KEY=your_openai_api_key

The service does not read the environment itself. Pass the key as api_key=.

Create the service

Inside your bot’s async entry point:

import os
import aiohttp
from pipecat.services.speechify.tts import SpeechifyHttpTTSService
async with aiohttp.ClientSession() as session:
tts = SpeechifyHttpTTSService(
api_key=os.environ["SPEECHIFY_API_KEY"],
aiohttp_session=session,
settings=SpeechifyHttpTTSService.Settings(voice="geffen_32", model="simba-3.2"),
)
ArgumentDefaultNotes
api_keyRequiredYour Speechify API key.
aiohttp_sessionRequiredAn aiohttp.ClientSession you create and close. Keep it open for as long as the pipeline runs.
base_urlhttps://api.speechify.aiLeave it unset. The service appends /v1/audio/stream/with-timestamps, so a value ending in /v1 requests /v1/v1/... and fails with 404.
sample_rateThe pipeline’s output rateSent as output_format: pcm_<rate>. Supported rates are 8000, 16000, 22050, 24000, 44100 and 48000 Hz; any other rate falls back to 24000 Hz and the output transport resamples.
settings.voicegeffen_32Any voice whose models list includes settings.model.
settings.modelsimba-3.2simba-3.2 for English, simba-3.0 for other languages.
settings.languageNot sentA Pipecat Language: DE, EN, ES, FR, IT and PT map to de-DE, en-US, es-ES, fr-FR, it-IT and pt-BR.
settings.loudness_normalization, settings.text_normalizationNot sentEach adds latency.

Change any setting mid-conversation by queueing a TTSUpdateSettingsFrame(delta=SpeechifyHttpTTSService.Settings(...)).

Pair voice and model by language

Languagemodelvoice
Englishsimba-3.2Any English voice: the *_32 roster (geffen_32, dominic_32, harper_32, …) or your own cloned voices
German, Spanish, French, Italian, Portuguesesimba-3.0A voice in that language, with the matching settings.language
from pipecat.transcriptions.language import Language
settings = SpeechifyHttpTTSService.Settings(voice="anton", model="simba-3.0", language=Language.DE)

simba-3.2 is English only and returns 400 for a non-English voice. Each voice’s models list in GET /v1/voices is the authority. Store voice and model together in config so a later swap cannot mismatch them.

Use simba-3.2 or simba-3.0 with this service. Older model ids are retired from API version 2026-09-21 and return 400 model_retired. See Models.

Word timestamps

Each speech.chunk event carries base64 PCM, the speech marks that became final with it, or both: marks lag their audio slightly, and the last chunk is often marks-only. Mark times are absolute milliseconds from the start of the request’s audio, not offsets into the chunk they arrive on. The service joins the marks into whole words and pushes each one as a TTSTextFrame whose presentation timestamp is the moment that word starts playing.

Pipecat uses those frames to record what the bot actually said. When the user interrupts, the assistant’s turn in the LLM context keeps only the words that were spoken, not the whole reply the LLM wrote.

Verify the TTS stage

This script runs a two-stage pipeline, the Speechify service and a processor that captures its output, so it needs only your Speechify key:

import asyncio
import os
import wave
import aiohttp
from pipecat.frames.frames import EndFrame, TTSAudioRawFrame, TTSSpeakFrame, TTSTextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineWorker
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.services.speechify.tts import SpeechifyHttpTTSService
from pipecat.workers.runner import WorkerRunner
class Capture(FrameProcessor):
def __init__(self):
super().__init__()
self.audio = bytearray()
self.audio_frames = 0
self.sample_rate = 0
self.words = []
async def process_frame(self, frame, direction: FrameDirection):
await super().process_frame(frame, direction)
if isinstance(frame, TTSAudioRawFrame):
self.audio += frame.audio
self.audio_frames += 1
self.sample_rate = frame.sample_rate
elif isinstance(frame, TTSTextFrame):
self.words.append((frame.text, frame.pts))
await self.push_frame(frame, direction)
async def main():
async with aiohttp.ClientSession() as session:
tts = SpeechifyHttpTTSService(
api_key=os.environ["SPEECHIFY_API_KEY"],
aiohttp_session=session,
settings=SpeechifyHttpTTSService.Settings(voice="geffen_32", model="simba-3.2"),
)
capture = Capture()
worker = PipelineWorker(Pipeline([tts, capture]))
await worker.queue_frames([TTSSpeakFrame("Hello from Speechify on Pipecat."), EndFrame()])
runner = WorkerRunner(handle_sigint=False)
await runner.add_workers(worker)
await runner.run()
with wave.open("speechify-pipecat.wav", "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(capture.sample_rate)
wav.writeframes(capture.audio)
seconds = len(capture.audio) / 2 / capture.sample_rate
print(f"{capture.audio_frames} audio frames, {seconds:.2f}s at {capture.sample_rate} Hz")
first = capture.words[0][1]
for word, pts in capture.words:
print(f"{(pts - first) / 1e9:5.2f}s {word}")
asyncio.run(main())

It writes speechify-pipecat.wav and prints the audio frames and word timestamps it received. Frame counts and times vary from run to run:

12 audio frames, 2.77s at 24000 Hz
0.00s Hello
0.46s from
0.63s Speechify
1.74s on
1.96s Pipecat.

Run a full agent

bot.py wires Speechify into a Deepgram and OpenAI pipeline and serves it over Pipecat’s local WebRTC transport:

import os
import aiohttp
from dotenv import load_dotenv
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.speechify.tts import SpeechifyHttpTTSService
from pipecat.transports.base_transport import TransportParams
from pipecat.workers.runner import WorkerRunner
load_dotenv(override=True)
async def bot(runner_args: RunnerArguments):
transport = await create_transport(
runner_args,
{"webrtc": lambda: TransportParams(audio_in_enabled=True, audio_out_enabled=True)},
)
async with aiohttp.ClientSession() as session:
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a helpful voice assistant. Your replies are spoken aloud, "
"so keep them short and avoid lists, markdown and emojis."
),
),
)
tts = SpeechifyHttpTTSService(
api_key=os.environ["SPEECHIFY_API_KEY"],
aiohttp_session=session,
settings=SpeechifyHttpTTSService.Settings(voice="geffen_32", model="simba-3.2"),
)
context = LLMContext()
user_aggregator, assistant_aggregator = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
pipeline = Pipeline(
[
transport.input(),
stt,
user_aggregator,
llm,
tts,
transport.output(),
assistant_aggregator,
]
)
worker = PipelineWorker(pipeline, params=PipelineParams(enable_metrics=True))
runner = WorkerRunner(handle_sigint=runner_args.handle_sigint)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
context.add_message({"role": "developer", "content": "Greet the user in one short sentence."})
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
await runner.cancel()
await runner.add_workers(worker)
await runner.run()
if __name__ == "__main__":
from pipecat.runner.run import main
main()

Pipeline order matters: transport.input() receives the user’s audio, Deepgram transcribes it, the user aggregator adds the transcript to the LLM context, the LLM writes the reply, Speechify speaks it, and transport.output() plays it back. The assistant aggregator sits last so it records only what reached the transport.

Run it and open http://localhost:7860 to talk to the agent in the browser:

python bot.py -t webrtc

The session lives inside async with aiohttp.ClientSession(), so it stays open for the whole conversation and closes when the runner ends.

Sample rate and your API version

The service requests audio at the pipeline’s output rate, 24 kHz unless your transport or PipelineParams(audio_out_sample_rate=...) sets another. It sends no Speechify-Version header, so your workspace’s pinned API version decides how the request is served.

On a workspace pinned before 2026-09-30, pcm_16000 returns 24 kHz audio, which a 16 kHz pipeline plays 1.5x slow and pitched down. If your pipeline runs at 16 kHz, move the pin to 2026-09-30 or later, or pass sample_rate=24000 to the service so the output transport resamples the audio. See the pcm_16000 changelog entry.

Troubleshooting

The service needs an aiohttp.ClientSession. Create one with async with aiohttp.ClientSession() as session: and pass it as aiohttp_session=session.

The class is SpeechifyHttpTTSService, and it first shipped in pipecat-ai 1.8.0. Upgrade with pip install -U "pipecat-ai[speechify]>=1.8.0".

base_url ends in /v1. Remove the argument; the default is https://api.speechify.ai.

The voice and model do not pair. simba-3.2 takes English voices only; use simba-3.0 with a voice in another language. The voice’s models list in GET /v1/voices names every model it pairs with.

Resources