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.

flowchart LR IN["transport.input()"] --> STT["STT service"] STT --> UA["user aggregator (VAD, turn detection)"] UA --> LLM["LLM service or DocsAssistant agent processor"] LLM --> TTS["TTS service"] TTS --> OUT["transport.output()"] OUT --> AA["assistant aggregator"] UA -. "interruption frame" .-> TTS UA -. "interruption frame" .-> OUT

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.

bot.py: minimal cloud-model voice bot
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_secs directly add to latency.

  • An LLMContext is provider-neutral. Switching the LLM service does not rewrite the pipeline, which also allows an LLMSwitcher or 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 (pipecat.transports.smallwebrtc)

Peer-to-peer WebRTC from a browser to your Python process, built on aiortc. No third-party media server, which suits local development and on-premises deployments; a TURN server is needed for callers behind strict NATs (Voice in the Browser). The runner exposes the signaling endpoint for you.

WebSocket (pipecat.transports.websocket.fastapi)

A FastAPIWebsocketTransport over a FastAPI WebSocket route. Audio is carried as frames serialized by a pluggable serializer. Simple and firewall-friendly, with head-of-line blocking and no jitter handling, so it is mostly used for telephony bridges and server-to-server audio.

Daily (pipecat.transports.daily)

DailyTransport joins a room on Daily’s WebRTC infrastructure as a participant, giving multi-party rooms, recording and PSTN dial-in/out at the cost of a Daily account. Other media-server transports (LiveKit, Vonage, MoQ) exist as well; the LiveKit route is covered on LiveKit Agents.

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.

twilio_ws.py: FastAPI WebSocket endpoint using the Twilio serializer
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.

docs_agent_processor.py: a create_agent agent as a frame processor
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.

flowchart LR B["Browser microphone"] <-- "WebRTC (SmallWebRTC)" --> V subgraph PP["Pipecat process (one host)"] V["Silero VAD + Smart Turn"] --> W["faster-whisper (WhisperSTTService)"] W --> L["OLLamaLLMService"] L --> K["Kokoro (KokoroTTSService)"] end L -- "HTTP /v1" --> O["Ollama server (GPU or CPU)"]
onprem_bot.py: STT, LLM and TTS all local
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.

turns.py: explicit user turn strategies with Smart Turn
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.

flows.py: a two-node flow for the DocsAssistant call
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.py runs under the runner with -t webrtc, -t daily or 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 PipelineParams and export traces so time to first audio is measurable per stage (Voice Agents defers the rest to Production Voice Agents).

Dockerfile: the on-prem voice bot
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.