Build an end-to-end app that answers real calls with a Pipecat pipeline over QUIC/MoQT — the shipping audio-bridge spine today, plus the shape of a first-class transport.
You have a Pipecat bot — a pipeline of frame processors wired to ASR, an LLM,
and TTS — and you want it to answer real phone calls and browser sessions over
TeleQuick’s transport, without operating a media server or negotiating
ICE/DTLS/SRTP. This recipe assembles that end to end: a small host process that
takes a live call, subscribes the caller’s audio, runs it through a Pipecat
pipeline, and streams the agent’s replies back — all over QUIC/MoQT on the
single :443 plane.
No packaged Pipecat adapter ships. There is no pipecat transport,
plugin, or vendor-bridge target in TeleQuick today — the built-in
VENDOR_BRIDGE routes only to
LiveKit, Vapi, and Twilio backends. What is real and shipping is the
audio bridge this recipe is built around: caller audio in, agent audio
out, over raw MoQT. You own the thin glue that hands those frames to Pipecat.
The QuicMoqtTransport wrapper near the end is an illustrative design
sketch of a future first-class adapter — do not pip install it. For the
conceptual framing, see
Pipecat + TeleQuick Transport.
Pipecat and TeleQuick sit at different layers, which is why they compose
cleanly: Pipecat owns the conversation (the frame pipeline, turn-taking, and
your model choices), and TeleQuick owns the transport and telephony (the
SIP gateway/B2BUA that terminates the carrier trunk, and the media plane that
carries the audio over QUIC). The seam between them is a pair of tracks keyed by
the call id — an uplink (caller → bot) and a downlink (bot → caller):
Skip agents.attach (that runs our managed runtime) and open the bridge
yourself. Ask for pcm16 at 16 kHz — the shape most frame pipelines expect;
the bridge transcodes the PSTN (G.711) leg and the runtime’s 8 kHz bus for
you in both directions.
TypeScript
Python
import { Voice } from "@telequick/sdk/voice";const v = new Voice({ baseUrl: "https://engine.telequick.dev", apiKey: process.env.TELEQUICK_CREDENTIALS!, orgId: "org_abc",});// Originate (or attach to an inbound sid you already hold).const call = await v.calls.originate({ to: "+15551234567", from: "+15558675309", trunkId: "trunk_main",});// pcm16 @ 16 kHz — raw audio in both directions.const bridge = await v.audioBridge.attach(call.sid, { codec: "pcm16", sampleRate: 16000, onUplink: (pcm, tsUs) => pipeline.pushAudio(pcm), // caller -> bot});
import osfrom telequick.voice import Voicev = Voice( base_url="https://engine.telequick.dev", api_key=os.environ["TELEQUICK_CREDENTIALS"], org_id="org_abc",)# Originate (or attach to an inbound sid you already hold).call = v.calls.originate( to="+15551234567", from_="+15558675309", trunk_id="trunk_main",)# pcm16 @ 16 kHz — raw audio in both directions.bridge = v.audio_bridge.attach( call.sid, codec="pcm16", sample_rate=16000, on_uplink=lambda pcm, ts_us: pipeline.push_audio(pcm), # caller -> bot)
2
Wrap the two hooks as a Pipecat transport
Pipecat’s Transport seam needs exactly two things: something that produces
input audio frames and something that consumes output audio frames. The
bridge already gives you both — onUplink is the producer, publishDownlink
is the consumer. Wrap them so the rest of the pipeline runs unchanged.
Illustrative — you own this class today. The transport below is your
glue code around the real audio bridge, not an installable package. Class
and frame names track Pipecat’s public shape; pin your Pipecat version and
match its exact transport base classes when you write it for real.
# ILLUSTRATIVE glue you write today — wraps the REAL audio bridge for Pipecat.from pipecat.frames.frames import InputAudioRawFrame, OutputAudioRawFramefrom pipecat.processors.frame_processor import FrameProcessorclass MoqtBridgeInput(FrameProcessor): """Emits caller uplink audio as Pipecat input frames.""" def __init__(self, sample_rate: int = 16000): super().__init__() self.sample_rate = sample_rate def on_uplink(self, pcm: bytes, ts_us: int): frame = InputAudioRawFrame(audio=pcm, sample_rate=self.sample_rate, num_channels=1) # hand the frame downstream into the pipeline self.push_frame(frame)class MoqtBridgeOutput(FrameProcessor): """Publishes agent output frames back as downlink audio.""" def __init__(self, bridge): super().__init__() self.bridge = bridge async def process_frame(self, frame, direction): await super().process_frame(frame, direction) if isinstance(frame, OutputAudioRawFrame): self.bridge.publish_downlink(frame.audio) # bot -> caller (SDK, real) await self.push_frame(frame, direction)
3
Assemble the pipeline
Drop the two wrappers on the ends of an ordinary Pipecat pipeline. The STT,
LLM, and TTS processors are stock Pipecat — pick whatever providers you
already use.
# ILLUSTRATIVE — a standard Pipecat pipeline between the two bridge wrappers.from pipecat.pipeline.pipeline import Pipelinefrom pipecat.pipeline.runner import PipelineRunnerfrom pipecat.pipeline.task import PipelineTaskbridge_in = MoqtBridgeInput(sample_rate=16000)bridge_out = MoqtBridgeOutput(bridge)# route the SDK's uplink callback into the input processor# (v.audio_bridge.attach(..., on_uplink=bridge_in.on_uplink))pipeline = Pipeline([ bridge_in, # caller audio in stt, # your ASR frame processor llm, # your LLM context tts, # your TTS frame processor bridge_out, # agent audio back to the caller])
4
Run the call, then tear down cleanly
Run the pipeline for the life of the call. When the conversation ends, close
the bridge before hanging up so both tracks tear down cleanly.
TypeScript
Python
// … your pipeline runs; when it decides the call is over:await bridge.close(); // release both tracks firstawait call.hangup();
runner = PipelineRunner()try: await runner.run(PipelineTask(pipeline))finally: bridge.close() # release both tracks first call.hangup()
Keep the bridge reference alive for the whole call. If it is
garbage-collected mid-call, both tracks drop and the audio goes silent.
A first-class integration would collapse the two wrapper classes above into a
single Pipecat Transport that binds directly to our media-over-QUIC primitives
— you’d construct it with the call id and drop transport.input() /
transport.output() onto the pipeline, with no onUplink/publishDownlink
glue to write.
Design sketch — not shipped.QuicMoqtTransport does not exist as a
package. It shows the intended shape of a future adapter only.
# ILLUSTRATIVE — this transport is not shipped. Design sketch only.from pipecat.pipeline.pipeline import Pipelinefrom pipecat.pipeline.runner import PipelineRunnerfrom pipecat.pipeline.task import PipelineTask# A transport that would carry Pipecat frames over the QUIC/MoQT plane:# input(): subscribe voice/<call_sid>/uplink -> InputAudioRawFrame# output(): publish voice/<call_sid>/downlink <- OutputAudioRawFrametransport = QuicMoqtTransport( url="quic://relay.telequick.dev", call_sid=call.sid, sample_rate=16000,)pipeline = Pipeline([ transport.input(), # caller audio in stt, llm, tts, # your pipeline, unchanged transport.output(), # agent audio back to the caller])await PipelineRunner().run(PipelineTask(pipeline))
The only real work such an adapter adds is the seam our own runtime already
handles: resample between Pipecat’s frame rate and the runtime’s 8 kHz PCM16
audio bus, keep the room-scoped MoQT namespaces valid, and carry Opus rather
than PCM on browser legs. None of it is exotic — it just isn’t packaged yet, so
today you own the wrapper.
If you don’t want to write even the wrapper, two seams already let a Pipecat bot
take live calls:
WebSocket audio on-ramp
Vendor bridge (if Pipecat fronts a backend)
Pipecat already speaks WebSocket audio, and TeleQuick accepts a bespoke
WebSocket audio sender as a media leg. Point Pipecat’s WebSocket server
transport at that on-ramp and route the trunk’s inbound rule to your
endpoint — the SIP gateway terminates the carrier while Pipecat drives the
conversation, with no TeleQuick code in the bot. Same path as
migrating a custom WebSocket audio sender.
If your Pipecat bot fronts a LiveKit, Vapi, or Twilio media backend, the
config-only VENDOR_BRIDGE node
routes call media to that backend via a sidecar. Pipecat itself is not a
bridge target — this only helps when one of those vendors sits behind it.
If you want Pipecat only for the pipeline — cascaded ASR → LLM → TTS,
turn-taking, tools — TeleQuick’s runtime already ships that as a configured
DAG with transport, telephony, and barge-in built in. Bringing only your
models via provider credentials is often less work than porting a Pipecat
pipeline onto an adapter that doesn’t exist yet.