pipedog is an unofficial community port of Pipecat, not affiliated with Daily.
pipedog

User muting

Ignore the user's audio and transcripts while the bot greets them, runs a tool, or speaks, with built-in or custom mute strategies.

User mute strategies decide when the user aggregator ignores the user. While the user is muted, their audio, VAD events, transcripts and interruptions are dropped at the aggregator: the bot can't be interrupted, and whatever the user says meanwhile never reaches the context. The user is not muted by default.

import {
  AlwaysUserMuteStrategy,
  BaseUserMuteStrategy,
  EndingUserMuteStrategy,
  FirstSpeechUserMuteStrategy,
  FunctionCallUserMuteStrategy,
  MuteUntilFirstBotCompleteUserMuteStrategy,
  OpeningUserMuteStrategy,
} from "@pipedog/core/turns/user_mute/index";

Configuration

Pass the strategies as userMuteStrategies on LLMUserAggregatorParams:

const aggregators = new LLMContextAggregatorPair(context, {
  userParams: new LLMUserAggregatorParams({
    vadAnalyzer: new SileroVADAnalyzer(),
    userMuteStrategies: [
      // Let the greeting play out in full...
      new MuteUntilFirstBotCompleteUserMuteStrategy(),
      // ...and ignore the user while a tool runs.
      new FunctionCallUserMuteStrategy(),
    ],
  }),
});

Every strategy sees every frame, and the user is muted while any of them says so. The example above mutes from the start of the session until the bot has finished its greeting, and again whenever a function call runs.

Built-in strategies

StrategyMutesUse it to
MuteUntilFirstBotCompleteUserMuteStrategyfrom the start until the bot first finishes speakingprotect the greeting, including the silence before it
OpeningUserMuteStrategyfrom the start until the bot first finishes speaking, or has spoken for maxSecondsprotect the start of a long greeting without muting all of it
FirstSpeechUserMuteStrategyduring the bot's first speech onlyprotect the greeting, but let the user speak before it starts
FunctionCallUserMuteStrategywhile function calls runkeep the user from talking over a tool call
AlwaysUserMuteStrategywhenever the bot speaksturn off barge-in completely
EndingUserMuteStrategyfor good, once the conversation is endinglet the bot finish its goodbye

OpeningUserMuteStrategy and EndingUserMuteStrategy are pipedog additions; the others are upstream's and take no options.

MuteUntilFirstBotCompleteUserMuteStrategy

Mutes from the first frame, before the bot has said anything, and unmutes for good on the first BotStoppedSpeakingFrame. If the bot's first turn fails before it speaks (an ErrorFrame arrives before any BotStartedSpeakingFrame), it unmutes and logs a warning, so a failed greeting doesn't leave the user unable to talk.

OpeningUserMuteStrategy

Like MuteUntilFirstBotCompleteUserMuteStrategy, but the first speech is only protected for maxSeconds from its BotStartedSpeakingFrame. On a phone call, that keeps the "hello?" of picking up from cutting the greeting or reaching the LLM as an answer, while the user can still talk over the rest of a long greeting. The window is checked as frames arrive (input audio keeps them coming). Pass clock to control time in tests.

new OpeningUserMuteStrategy({ maxSeconds: 3 });

FirstSpeechUserMuteStrategy

Lets the user speak until the bot starts speaking, mutes them during that first speech, and unmutes for good when it ends. Later speech can be interrupted as usual.

FunctionCallUserMuteStrategy

Mutes on FunctionCallsStartedFrame and tracks each call by its tool call ID; it unmutes once every call has a FunctionCallResultFrame or a FunctionCallCancelFrame.

EndingUserMuteStrategy

Mutes, and stays muted, once it sees an EndWorkerFrame or StopWorkerFrame, once its isEnding() option returns true, or after end() is called. Strategies never see EndFrame, and an EndFrame queued after a goodbye arrives once the goodbye has been queued, so when the end is decided elsewhere (a tool that queues a goodbye and an EndFrame, say) tell the strategy with isEnding or end():

const ending = new EndingUserMuteStrategy();
// In the tool that ends the call:
ending.end();
await worker.queueFrames([new TTSSpeakFrame({ text: "Goodbye!" }), new EndFrame()]);

AlwaysUserMuteStrategy

Mutes on BotStartedSpeakingFrame and unmutes on BotStoppedSpeakingFrame, for the whole session:

const userParams = new LLMUserAggregatorParams({
  vadAnalyzer: new SileroVADAnalyzer(),
  userMuteStrategies: [new AlwaysUserMuteStrategy()],
});

What muting drops

While muted, the user aggregator drops these frames instead of processing or forwarding them: InputAudioRawFrame, VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame, UserStartedSpeakingFrame, UserStoppedSpeakingFrame, the Proposed... variants of those two, InterimTranscriptionFrame, TranscriptionFrame, EagerTranscriptionFrame, UserBackchannelFrame and InterruptionFrame. StartFrame, EndFrame and CancelFrame always pass, and are never handed to the strategies.

The mute state is updated after each frame: the frame that makes a strategy mute (a BotStartedSpeakingFrame, say) still passes, and muting starts with the next one.

Muting happens in the user aggregator, so processors before it (the STT service, for example) still receive the audio. To stop the speech-to-text service itself, push an STTMuteFrame.

Muting and interruptions

Muting and disabling interruptions both keep the bot talking, but they treat the user's words differently:

  • Muted: what the user says over the bot is discarded. It is never transcribed into the context, and the bot never answers it.
  • Interruptions disabled (enableInterruptions: false on the start strategies, see Interruptions): the user's turn still starts and is transcribed, and the bot answers it once it has finished speaking.

Use a mute strategy when the bot must finish and anything said over it should be ignored, and enableInterruptions: false when the bot should finish but still hear the user out.

Events

The user aggregator emits on_user_mute_started when the user becomes muted and on_user_mute_stopped when they are unmuted, whichever strategy caused it, and broadcasts a UserMuteStartedFrame or UserMuteStoppedFrame through the pipeline at the same moment:

const user = aggregators.user();

user.eventHandler("on_user_mute_started", async () => {
  console.log("user muted");
});
user.eventHandler("on_user_mute_stopped", async () => {
  console.log("user unmuted");
});

Writing a strategy

Subclass BaseUserMuteStrategy (@pipedog/core/turns/user_mute/base_user_mute_strategy) and override processFrame(). Unlike turn strategies, which report their decisions through events, a mute strategy returns a boolean, true to mute. The user aggregator calls it with every frame it receives (except the lifecycle frames above), so it sees the frames that flow downstream to the aggregator and those broadcast upstream to it, such as the bot speaking frames, the function call frames and errors. Frames that only travel downstream after the aggregator, like the LLM's response frames, never reach it.

This one mutes the user while the bot speaks, but only when the application asks for it, such as before reading terms that must be heard in full:

/**
 * Mutes the user while the bot speaks, but only while `protecting` is on.
 * Turn it on before the bot reads something that must be heard in full.
 */
export class ProtectedSpeechUserMuteStrategy extends BaseUserMuteStrategy {
  protecting = false;
  protected _botSpeaking = false;

  override async processFrame(frame: Frame): Promise<boolean> {
    await super.processFrame(frame);

    if (frame instanceof BotStartedSpeakingFrame) {
      this._botSpeaking = true;
    } else if (frame instanceof BotStoppedSpeakingFrame) {
      this._botSpeaking = false;
      this.protecting = false; // one protected utterance at a time
    }

    // true mutes the user, starting with the next frame.
    return this.protecting && this._botSpeaking;
  }
}

const protectedSpeech = new ProtectedSpeechUserMuteStrategy();
const userParams = new LLMUserAggregatorParams({
  vadAnalyzer: new SileroVADAnalyzer(),
  userMuteStrategies: [protectedSpeech],
});

// Later, before the bot reads the terms:
protectedSpeech.protecting = true;
await worker.queueFrame(new TTSSpeakFrame({ text: "Before we continue, please listen..." }));

Mute strategies are BaseObjects, so override setup() and cleanup() (calling super) if the strategy needs tasks or resources; the aggregator calls both.

pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.

Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).

On this page