Pipecat is engineered for real-time, streaming AI applications. Its architecture is built on three fundamental concepts that work in concert to create responsive and modular systems.

  1. Frames: The Data Packets
  2. Processors: The Workers
  3. Pipelines: The Assembly Line

Let’s break down each component, referencing the implementation details we found in the code.


1. Frames (src/pipecat/frames/frames.py)

Frames are the fundamental unit of data or signaling in Pipecat. Think of them as standardized packages carrying specific payloads and information. Every piece of data—be it audio, text, a system command, or an error—is wrapped in a Frame subclass.

The base Frame is a simple dataclass with essential metadata for tracking and debugging:

  • id: A unique identifier for the frame.
  • pts: Presentation Timestamp, indicating when the frame’s data should be “presented” or used.
  • metadata: A dictionary for carrying custom information.

From this base, frames are categorized into three critical types:

A. DataFrame

These frames carry the primary application data that flows sequentially through the pipeline. They are designed to be processed in the order they are received.

  • Purpose: To transport the content your application works with.
  • Examples:
    • AudioRawFrame: Carries a bytes chunk of audio data, along with sample_rate and num_channels.
    • TextFrame: Carries a str of text, such as a transcription or an LLM response.
    • ImageRawFrame: Carries raw image bytes and format information.
    • LLMMessagesFrame: Carries a list of messages to be sent to an LLM, representing the conversational context.

B. SystemFrame

These are high-priority frames for out-of-band communication. They are not queued like DataFrames and are processed immediately to allow for real-time control and responsiveness.

  • Purpose: To signal urgent events or system-level state changes that must bypass the normal data flow.
  • Examples:
    • StartFrame: The very first frame sent to initialize all processors in the pipeline with configuration details.
    • CancelFrame / EndTaskFrame: Instructs the pipeline to terminate immediately or gracefully.
    • ErrorFrame: Propagated upstream (backwards) to signal a problem.
    • UserStartedSpeakingFrame / StopInterruptionFrame: Sent by the Voice Activity Detection (VAD) processor to signal that the user is interrupting, allowing other processors (like TTS) to react instantly.

C. ControlFrame

These frames represent commands that, unlike SystemFrames, need to be processed in-order with the data. They provide a way to synchronize actions with the data stream.

  • Purpose: To manage the flow and state of the pipeline in a way that respects data ordering.
  • Examples:
    • EndFrame: Signals the graceful end of a stream. Since it’s a ControlFrame, it will only be processed after all preceding DataFrames have been handled, ensuring a clean shutdown.
    • LLMFullResponseStartFrame / LLMFullResponseEndFrame: These frames wrap a stream of TextFrames from an LLM, allowing downstream processors to know precisely when a complete LLM response begins and ends.
    • TTSStartedFrame / TTSStoppedFrame: Similar to the above, they bracket a stream of TTSAudioRawFrames.

2. Processors (src/pipecat/processors/frame_processor.py)

Processors are the “workers” on the assembly line. Each processor is a class designed to perform a specific action on incoming frames.

The base FrameProcessor class provides the core machinery for all processors:

  • Asynchronous Core: Processors are built on asyncio. They have their own task manager (self.create_task()) to run background operations without blocking the main processing loop.

  • Core Logic (process_frame): The heart of a processor is the async def process_frame(self, frame: Frame, direction: FrameDirection) method. Subclasses override this method to implement their specific logic. A processor will typically:

    1. Check the type of the incoming frame.
    2. If it’s a frame type it cares about, it performs its action (e.g., a TTS processor acts on a TextFrame).
    3. If it doesn’t handle that frame type, it simply passes it along.
  • Frame Flow (push_frame): After processing a frame (or choosing to ignore it), a processor calls async def push_frame(self, frame: Frame, direction: FrameDirection) to send it to the next link in the chain. This is how frames move from one processor to the next. The direction can be DOWNSTREAM (the normal flow) or UPSTREAM (typically for errors and some control signals).

  • Lifecycle Management (setup and cleanup): Processors have async def setup() and async def cleanup() methods. These are called by the pipeline once at the beginning and end of the run, respectively, to initialize and release resources like network clients or hardware devices.

  • State and Control: Processors can be paused and resumed via FrameProcessorPauseFrame and FrameProcessorResumeFrame. This is handled internally by an asyncio.Event that blocks the input queue, demonstrating the fine-grained control possible within the system.


3. Pipelines (src/pipecat/pipeline/pipeline.py)

A Pipeline is what connects a series of FrameProcessors together to form a complete application flow.

  • Structure: The Pipeline class is elegantly simple. Its constructor takes a list of FrameProcessor instances: __init__(self, processors: List[FrameProcessor]).

  • Linking: Internally, the pipeline calls a _link_processors() method. This method iterates through the list of processors and links them together like a doubly linked list, setting the _next and _prev attributes on each one. This creates the “conveyor belt” path for frames to travel.

  • Source and Sink: To manage frames entering and exiting the chain, the Pipeline automatically prepends a PipelineSource and appends a PipelineSink.

    • PipelineSource: When a frame is pushed into the pipeline, the source processor receives it and pushes it downstream to the first “real” processor.
    • PipelineSink: When a frame reaches the end of the chain, the sink pushes it out of the pipeline. This is also the entry point for UPSTREAM frames to travel backward through the pipeline.
  • Hierarchical Nature: A Pipeline is itself a FrameProcessor. This powerful design choice allows you to embed pipelines within other pipelines. The ParallelPipeline class is a perfect example of this, enabling you to create branching flows where, for instance, audio and video frames are processed simultaneously in separate sub-pipelines before being rejoined later.

How It All Works Together

  1. A Transport (e.g., a WebSocket connection or microphone input) creates an initial Frame, such as an InputAudioRawFrame.
  2. The transport pushes this frame into a Pipeline.
  3. The PipelineSource receives the frame and pushes it to the first processor (e.g., a VAD processor).
  4. The frame travels downstream through the linked list of Processors. A speech-to-text processor consumes InputAudioRawFrames and produces TranscriptionFrames. An LLM processor consumes TranscriptionFrames and produces a stream of TextFrames. A TTS processor consumes TextFrames and produces OutputAudioRawFrames.
  5. All the while, SystemFrames like UserStartedSpeakingFrame can be injected to immediately interrupt the flow (e.g., stop the TTS output).
  6. Finally, a resulting OutputAudioRawFrame reaches the PipelineSink, which pushes it out of the pipeline and back to the Transport, which plays the audio.

This architecture makes Pipecat exceptionally modular, extensible, and well-suited for the demands of real-time AI by breaking down complex tasks into a series of independent, asynchronous, and reusable components.