Pipecat
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Written on 2026-10-01 against pipecat-ai 1.12.0 (BSD-2-Clause; Pipecat Flows ships inside it as pipecat.flows, while the
separate pipecat-ai-flows package is at 1.4.0), Smart Turn v3, faster-whisper 1.2.1, Kokoro-82M, LangChain 1.4.3
create_agent and FastAPI 0.142.2 on Python 3.12 (see the version baseline).
Only the code structure was checked here, by reading the 1.12.0 source and the official examples: constructor signatures,
module paths and the runner conventions. Nothing was run, because the pipelines need a microphone, WebRTC peers, Twilio
Media Streams and model downloads. The LangChain processor, the on-prem pipeline, the Flows nodes and the Dockerfile are
adapted from the documented patterns and are illustrative, not compiled. Pipecat moves quickly (the pipeline task class
was renamed to PipelineWorker in the 1.x line and old names are deprecated), so check the release notes of the version you
install.
Pipecat is an open-source Python framework for real-time voice and multimodal agents. Where a hosted platform (ElevenLabs Agents) or a speech-to-speech API (Realtime APIs) owns the pipeline, Pipecat gives you the pipeline and lets you choose every model, run any of them locally, and connect it to the web or the telephone network with a transport. It is the "open framework" branch of the decision map on the landing page, and the one that fits the cascaded design explained in Voice Architectures.
Pipelines and frames
The model has two ideas. A frame is a unit of data that moves through the system: audio chunks, transcription text, LLM text, synthesized audio, but also control signals such as "the user started speaking", "interrupt the bot" and "end of response". A frame processor receives frames, acts on those it understands and pushes everything onward. A pipeline is an ordered list of processors; audio and text flow downstream, and control frames (interruptions, errors) can flow upstream too. Services for STT, LLM and TTS are just processors, as are the transports, aggregators and anything you write.
Two details explain the behavior callers hear. The user aggregator turns speech start and stop events into the context
messages the LLM sees and decides when the bot should answer (see Smart Turn below). When the user speaks over the bot, an
interruption frame cancels the TTS output and the audio already queued in the transport, which is the barge-in mechanism of
Turn-Taking and Interruptions implemented as frame traffic. The assistant
aggregator sits after transport.output() so the context records what was actually spoken rather than what the LLM generated.
A first bot
The runner convention is: write run_bot(transport, runner_args) and a bot(runner_args) entry point, and let Pipecat’s
runner choose the transport (webrtc, daily, websocket or a telephony provider) from the command line.
import os
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.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
from pipecat.workers.runner import WorkerRunner
transport_params = {
"webrtc": lambda: TransportParams(audio_in_enabled=True, audio_out_enabled=True),
"daily": lambda: DailyParams(audio_in_enabled=True, audio_out_enabled=True),
"twilio": lambda: FastAPIWebsocketParams(audio_in_enabled=True, audio_out_enabled=True),
}
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(voice="86e30c1d-714b-4074-a1f2-1cb6b552fb49"),
)
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction="You are DocsAssistant on a phone call. Answer in short plain sentences."
),
)
context = LLMContext()
user_agg, assistant_agg = LLMContextAggregatorPair(
context, user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer())
)
pipeline = Pipeline([
transport.input(), stt, user_agg, llm, tts, transport.output(), assistant_agg,
])
worker = PipelineWorker(pipeline, params=PipelineParams(enable_metrics=True))
runner = WorkerRunner(handle_sigint=runner_args.handle_sigint)
await runner.add_workers(worker)
@transport.event_handler("on_client_connected")
async def on_connected(transport, client):
context.add_message({"role": "developer", "content": "Greet the caller and say you are an AI assistant."})
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_disconnected(transport, client):
await runner.cancel()
await runner.run()
async def bot(runner_args: RunnerArguments):
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
main()
Source: Pipecat: Quickstart and the
getting-started voice agent example
(imports, aggregator pair, pipeline order and event handlers). The service settings are passed through each service’s
Settings class; older keyword arguments such as voice= on the constructor are deprecated. Running python bot.py -t webrtc
starts the runner with a small browser test client, and -t daily or a telephony transport switches the delivery mechanism
without touching the pipeline. Install the services as extras, for example pip install "pipecat-ai[webrtc,deepgram,cartesia,openai,silero,runner]".
Services: STT, LLM and TTS
Each service family has many providers behind one interface, selected by module path under pipecat.services.<provider>.
Examples used on this site: Deepgram, Cartesia, ElevenLabs and Azure for hosted speech; OpenAI, Anthropic, Google and any
OpenAI-compatible server for the LLM; and the local whisper, kokoro, piper, moonshine and ollama integrations in
the next sections. The speech-model trade-offs (accuracy, streaming, latency, licenses) are on
Speech to Text and Text to Speech; Pipecat
adds no new ones, only the plug. Two framework-specific points matter:
-
STT services come in two flavors. Streaming ones (Deepgram and similar) hold a WebSocket and emit interim and final transcripts. Segmented ones (the local Whisper service) wait for VAD to close a speech segment and transcribe it whole, so VAD quality and
stop_secsdirectly add to latency. -
An
LLMContextis provider-neutral. Switching the LLM service does not rewrite the pipeline, which also allows anLLMSwitcheror per-node model changes in Flows.
Transports
A transport joins the pipeline to the outside world and provides input() and output() processors plus connect and
disconnect events.
| Transport | Use |
|---|---|
SmallWebRTC ( |
Peer-to-peer WebRTC from a browser to your Python process, built on |
WebSocket ( |
A |
Daily ( |
|
For a phone call the WebSocket transport takes a frame serializer that speaks the provider’s wire protocol. The Twilio
serializer decodes Media Streams JSON messages carrying mu-law 8 kHz audio into frames and encodes the bot’s audio back;
with auto_hang_up on (the default) it ends the call through the Twilio REST API when the pipeline ends, which is why it
takes the account SID and auth token.
import json
import os
from fastapi import FastAPI, WebSocket
from pipecat.serializers.twilio import TwilioFrameSerializer
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
app = FastAPI()
@app.websocket("/twilio/media")
async def media(websocket: WebSocket):
await websocket.accept()
# Twilio sends a "connected" message and then a "start" message carrying the stream and call SIDs.
await websocket.receive_text()
start = json.loads(await websocket.receive_text())["start"]
serializer = TwilioFrameSerializer(
stream_sid=start["streamSid"],
call_sid=start["callSid"],
account_sid=os.environ["TWILIO_ACCOUNT_SID"],
auth_token=os.environ["TWILIO_AUTH_TOKEN"],
)
transport = FastAPIWebsocketTransport(
websocket=websocket,
params=FastAPIWebsocketParams(
audio_in_enabled=True,
audio_out_enabled=True,
add_wav_header=False,
serializer=serializer,
),
)
await run_bot(transport) # builds the same pipeline as above, with 8 kHz audio
Source: Pipecat: Twilio WebSocket telephony guide and the
runner utilities, which build the same serializer
from the parsed handshake. In practice the runner does this parsing for you (create_transport detects the provider and
fills runner_args.call_data), so hand-writing the route is only needed when you embed the bot in your own FastAPI app. Set
audio_in_sample_rate=8000 and audio_out_sample_rate=8000 in PipelineParams for telephony. The Twilio side (TwiML
<Connect><Stream>, numbers, security) is in Telephony with Twilio;
Asterisk reaches the same WebSocket transport through the route in
Telephony with Asterisk and SIP.
A LangChain agent as the LLM step
The DocsAssistant core is a LangChain create_agent agent (Agents with create_agent) with
retrieval tools. In a Pipecat pipeline it replaces the LLM service: a processor receives the LLMContextFrame that the user
aggregator emits, takes the latest user text, streams the agent’s tokens as text frames between start and end markers, and the
TTS service speaks them. Pipecat ships LangchainProcessor (pip install "pipecat-ai[langchain]") that does this for a
runnable taking {"input": text}. A create_agent graph takes messages and emits message chunks, so the sketch below
writes the small adapter explicitly; it also keeps the conversation memory in the agent’s checkpointer keyed by the call, as
the text channels do.
from langchain.agents import create_agent
from langchain_core.messages import AIMessageChunk
from langgraph.checkpoint.memory import InMemorySaver
from pipecat.frames.frames import (
Frame,
LLMContextFrame,
LLMFullResponseEndFrame,
LLMFullResponseStartFrame,
TextFrame,
)
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from docsassistant.tools import search_docs # the shared retrieval tool
from docsassistant.models import chat_model # a LangChain chat model
agent = create_agent(
chat_model,
tools=[search_docs],
system_prompt="You are DocsAssistant, speaking on a call. Reply in short plain sentences with no lists or markdown.",
checkpointer=InMemorySaver(),
)
class DocsAgentProcessor(FrameProcessor):
def __init__(self, thread_id: str):
super().__init__()
self._config = {"configurable": {"thread_id": thread_id}}
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
if not isinstance(frame, LLMContextFrame):
await self.push_frame(frame, direction)
return
messages = frame.context.get_messages()
last = messages[-1] if messages else {}
text = last.get("content", "") if isinstance(last, dict) else ""
if not isinstance(text, str) or not text.strip():
return
await self.push_frame(LLMFullResponseStartFrame())
async for chunk, meta in agent.astream(
{"messages": [{"role": "user", "content": text}]},
self._config,
stream_mode="messages",
):
if isinstance(chunk, AIMessageChunk) and chunk.content and not chunk.tool_call_chunks:
out = TextFrame(chunk.content if isinstance(chunk.content, str) else "")
out.includes_inter_frame_spaces = True
await self.push_frame(out)
await self.push_frame(LLMFullResponseEndFrame())
Source: LangChain: Streaming (the messages stream mode) and the
Pipecat LangchainProcessor source
(the start frame, text frames with includes_inter_frame_spaces, and the end frame). Put DocsAgentProcessor(call_id) where the
llm sat. Two consequences follow. Pipecat’s own LLM-level function calling, Flows and context summarization do not apply to a
processor that bypasses the LLM service, because tool calls now happen inside the agent. And a retrieval tool that takes two
seconds is two seconds of silence, so speak a filler first or use a faster tier for the voice agent (the techniques are in
Turn-Taking and Interruptions). Alternatively, keep the Pipecat LLM service
and call the agent as a tool, or serve it behind an OpenAI-compatible endpoint, as in
ElevenLabs Agents, and point OpenAILLMService at it with base_url.
A fully on-prem stack
Nothing in the pipeline has to leave your network: faster-whisper for STT, Ollama for the LLM and Kokoro for TTS, with SmallWebRTC as the transport and Silero VAD plus Smart Turn running on the CPU.
from pipecat.services.kokoro.tts import KokoroTTSService
from pipecat.services.ollama.llm import OLLamaLLMService
from pipecat.services.whisper.stt import Model, WhisperSTTService
stt = WhisperSTTService(
device="cuda", # or "cpu"
compute_type="int8_float16", # "int8" on CPU
settings=WhisperSTTService.Settings(model=Model.SMALL.value),
)
llm = OLLamaLLMService(
base_url="http://ollama:11434/v1",
settings=OLLamaLLMService.Settings(
model="qwen3:8b",
system_instruction="You are DocsAssistant on a call. Reply in short plain sentences.",
),
)
tts = KokoroTTSService(
settings=KokoroTTSService.Settings(voice="af_heart"),
)
# then: Pipeline([transport.input(), stt, user_agg, llm, tts, transport.output(), assistant_agg])
Source: Pipecat: Whisper STT,
Pipecat: Ollama LLM and
Pipecat: Kokoro TTS, checked against the 1.12.0 service classes
(WhisperSTTService wraps faster-whisper, OLLamaLLMService extends the OpenAI service with the Ollama /v1 endpoint, KokoroTTSService
uses the ONNX build and downloads its model and voice files on first use). Install pipecat-ai[whisper,kokoro,silero,webrtc,runner].
The model name, voice and quantization are examples to size against your hardware; the sizing and quantization trade-offs are in
the local LLM pages (Hardware Sizing) and the open speech models in
Speech Models. Expect the stack to be slower than hosted services on CPU: the segmented Whisper
service adds the whole utterance’s transcription time after the user stops, so a latency budget measured on your own hardware is the
only trustworthy number (see Voice Architectures). Keep the Kokoro and Whisper model
files in the image or a volume so a restart does not download them, and check each model’s license before commercial use.
Smart Turn
Silence-based end-of-speech detection cuts people off at a pause or waits too long after a finished sentence. Smart Turn
is an open audio model (the pipecat-ai/smart-turn project) that looks at the speech itself, with its prosody and phrasing, and
predicts whether the speaker is done. In Pipecat 1.12.0 it is the default: user turn start is decided by VAD or a transcription,
and turn stop by TurnAnalyzerUserTurnStopStrategy with LocalSmartTurnAnalyzerV3, an ONNX model bundled with the package and
run on the CPU. VAD still detects speech and the silence that triggers the analysis, so the two work together. You only configure
it to change the defaults.
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.turns.user_stop import TurnAnalyzerUserTurnStopStrategy
from pipecat.turns.user_turn_strategies import UserTurnStrategies, default_user_turn_start_strategies
strategies = UserTurnStrategies(
start=default_user_turn_start_strategies(),
stop=[TurnAnalyzerUserTurnStopStrategy(turn_analyzer=LocalSmartTurnAnalyzerV3(cpu_count=2))],
)
user_agg, assistant_agg = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(
vad_analyzer=SileroVADAnalyzer(),
user_turn_strategies=strategies,
),
)
Source: Pipecat: User turn strategies and
Smart Turn. The cpu_count argument is the inference thread count (default 1), and
pip install "pipecat-ai[local-smart-turn]" pulls the extra dependencies. The concepts (VAD versus semantic turn detection, endpointing,
barge-in) are on Turn-Taking and Interruptions; Smart Turn v3 is trained
mainly for speech, so evaluate it on your callers' languages and accents and fall back to a longer VAD silence timeout where it
underperforms.
Pipecat Flows
A free-form prompt is hard to keep on track in a structured conversation such as identifying a caller, collecting an account number
and then answering. Pipecat Flows models the conversation as a graph of nodes. Each node has a role and task messages, the
functions the LLM may call there, and optional actions; a function handler does the work and returns the next node, so the
transitions are code you control. Flows is part of the core package as pipecat.flows (the older pipecat_flows import path of the
standalone package is replaced), requires a text LLM with function calling, and does not work with speech-to-speech models that
cannot have their context rewritten mid-session.
from pipecat.flows import FlowManager, NodeConfig
def ask_node() -> NodeConfig:
return NodeConfig(
name="ask",
role_message="You are DocsAssistant on a phone call. Use short plain sentences with no markdown.",
task_messages=[{
"role": "developer",
"content": "Ask which product the caller needs help with, then call select_product with the answer.",
}],
functions=[select_product],
)
async def select_product(flow_manager: FlowManager, product: str) -> tuple[str, NodeConfig]:
"""Record the product the caller is asking about.
Args:
product: The product name the caller mentioned.
"""
flow_manager.state["product"] = product
return product, answer_node(product)
def answer_node(product: str) -> NodeConfig:
return NodeConfig(
name="answer",
task_messages=[{
"role": "developer",
"content": f"Answer questions about {product}. When the caller is done, say goodbye.",
}],
functions=[],
post_actions=[{"type": "end_conversation"}],
)
# in run_bot, after building the pipeline and worker:
flow_manager = FlowManager(worker=worker, llm=llm, context_aggregator=context_aggregator, transport=transport)
# in on_client_connected: await flow_manager.initialize(ask_node())
Source: Pipecat Flows: Introduction and the
hello_world flow example (NodeConfig with
role_message, task_messages, functions and post_actions, and a handler that returns a result and the next node). In that
example context_aggregator is the whole LLMContextAggregatorPair, passed to FlowManager rather than unpacked. Use Flows
for the scripted spine of a call (verification, routing, hand-off) and keep open-ended documentation questions in a node that
calls the retrieval tool, since Flows cannot drive a LangChain agent that has already taken over the LLM step.
Deployment
A Pipecat bot is one long-lived asyncio process per call or per few calls, holding a WebRTC or WebSocket connection, model objects and an event loop that must not block. Operationally this means:
-
One image, several transports. The same
bot.pyruns under the runner with-t webrtc,-t dailyor a telephony transport. Build it as a normal Python image (Dockerfile Fundamentals) and run it behind a reverse proxy that terminates TLS and passes WebSocket upgrades. -
WebRTC needs UDP. SmallWebRTC peers need a UDP port range or a TURN server reachable by callers; a container network that hides the host address breaks ICE candidates unless you set the runner’s proxy host option (
--proxy) or publish a fixed range. -
Load models once. Whisper, Kokoro and Smart Turn load at start, so construct them per process (not per call) where the services allow, and do not run CPU inference on the event loop thread for many concurrent calls. Scale by processes or pods and cap concurrent sessions per instance from a measured per-call CPU and GPU cost, not a guess.
-
Secrets and observability. Provider keys come from the environment; enable the metrics in
PipelineParamsand export traces so time to first audio is measurable per stage (Voice Agents defers the rest to Production Voice Agents).
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends espeak-ng libsndfile1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # includes pipecat-ai[whisper,kokoro,silero,local-smart-turn,webrtc,runner]
COPY bot.py ./
COPY docsassistant ./docsassistant
ENV HF_HOME=/models
EXPOSE 7860
CMD ["python", "bot.py", "-t", "webrtc", "--host", "0.0.0.0", "--port", "7860"]
Source: Docker: Dockerfile reference and the Pipecat runner options
(--host, --port, -t, --proxy) in Pipecat: Quickstart. The system packages
are for Kokoro’s phonemizer and audio libraries; adjust to the extras you install, mount /models as a volume so downloads
survive restarts, and publish the UDP range for WebRTC when running outside host networking. Compose ties the bot, Ollama and a
TURN server together on a laptop (Compose for Local Development); first steps
and the container model are in Getting Started with Docker. Managed hosting also exists in Pipecat Cloud, which this page does not cover.
Choosing Pipecat
Choose Pipecat when you want to pick and swap every model, run some or all of the pipeline on your own hardware, keep audio in-house, or build a custom pipeline with processors of your own, and you are comfortable operating a Python real-time service. Pair it with Smart Turn for natural turn-taking and with Flows when the call has a structured spine. Choose a hosted platform (ElevenLabs Agents) or a realtime API (Realtime APIs) when speed to launch or the lowest latency outweighs control, and LiveKit Agents (LiveKit Agents) when a LiveKit room is already your media layer. If you want no framework at all, see Building a Voice Pipeline by Hand.