Added
-
TTS sentence aggregation follows
Settings.language, defaulting to English when unspecified. Language updates apply with TTS settings.
(PR #5737) -
Added
TTSService.pronunciation_transform_ipa(), which builds a text transform from a word-to-IPA mapping so a voice says names and terms it would otherwise guess from spelling. Each matched word is replaced with the service'sformat_pronunciation()output, so one mapping works with any service that supports pronunciation hints; a word the service cannot use is reported once when the transform is built and spoken as written. Cartesia writes IPA as inline phoneme blocks, ElevenLabs as SSML<phoneme>tags (read byeleven_flash_v2andeleven_turbo_v2, and on the WebSocket service only withenable_ssml_parsing=True; on any other setup the transform is skipped with a warning and words are spoken as written), Eleven v3 and Inworld as IPA between slashes, and Deepgram Aura-2 as an inline pronunciation object. Deepgram Flux has no pronunciation markup, so its words are spoken as written.pipecat.utils.text.phonemesholds the IPA parsing and normalization the formatters share. Register the transform last intext_transforms, so no later transform rewrites the markup it inserts.pronounce = CartesiaTTSService.pronunciation_transform_ipa({"Metformin": "mɛtˈfɔɹmɪn"}) tts = CartesiaTTSService( text_transforms=[ ("*", strip_markdown), ("*", pronounce), # last, so nothing rewrites its markup ], )
(PR #5798)
-
Added client-mode reconnect to
MOQTransport: when the relay session drops, the transport redials with backoff for as long asMOQParams.connection_timeoutallows the client to be missing, keeps its broadcast across dials, and reports the client gone only when that time is up. A relay that vanishes without closing the session is detected by watching the connection's traffic counters, rather than waiting out the QUIC idle timeout. Each side now sends asession-endingmarker on its transcript before leaving; a peer whose tracks end without it is treated as an outage rather than a hangup.on_disconnectedandon_connectednow fire for each relay session, so a redial shows up as a disconnect followed by a connect; end the call fromon_client_disconnected, which fires only once the client is gone.
(PR #5800) -
Added
MOQRunnerArguments.relay_urlfor dialing a relay by its full URL, query string included.hostandportare optional when it is set, andcreate_transportpasses it through toMOQParams.relay_url. Whenhostorportis given as well,relay_urlwins and a warning is logged.
(PR #5800) -
Added
interruptibleto every frame: True by default and False by default forUninterruptibleFramesubclasses, and what the frame queue, the processor's interruption handling and the speculation gate decide by. Set it on a frame before pushing it to keep that one frame through an interruption, or to let one frame of a protected type be dropped, without a frame class of your own.
(PR #5835) -
JudgeVerdictnow has aconfidence, from 0 to 1, saying how sure the judge is. With Jev the number is calibrated. With an LLM it is the LLM's own guess.
(PR #5857) -
The eval judge can now use a classifier. Put a
factory:in the scenario'sjudge.eval:block that returns aBaseClassifier, or an LLM service as before. Our release evals judge with Jev this way, throughevals/judges.py. Jev answers each check in a few hundred milliseconds and says how sure it is. It needs thejevextra andTYPESAFE_API_KEY.
(PR #5857) -
Added
allow_continueto a scenario'sjudge.eval:block and toEvalJudge. Set it tofalsewhen every reply you judge is a final answer. The judge then answers yes or no and nevercontinue, so it never waits for more text that isn't coming.
(PR #5857) -
Added
LLMClassifier, which answers classifier questions with any Pipecat LLM service that supportsrun_inference(). All the questions about one state go to the LLM in one call, and the LLM replies with one JSON object holding an answer per question. It waitstimeoutseconds for the LLM, 10 by default, and raisesClassifierErrorif the LLM does not answer in time or the call fails.classifier = LLMClassifier(llm=OpenAILLMService(model="gpt-4o-mini")) results = await classifier.yes_no( "Hi, you've reached Dana. Leave a message.", {"voicemail": YesNoQuestion(instructions="is this a voicemail greeting?")}, ) results["voicemail"].is_yes # True results["voicemail"].probability # 0.97
(PR #5863)
-
Classifiers report metrics through an
on_metricsevent: after every call, the time it took asProcessingMetricsDataand, forJevClassifier, the tokens it used asLLMUsageMetricsData. A classifier cannot push frames, so its owner puts the data in aMetricsFrame.
(PR #5863) -
Added Pipecat Classifiers. A classifier is a small object that answers typed questions about some state. A question is a
YesNoQuestion, aChoiceQuestionamong options, or aScoreQuestionon a scale. Questions are asked by name, several about one state at once.ask()takes any mix of kinds, andyes_no(),choice()andscore()take questions of one kind and return typed results: aYesNoResultwith the probability of yes andis_yes, aChoiceResultwith thechoiceand a probability per option, and aScoreResultwith thescoreon the scale and a probability per level.JevClassifieranswers them through Jev, TypeSafe's classification model, in one request. A choice question can have at most 255 options, Jev's limit (JEV_MAX_CHOICE_OPTIONS). Several classifiers can share oneJevClient, which handles the HTTP/2 connection, auth, retries and token accounting. Install withuv add "pipecat-ai[jev]".examples/features/features-classifiers.pyasks one question of each kind.classifier = JevClassifier(api_key=os.getenv("TYPESAFE_API_KEY")) results = await classifier.choice( "I'd like to book a table for, um", { "turn": ChoiceQuestion( instructions="is the user's turn over?", options={ "complete": "the user finished", "short": "a brief pause", "long": "asked for time", }, ) }, ) results["turn"].choice # "short"
(PR #5863)
-
Added a read-only
settingsproperty toAIService, so code outside a service can read its current settings, such as the model. Settings are still changed through aServiceUpdateSettingsFrame.
(PR #5863) -
Added an optional
response_schemaargument toLLMService.run_inference(), a JSON schema the reply must follow. OpenAI, Anthropic and Google have the provider enforce it, so the reply is JSON text in that shape. A service or model that cannot enforce one, such as Bedrock, DeepSeek or an OpenAI model before gpt-4o-mini, ignores it with a warning, andsupports_response_schemasays whether a service can at all.reply = await llm.run_inference(context, response_schema=schema)
(PR #5876)
-
Added
LLMUserAggregatorParams.empty_user_turn, anEmptyUserTurnConfigfor user turns that end with no transcript, such as a cough, background noise, or speech the STT could not recognize. It is on by default, even whenempty_user_turnisn't passed, and treats two cases differently:- Interrupted: the turn cut the bot off. The bot would otherwise stay silent mid-response, so the aggregator appends a developer message (
interrupted_prompt) and runs the LLM once, and the bot asks the user to repeat or picks up where it left off. On by default. - Idle: the bot had finished and was waiting for the user. The conversation isn't stuck, and answering what may be noise would be intrusive, so these turns get no answer unless
idle_promptis set.
LLMUserAggregatorParams( empty_user_turn=EmptyUserTurnConfig( idle_prompt=( "The user may have said something, but it was not " "recognized. Briefly ask them to repeat it." ), ), )
Setting
interrupted_prompt=Noneturns off the interrupted case, andempty_user_turn=Noneturns off both.max_consecutive_recoveries(default 1) limits how many empty turns in a row get an answer.
(PR #5907) - Interrupted: the turn cut the bot off. The bot would otherwise stay silent mid-response, so the aggregator appends a developer message (
-
UIWorkercan now use a classifier for small decisions about the screen, with no LLM turn: which element the user means (which_element), whether something is true on the screen (check_screen), which elements match a description (select_elements), whether a UI event deserves a reply (should_respond), andacton an element named in words. Pass aclassifier, for example aJevClassifier. If you don't, the worker's own LLM answers through anLLMClassifier, which works but is slower.worker = MyUIWorker("ui", llm=OpenAILLMService(api_key=...), classifier=JevClassifier(api_key=...)) ref = await worker.which_element("the checkout button")
(PR #5909)
-
A voice LLM can now ask a
UIWorkerabout the screen without ever seeing it. The worker has a built-inscreenjob with anaction:find(which element a description means),check(whether something is true on the screen),select(which elements match a description),list(what is on the screen),selection(the text the user has selected) andclick,scroll_to,highlight,select_textorfill(do that to the element a description means). Every answer is short data, never the page.screen_tools("ui")gives the voice LLM the tool that sends it.context = LLMContext(tools=screen_tools("ui"))
(PR #5909)
-
UIWorkernow has asnapshotproperty with the latest page snapshot and aselectionproperty with the text the user has selected, so a subclass can read the screen with plain code.
(PR #5909) -
Added
RTVIFunctionCallReportLevel.ARGUMENTS, betweenNAMEandFULL. It reports the function name and the arguments, and no result.
(PR #5918)
Changed
-
InworldRealtimeLLMServicenow sendsproviderData.auto_tool_response=false, leaving Pipecat responsible for requesting continuation after submitting a tool result.InworldRealtimeLLMServicenow generates a unique session key for every connection, including connections opened within the same millisecond.
(PR #5116)
-
Sentence aggregation uses self-contained sentencex rules without NLTK data downloads or tokenizer warm-up. Language-specific sentence boundaries may differ from Punkt.
(PR #5737) -
MOQTransportnumbers every transcript record withseqandepochand drops replayed records on subscribe, so a reconnect on either side no longer redelivers the whole RTVI log and re-firesclient-ready. Both fields are stripped before the message reaches the pipeline; records without them pass through unchanged, so older peers keep working.
(PR #5800) -
MOQTransportfailures now reach the pipeline as anErrorFramewith an error category, in addition to theon_errorevent. A relay that refuses the token, at the dial or by closing the session as unauthorized, is not retried and marks the transport unusable, as does a relay that cannot be reached withinconnection_timeout.
(PR #5800) -
The
moqextra requiresmoq-rs0.4.6 or later.moq-rsis the client libraryMOQTransportdials a relay with;moq-relayis the separate server. The transport is tested againstmoq-relay0.14. A relay from an incompatible release line can accept the connection and still drop the transcript track without an error.
(PR #5800) -
MOQParams.connection_timeoutis now the one limit on how long the client may be missing, and defaults to 60 s (was 30 s). It bounds the wait for the client to join and, in client mode, an outage: the time from the relay session dropping until the client's data flows again, across every redial. Set it longer than the time your load balancer takes to fail a dead relay out.
(PR #5800) -
XAISTTServicenow sends itsmodelsetting to xAI, and defaults togrok-voice-transcribe-2.0. Previously no model was sent, so xAI used its server default,grok-voice-transcribe-1.0. To keep the previous model, passsettings=XAISTTService.Settings(model="grok-voice-transcribe-1.0").
(PR #5847) -
The
local-smart-turnandmoondreamextras now requiretransformers>=5.10.0. Themoondreamextra'saccelerate,einops,pyvipsandtimmpins are relaxed to a lower bound with a major-version cap.
(PR #5850) -
EvalJudgenow decides every verdict with a classifier. By default that is anLLMClassifierover the LLM thejudge.eval:block names, so existing scenarios work as before. A classifier gives no reasons, so the judge asks an LLM, the explainer, for the reason behind everynoand every verdict belowexplain_below. The explainer is the judging LLM unless anexplainer:block names another one, andexplainer: falseturns reasons off. The classifier's verdict always stands.
(PR #5857) -
pipecat eval suitenow keepsconcurrencyruns going at all times, taking the next run in manifest order. Before, each entry ran its scenarios one at a time on one slot, so a manifest with fewer entries than slots left slots idle, and one entry with ten scenarios ran them one by one. An entry whose provider rate-limits sets its ownconcurrency:to cap its runs in flight, as the turn-completion manifest does.
(PR #5857) -
The
mcpextra now requiresmcp2 (mcp[cli]>=2.1.1,<3).MCPClientworks the same; it no longer runs on the 1.x SDK.
(PR #5857) -
An LLM judge now classifies instead of answering a prose prompt: it gets the conversation as structured state and picks one of the outcomes. A borderline reply can get a different verdict than before. A simulation is judged one bot turn per call instead of the whole run in one call, so it costs more calls.
(PR #5857) -
VoicemailDetectornow takes aclassifierand is a single processor instead of a parallel pipeline with its own LLM. It asks the classifier after each transcription and acts once the caller has been quiet fordecision_timeout(default 1 s), when the latest answer decides. No verdict acts on a fragment: "hi, this is Sam" is what a person says and how a greeting starts, and only the silence that follows tells them apart. Then the held-back speech is released or dropped as before. Theon_voicemail_detectedandon_conversation_detectedhandlers receive the detector itself.# Before detector = VoicemailDetector(llm=OpenAILLMService(api_key=...)) # After detector = VoicemailDetector(classifier=JevClassifier(api_key=...))
(PR #5869)
-
Observers created with
observe_every_push=Falseare told about a frame once, on its first push, instead of on every push by every processor that passes it along. The built-in observers that handle a frame once do so, andFramePushed.first_pushtells the first push from the ones that follow for the ones that observe every push.
(PR #5908) -
A
UIWorkernow answers arespondjob with the reply its LLM writes, so the smallest UI worker is aUIWorkerwith an LLM and a system prompt. A@toolthat callsrespond_to_jobstill answers instead when it needs to, for example to speak the answer through TTS.
(PR #5909) -
UI_SNAPSHOT_EVENT_NAMEandUI_CANCEL_JOB_GROUP_EVENT_NAMEare now public inpipecat.bus.ui. They are the bus event names of the client's screen snapshot and of the client asking to cancel a job group.
(PR #5909)
Deprecated
-
Deprecated
UninterruptibleFrame. A frame class that should be uninterruptible by default declaresinterruptible: bool = field(default=False, init=False)instead. The marker still sets the flag until it is removed in 2.0.0.
(PR #5835) -
Passing an LLM service to
EvalJudge, as the first argument or asservice=, is deprecated and will be removed in 2.0.0. PassEvalJudge(LLMClassifier(llm=service), explainer=service)instead, which is what the old form did.
(PR #5857) -
Deprecated the startup warming timings reported by
StartupTimingObserver, all removed in 2.0.0: theStartupTimingReport.warmupfield,StartupWarmupTiming,StartupWarmup, and theon_startup_warmup()hook onBaseObserver,WorkerObserverandStartupTimingObserver. Pipecat warms no deferred imports while the pipeline sets up, sowarmupis alwaysNoneand the hook is never called. The rest of the report — the phase totals and the per-processor timings — is unchanged.
(PR #5859) -
VoicemailDetector'sllmandcustom_system_promptparameters are deprecated. Pass aclassifierinstead. Until removal, thellmis wrapped in anLLMClassifier, with the custom prompt in front of the classifier's instructions.
(PR #5869) -
The
max_framesparameters ofTurnTrackingObserverandUserBotLatencyObserverare now deprecated, removed in 2.0.0. Observers no longer keep a window of the frames they have seen.
(PR #5908) -
tts_speakonUIWorker.respond_to_jobis deprecated and will be removed in 2.0.0. A UI worker should not speak; respond with the answer and let the voice LLM say it.
(PR #5909) -
BaseUIWorkeris deprecated and will be removed in 2.0.0. UseUIWorkerinstead.UIWorkernow reports its job groups to the client on its own, so instead of a separateBaseUIWorkerdispatcher, dispatch job groups from a@jobhandler in yourUIWorker.
(PR #5909) -
ReplyToolMixinis deprecated and will be removed in 2.0.0. Usescreen_toolsinstead: the voice LLM asks the UI worker through thescreenjob and says the answer itself.
(PR #5909)
Removed
- Removed
NotifierGate,ClassifierGate,ConversationGateandClassificationProcessorfrompipecat.extensions.voicemail.voicemail_detector, and theCLASSIFIER_RESPONSE_INSTRUCTIONandDEFAULT_SYSTEM_PROMPTattributes ofVoicemailDetector. They were parts of the old parallel-pipeline detector and its prompt.
(PR #5869)
Fixed
-
Fixed an
LLMServiceregression where re-advertising a tool with the same name but a different handler (for example, a per-node handler in Pipecat Flows) silently kept the previous handler bound. Auto-registered handlers are now rebound when the advertised handler changes, while explicitregister_functionregistrations are still left untouched.
(PR #4823) -
Fixed
InworldRealtimeLLMServiceresending a server-VAD user transcript after a fast tool result context update, which duplicated user turns and responses.
(PR #5116) -
InworldRealtimeLLMServicenow serializes Inworld extensions underproviderDatainstead ofprovider_data, allowing the server to apply them.
(PR #5116) -
Applied NVIDIA STT settings changes to the running stream.
NvidiaSTTServicerebuilt its recognition config on a settings update but never reconnected, so the open gRPC stream kept transcribing with the previous settings whileself._settingsreported the new ones.
(PR #5632) -
Fixed Flows node transitions where an interruption could leave the LLM running with the previous node's context and tools. The
LLMMessagesAppendFrameorLLMMessagesUpdateFrameand theLLMSetToolsFramea transition queues are now uninterruptible, so they are still delivered.
(PR #5837) -
TelnyxFrameSerializernow sendsOutputTransportMessageFrameandOutputTransportMessageUrgentFrameto the client as JSON, matching the other telephony serializers. Previously these messages (including RTVI messages) were silently dropped.
(PR #5841) -
Fixed the eval harness treating the bot's earlier speech as its reply when a scenario interrupts the bot. Speech from before the interruption is now dropped.
(PR #5846) -
Fixed
MoondreamServicewith transformers 5: loading the model on a GPU or Apple Silicon failed withAttributeError: 'HfMoondream' object has no attribute 'all_tied_weights_keys', and a model that did load produced garbage descriptions.
(PR #5856) -
Fixed a bridged
PipelineWorkerreporting every frame from its LLM twice over RTVI, once in its own pipeline and once when the frame crossed the bridge, which doubled every word of a bridged worker's reply in the client's LLM text and in the evals.enable_rtvinow defaults to off for a bridged pipeline worker, which has no client of its own;LLMWorkeralready did this. Passenable_rtvi=Trueto keep it.
(PR #5857) -
Fixed TTS word tracking falling out of step on markdown-heavy replies. Most word-timestamp events were dropped with "Dropping word ... not recognised by any slot" warnings, and the rest of the reply arrived in large chunks, so word highlighting stalled and then jumped ahead. Two token shapes triggered it: the period Cartesia adds to the last token of a line (
images:.,---.), and a symbol a provider reports differently from the text (ElevenLabs reports→as-) followed by more symbols such as###or**.
(PR #5866) -
Fixed
WorkerRunnerskipping a worker that another worker added while the runner was still starting, for example a child worker added by a processor during its setup.
(PR #5872) -
Fixed
pcm_to_wav()dropping complete samples when given a typedmemoryview.
(PR #5879) -
Fixed
is_silence()misclassifying full-scale negative int16 PCM samples as silence.
(PR #5881) -
Fixed
MOQTransportending the call when a relay refused a subscription to the peer's broadcast withdropped, which a surviving relay in a mesh does while it still holds a route through a relay that went away. The refusal is now treated as the peer's tracks ending, so the transport retries and redials as it does for any other relay loss.
(PR #5892) -
Fixed
RTVIObserverremembering the IDs of frames it does not handle, such as every audio frame. Its memory still grows with the frames it handles, but much more slowly.
(PR #5906) -
Fixed
on_user_turn_idlenever firing after a user turn that ended with no transcript, since that turn cancelled the idle timer and nothing restarted it.
(PR #5907) -
Fixed observers keeping the IDs of every frame they had seen for the life of the session, the pipeline worker's idle detection included.
(PR #5908) -
The
runnerextra now requirespipecat-ai-prebuilt>=1.2.2. The conversation panel in the prebuilt client UI served by the development runner now keeps autoscrolling while long bot replies stream in.
(PR #5917)
Performance
RTVIObservernow skips audio frames before checking anything else when audio levels are not reported, instead of testing every condition on every push.
(PR #5906)