"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var audio_recognition_exports = {};
__export(audio_recognition_exports, {
  AudioRecognition: () => AudioRecognition,
  STTPipeline: () => STTPipeline
});
module.exports = __toCommonJS(audio_recognition_exports);
var import_mutex = require("@livekit/mutex");
var import_rtc_node = require("@livekit/rtc-node");
var import_throws = require("@livekit/throws-transformer/throws");
var import_api = require("@opentelemetry/api");
var import_web = require("node:stream/web");
var import_exceptions = require("../_exceptions.cjs");
var import_base = require("../inference/eot/base.cjs");
var import_defaults = require("../inference/interruption/defaults.cjs");
var import_errors = require("../inference/interruption/errors.cjs");
var import_interruption_stream = require("../inference/interruption/interruption_stream.cjs");
var import_types = require("../inference/interruption/types.cjs");
var import_chat_context = require("../llm/chat_context.cjs");
var import_log = require("../log.cjs");
var import_deferred_stream = require("../stream/deferred_stream.cjs");
var import_identity_transform = require("../stream/identity_transform.cjs");
var import_merge_readable_streams = require("../stream/merge_readable_streams.cjs");
var import_stream_channel = require("../stream/stream_channel.cjs");
var import_stt = require("../stt/stt.cjs");
var import_telemetry = require("../telemetry/index.cjs");
var import_word = require("../tokenize/basic/word.cjs");
var import_utils = require("../utils.cjs");
var import_vad = require("../vad.cjs");
var import_events = require("./events.cjs");
var import_endpointing = require("./turn_config/endpointing.cjs");
var import_utils2 = require("./utils.cjs");
class SpeakingEvent extends import_utils.Event {
  /**
   * Resolves on the next `set()`. Rejects (and cleans up the listener) if
   * `signal` aborts first. Returns immediately if the event is already set.
   */
  async waitOnce(signal) {
    if (this.isSet) return;
    let abortListener;
    try {
      await Promise.race([
        this.wait().then(() => void 0),
        new Promise((_resolve, reject) => {
          if (signal.aborted) {
            reject(signal.reason ?? new Error("aborted"));
            return;
          }
          abortListener = () => reject(signal.reason ?? new Error("aborted"));
          signal.addEventListener("abort", abortListener, { once: true });
        })
      ]);
    } finally {
      if (abortListener !== void 0) signal.removeEventListener("abort", abortListener);
    }
  }
}
class STTPipeline {
  static PUMP_TASK_CANCEL_TIMEOUT = 5e3;
  sttNode;
  _audioChannel = (0, import_stream_channel.createStreamChannel)();
  _eventChannel = (0, import_stream_channel.createStreamChannel)();
  _pumpTask;
  constructor(sttNode) {
    this.sttNode = sttNode;
    this._pumpTask = import_utils.Task.from(({ signal }) => this.sttPump(signal));
    this._pumpTask.addDoneCallback(() => this._eventChannel.close());
  }
  get audioChannel() {
    return this._audioChannel;
  }
  get eventChannel() {
    return this._eventChannel;
  }
  async sttPump(signal) {
    const node = await this.sttNode(this._audioChannel.stream(), {});
    if (node === null) return;
    try {
      for await (const value of (0, import_utils.readStream)(node, signal)) {
        if (typeof value === "string") {
          throw new Error(`STT node must yield SpeechEvent, got: ${typeof value}`);
        }
        await this._eventChannel.write(value);
      }
    } finally {
      await node.cancel().catch(() => {
      });
    }
  }
  async close() {
    await (0, import_utils.cancelAndWait)([this._pumpTask], STTPipeline.PUMP_TASK_CANCEL_TIMEOUT);
  }
}
class AudioRecognition {
  hooks;
  stt;
  sttPipeline;
  vad;
  usingDefaultVad;
  turnDetector;
  turnDetectorStream;
  /**
   * Future for the in-flight audio-EOT inference request. Recognition owns the
   * request lifecycle: it starts a request on the VAD silence tick, holds the
   * future here, awaits it (with the endpointing delay) in the eou bounce, and
   * clears it on turn boundaries / superseding speech.
   */
  turnDetectorPredictionFut;
  /**
   * True between a turn flush (commit / clearUserTurn) and the next VAD
   * start-of-speech. While set, a late stt final won't start a fresh request;
   * the eou bounce short-circuits via `onMissingEotPrediction`.
   */
  turnDetectorFlushed = false;
  /** Warn once per recognition when the eou bounce runs after a flush. */
  turnDetectorLatePredictionWarned = false;
  /**
   * The last `TurnDetectionEvent` we forwarded via `onEotPrediction`, kept
   * by reference to dedupe: both EOU triggers in a turn read the same
   * resolved prediction future, but the event should fire once per request.
   */
  lastEmittedEotPrediction;
  /**
   * Edge-triggered "user is speaking" event used by the audio-EOT bounce
   * race. Set on VAD `START_OF_SPEECH` (and on any `INFERENCE_DONE` with
   * accumulated speech), cleared on `END_OF_SPEECH`.
   *
   * `Event.set()` is idempotent (re-setting an already-set event resolves
   * any new waiters immediately); cleared on EOS so subsequent waiters
   * park until the next utterance.
   */
  userSpeakingEvent = new SpeakingEvent();
  warnedTurnDetectorPushFailure = false;
  turnDetectionMode;
  endpointing;
  userTurnLimit;
  lastLanguage;
  rootSpanContext;
  sttModel;
  sttProvider;
  getLinkedParticipant;
  deferredInputStream;
  logger = (0, import_log.log)();
  lastFinalTranscriptTime = 0;
  audioTranscript = "";
  audioInterimTranscript = "";
  audioPreflightTranscript = "";
  finalTranscriptConfidence = [];
  lastSpeakingTime;
  speechStartTime;
  userTurnStart;
  userTurnCommitted = false;
  speaking = false;
  vadSpeechStarted = false;
  sampleRate;
  userTurnSpan;
  userTurnTracker = { words: 0, transcript: "" };
  // Provider-known STT ids for the current user turn. Written to the
  // `user_turn` span when it ends so we can correlate traces with the
  // provider's logs for debugging.
  sttRequestIds = [];
  vadInputStream;
  sttInputStream;
  /**
   * Active subscriber writers fed from {@link subscribersBroadcast}. Each
   * {@link subscribeAudioStream} call appends one entry; entries are dropped
   * (and their stream closed) on {@link close}.
   *
   * The broadcast pattern replaces an earlier `tee()`-based approach where
   * the second branch grew without bound for sessions that never called
   * `subscribeAudioStream()` (every voice session that doesn't use AMD).
   * With this design no extra buffering happens until at least one
   * subscriber is registered.
   */
  subscriberWriters = [];
  silenceAudioTransform = new import_identity_transform.IdentityTransform();
  silenceAudioWriter;
  sttOwnershipTransferred = false;
  sttLifecycleLock = new import_mutex.Mutex();
  // all cancellable tasks
  bounceEOUTask;
  commitUserTurnTask;
  sttForwardTask;
  vadTask;
  vadStream;
  sttConsumerTask;
  interruptionTask;
  // interruption detection
  interruptionDetection;
  _inputStartedAt;
  ignoreUserTranscriptUntil;
  transcriptBuffer;
  isInterruptionEnabled;
  isAgentSpeaking;
  interruptionDetected;
  interruptionStreamChannel;
  closed = false;
  // backchannel boundary for adaptive interruption suppression
  backchannelBoundary;
  backchannelBoundaryTimer;
  /** Callback invoked when the backchannel boundary timer expires naturally. */
  backchannelBoundaryCallback;
  constructor(opts) {
    this.hooks = opts.recognitionHooks;
    this.stt = opts.stt;
    this.vad = opts.vad;
    this.usingDefaultVad = opts.usingDefaultVad ?? false;
    this.turnDetector = opts.turnDetector;
    this.checkVadSilenceRequirement();
    this.turnDetectionMode = opts.turnDetectionMode;
    this.userTurnLimit = opts.userTurnLimit;
    this.endpointing = opts.endpointing ?? (0, import_endpointing.createEndpointing)({
      ...import_endpointing.defaultEndpointingOptions,
      minDelay: opts.minEndpointingDelay ?? import_endpointing.defaultEndpointingOptions.minDelay,
      maxDelay: opts.maxEndpointingDelay ?? import_endpointing.defaultEndpointingOptions.maxDelay
    });
    this.lastLanguage = void 0;
    this.rootSpanContext = opts.rootSpanContext;
    this.sttModel = opts.sttModel;
    this.sttProvider = opts.sttProvider;
    this.getLinkedParticipant = opts.getLinkedParticipant;
    this.deferredInputStream = new import_deferred_stream.DeferredReadableStream();
    this.interruptionDetection = opts.interruptionDetection;
    this.transcriptBuffer = [];
    this.isInterruptionEnabled = !!(opts.interruptionDetection && opts.vad);
    this.isAgentSpeaking = false;
    this.interruptionDetected = void 0;
    const rawBoundary = opts.backchannelBoundary;
    if (rawBoundary === void 0 || rawBoundary === null) {
      this.backchannelBoundary = void 0;
    } else if (typeof rawBoundary === "number") {
      if (rawBoundary < 0) {
        throw new Error("backchannelBoundary must be a non-negative number");
      }
      this.backchannelBoundary = [rawBoundary, rawBoundary];
    } else {
      const [start, end] = rawBoundary;
      if (rawBoundary.length !== 2 || start < 0 || end < 0) {
        throw new Error("backchannelBoundary must be a tuple of two non-negative numbers");
      }
      this.backchannelBoundary = [start, end];
    }
    const broadcast = new import_web.TransformStream(
      {
        transform: (chunk, controller) => {
          controller.enqueue(chunk);
          if (this.turnDetectorStream !== void 0) {
            try {
              this.turnDetectorStream.pushAudio(chunk);
            } catch (err) {
              if (!this.warnedTurnDetectorPushFailure) {
                this.warnedTurnDetectorPushFailure = true;
                this.logger.warn(
                  { err: err instanceof Error ? err.message : String(err) },
                  "audio EOT stream pushAudio failed; dropping frames for this turn"
                );
              }
            }
          }
          if (this.subscriberWriters.length === 0) return;
          for (const writer of this.subscriberWriters) {
            writer.write(chunk).catch(() => {
            });
          }
        }
      },
      { highWaterMark: Number.MAX_SAFE_INTEGER },
      { highWaterMark: Number.MAX_SAFE_INTEGER }
    );
    const primaryInputStream = this.deferredInputStream.stream.pipeThrough(broadcast);
    const replaceSttInputWithSilence = (stream) => {
      if (!opts.shouldDiscardAudioForStt) {
        return stream;
      }
      return stream.pipeThrough(
        new import_web.TransformStream({
          transform: (frame, controller) => {
            controller.enqueue(
              opts.shouldDiscardAudioForStt(frame) ? (0, import_utils2.createSilenceFrameLike)(frame) : frame
            );
          }
        })
      );
    };
    if (opts.interruptionDetection) {
      const [vadInputStream, teedInput] = primaryInputStream.tee();
      const [inputStream, sttInputStream] = teedInput.tee();
      this.vadInputStream = vadInputStream;
      this.sttInputStream = (0, import_merge_readable_streams.mergeReadableStreams)(
        replaceSttInputWithSilence(sttInputStream),
        this.silenceAudioTransform.readable
      );
      this.interruptionStreamChannel = (0, import_stream_channel.createStreamChannel)();
      this.interruptionStreamChannel.addStreamInput(inputStream);
    } else {
      const [vadInputStream, sttInputStream] = primaryInputStream.tee();
      this.vadInputStream = vadInputStream;
      this.sttInputStream = (0, import_merge_readable_streams.mergeReadableStreams)(
        replaceSttInputWithSilence(sttInputStream),
        this.silenceAudioTransform.readable
      );
    }
    this.silenceAudioWriter = this.silenceAudioTransform.writable.getWriter();
  }
  /**
   * Current transcript of the user's speech, including interim transcript if available.
   */
  get currentTranscript() {
    if (this.audioInterimTranscript) {
      return `${this.audioTranscript} ${this.audioInterimTranscript}`.trim();
    }
    return this.audioTranscript;
  }
  /** @internal */
  get inputStartedAt() {
    return this._inputStartedAt;
  }
  /** @internal */
  updateOptions(options) {
    if (options.endpointing !== void 0) {
      this.endpointing = options.endpointing;
    }
    if (options.turnDetection !== void 0) {
      this.turnDetectionMode = options.turnDetection ?? void 0;
    }
  }
  /** True iff the user supplied their own VAD (default-VAD is treated as
   * absent at sites that decide between "use VAD signal" and "STT-derived
   * speaking"). */
  get hasUserVad() {
    return this.vad !== void 0 && !this.usingDefaultVad;
  }
  /**
   * Swap the active turn detector at runtime. When an `BaseStreamingTurnDetector`
   * is provided, opens a per-turn FSM stream after retiring the prior one.
   *
   * When `stream` is provided it is adopted as-is (handoff reuse) instead of
   * opening a fresh stream on `detector`; the live transport stream — and its
   * per-session cloud→local fallback state — survives the handoff.
   */
  updateTurnDetector(detector, options) {
    this.checkVadSilenceRequirement(detector);
    this.turnDetector = detector;
    const reuseStream = options == null ? void 0 : options.stream;
    const oldStream = this.turnDetectorStream;
    if (oldStream !== void 0 && oldStream !== reuseStream) {
      oldStream.detach();
      void oldStream.aclose().catch(() => void 0);
    }
    this.userSpeakingEvent.clear();
    const newStream = reuseStream !== void 0 ? reuseStream : detector instanceof import_base.BaseStreamingTurnDetector ? detector.stream() : void 0;
    if (this.turnDetectorStream !== newStream) {
      this.turnDetectorPredictionFut = void 0;
      this.turnDetectorFlushed = false;
    }
    this.turnDetectorStream = newStream;
  }
  /**
   * Detach the turn detector stream for handoff to another AudioRecognition.
   *
   * Returns the live stream (transport run loop intact) without closing it.
   * The caller passes it to the new AudioRecognition via
   * `start({ turnDetectorStream })`. The stream stays attached to its
   * detector, retaining the detector's single-stream slot, so the new
   * AudioRecognition must adopt it rather than open a second stream.
   */
  detachTurnDetector() {
    const stream = this.turnDetectorStream;
    this.turnDetectorStream = void 0;
    this.turnDetectorPredictionFut = void 0;
    return stream;
  }
  /**
   * The audio EOT detector needs a wider silence window than typical VAD
   * defaults. Rather than mutate the VAD's knob, require the caller to
   * configure it: raise if the bound VAD exposes `minSilenceDuration` and it
   * is below the floor. VADs that don't expose the knob are left untouched.
   */
  checkVadSilenceRequirement(detector = this.turnDetector) {
    if (!(detector instanceof import_base.BaseStreamingTurnDetector) || this.vad === void 0) {
      return;
    }
    const current = this.vad.minSilenceDuration;
    if (current === null) {
      return;
    }
    const required = import_base.MIN_SILENCE_DURATION_MS + 50;
    if (current < required) {
      throw new Error(
        `vad minSilenceDuration=${current}ms is too low for the TurnDetector. Raise the VAD's minSilenceDuration to at least ${required}ms.`
      );
    }
  }
  /**
   * Speaking-guard wrapper for the bounce-EOU task. When an
   * `BaseStreamingTurnDetector` is active, the bounce task races against the
   * `userSpeakingEvent`:
   *
   * - if the user is already speaking, skip the EOU outright;
   * - if the user starts speaking during the endpointing delay (e.g.
   *   the LLM hadn't returned yet but the user added another phrase),
   *   abort the inner bounce so the next turn drives the decision.
   *
   * VAD `START_OF_SPEECH` also calls `bounceEOUTask?.cancel()`, but the
   * cancel path only races VAD sessions. STT-only audio-EOT setups need
   * the explicit event-driven race here.
   */
  async bounceEOUTaskWithSpeakingGuard(controller, inner, context) {
    if (this.speaking) {
      this.logger.debug(context, "user is still speaking, skipping end of turn task");
      return;
    }
    const innerController = new AbortController();
    const onOuterAbort = () => innerController.abort();
    controller.signal.addEventListener("abort", onOuterAbort, { once: true });
    let speakingWon = false;
    try {
      const innerPromise = inner(innerController);
      void innerPromise.catch(() => {
      });
      await Promise.race([
        innerPromise,
        this.userSpeakingEvent.waitOnce(controller.signal).then(() => {
          speakingWon = true;
        })
      ]);
      if (speakingWon) {
        this.logger.debug(context, "user spoke during endpointing, cancelling end of turn task");
      }
    } finally {
      controller.signal.removeEventListener("abort", onOuterAbort);
      innerController.abort();
    }
  }
  async start(options) {
    this.startSttTasks(options == null ? void 0 : options.sttPipeline, options == null ? void 0 : options.inputStartedAt);
    this.vadTask = import_utils.Task.from(({ signal }) => this.createVadTask(this.vad, signal));
    this.vadTask.result.catch((err) => {
      this.logger.error(`Error running VAD task: ${err}`);
    });
    this.interruptionTask = import_utils.Task.from(
      ({ signal }) => this.createInterruptionTask(this.interruptionDetection, signal)
    );
    this.interruptionTask.result.catch((err) => {
      this.logger.error(`Error running interruption task: ${err}`);
    });
    if (this.turnDetector instanceof import_base.BaseStreamingTurnDetector || this.turnDetector === void 0) {
      this.updateTurnDetector(this.turnDetector, { stream: options == null ? void 0 : options.turnDetectorStream });
    }
  }
  async stop() {
    var _a, _b, _c, _d;
    await ((_a = this.sttConsumerTask) == null ? void 0 : _a.cancelAndWait());
    await ((_b = this.sttForwardTask) == null ? void 0 : _b.cancelAndWait());
    await ((_c = this.vadTask) == null ? void 0 : _c.cancelAndWait());
    await ((_d = this.interruptionTask) == null ? void 0 : _d.cancelAndWait());
    if (this.turnDetectorStream !== void 0) {
      const stream = this.turnDetectorStream;
      this.turnDetectorStream = void 0;
      await stream.aclose().catch(() => void 0);
    }
  }
  async disableInterruptionDetection() {
    var _a, _b;
    this.isInterruptionEnabled = false;
    this.interruptionDetection = void 0;
    await ((_a = this.interruptionTask) == null ? void 0 : _a.cancelAndWait());
    this.interruptionTask = void 0;
    await ((_b = this.interruptionStreamChannel) == null ? void 0 : _b.close());
    this.interruptionStreamChannel = void 0;
    this.cancelBackchannelBoundary();
    await this.flushHeldTranscripts(0, true);
  }
  /**
   * Whether the backchannel boundary timer is currently running.
   */
  get backchannelBoundaryActive() {
    return this.backchannelBoundaryTimer !== void 0;
  }
  /**
   * Fires when the backchannel boundary timer expires naturally. Drops the timer handle and
   * invokes the registered callback exactly once.
   */
  onBackchannelBoundaryDone() {
    this.backchannelBoundaryTimer = void 0;
    const cb = this.backchannelBoundaryCallback;
    this.backchannelBoundaryCallback = void 0;
    cb == null ? void 0 : cb();
  }
  /**
   * Cancel any pending backchannel boundary timer and clear the registered callback.
   */
  cancelBackchannelBoundary() {
    if (this.backchannelBoundaryTimer !== void 0) {
      clearTimeout(this.backchannelBoundaryTimer);
      this.backchannelBoundaryTimer = void 0;
    }
    this.backchannelBoundaryCallback = void 0;
  }
  async onStartOfAgentSpeech(startedAt) {
    this.isAgentSpeaking = true;
    this.endpointing.onStartOfAgentSpeech(startedAt);
    this.userTurnTracker = { words: 0, transcript: "" };
    if (this.backchannelBoundary && this.backchannelBoundary[0] > 0) {
      this.cancelBackchannelBoundary();
      const startCooldown = this.backchannelBoundary[0];
      this.backchannelBoundaryTimer = setTimeout(
        () => this.onBackchannelBoundaryDone(),
        startCooldown
      );
    }
    return this.trySendInterruptionSentinel(import_interruption_stream.InterruptionStreamSentinel.agentSpeechStarted());
  }
  async onEndOfAgentSpeech(ignoreUserTranscriptUntil) {
    this.cancelBackchannelBoundary();
    const now = Date.now();
    const wasAgentSpeaking = this.isAgentSpeaking;
    const priorIgnoreUserTranscriptUntil = this.ignoreUserTranscriptUntil;
    if (wasAgentSpeaking) {
      this.endpointing.onEndOfAgentSpeech(now);
    }
    if (!this.isInterruptionEnabled) {
      this.isAgentSpeaking = false;
      return;
    }
    let endCooldown = 0;
    if (wasAgentSpeaking) {
      endCooldown = this.backchannelBoundary ? this.backchannelBoundary[1] : 0;
      const ignoreUntil = this.ignoreUserTranscriptUntil ? Math.min(ignoreUserTranscriptUntil, this.ignoreUserTranscriptUntil) : ignoreUserTranscriptUntil;
      this.logger.trace({ ignoreUntil, endCooldown }, "flushing held transcripts");
      this.ignoreUserTranscriptUntil = ignoreUntil - endCooldown;
    }
    this.isAgentSpeaking = false;
    const inputOpen = await this.trySendInterruptionSentinel(
      import_interruption_stream.InterruptionStreamSentinel.agentSpeechEnded()
    );
    if (!inputOpen) {
      return;
    }
    if (wasAgentSpeaking) {
      if (priorIgnoreUserTranscriptUntil === void 0) {
        this.onEndOfOverlapSpeech(Date.now());
      }
      await this.flushHeldTranscripts(endCooldown);
    }
  }
  /** Start interruption inference when agent is speaking and overlap speech starts. */
  async onStartOfOverlapSpeech(speechDuration, startedAt, userSpeakingSpan) {
    if (this.isAgentSpeaking) {
      if (!this.endpointing.overlapping) {
        this.endpointing.onStartOfSpeech(startedAt, true);
      }
      this.trySendInterruptionSentinel(
        import_interruption_stream.InterruptionStreamSentinel.overlapSpeechStarted(
          speechDuration,
          startedAt,
          userSpeakingSpan
        )
      );
    }
  }
  /** End interruption inference when overlap speech ends. */
  async onEndOfOverlapSpeech(endedAt, userSpeakingSpan) {
    if (!this.isInterruptionEnabled) {
      return;
    }
    if (userSpeakingSpan && userSpeakingSpan.isRecording()) {
      userSpeakingSpan.setAttribute(import_telemetry.traceTypes.ATTR_IS_INTERRUPTION, "false");
    }
    return this.trySendInterruptionSentinel(import_interruption_stream.InterruptionStreamSentinel.overlapSpeechEnded(endedAt));
  }
  /**
   * Flush held transcripts. When `force` is true, all buffered events are emitted during
   * interruption-detector teardown because ignore-window gating can no longer be trusted.
   * Otherwise, only transcripts whose end time is after `ignoreUserTranscriptUntil - cooldown`
   * are emitted. Events without timestamps are treated as the next valid event.
   */
  async flushHeldTranscripts(cooldown = 0, force = false) {
    var _a;
    if (this.transcriptBuffer.length === 0) {
      this.resetInterruptionDetection();
      return;
    }
    if (force) {
      const eventsToEmit2 = [...this.transcriptBuffer];
      this.resetInterruptionDetection();
      for (const event of eventsToEmit2) {
        await this.onSTTEvent(event);
      }
      return;
    }
    if (!this.isInterruptionEnabled || this.ignoreUserTranscriptUntil === void 0 || this._inputStartedAt === void 0) {
      this.resetInterruptionDetection();
      return;
    }
    let emitFromIndex = null;
    let shouldFlush = false;
    for (let i = 0; i < this.transcriptBuffer.length; i++) {
      const ev = this.transcriptBuffer[i];
      if (!ev || !ev.alternatives || ev.alternatives.length === 0) {
        emitFromIndex = Math.min(emitFromIndex ?? i, i);
        continue;
      }
      const firstAlternative = ev.alternatives[0];
      if (firstAlternative.startTime === firstAlternative.endTime && firstAlternative.startTime === 0) {
        this.resetInterruptionDetection();
        return;
      }
      if (this.#alternativeEndsBeforeIgnoreWindow(firstAlternative)) {
        emitFromIndex = null;
      } else {
        emitFromIndex = Math.min(emitFromIndex ?? i, i);
        shouldFlush = true;
        break;
      }
    }
    const eventsToEmit = emitFromIndex !== null && shouldFlush ? this.transcriptBuffer.slice(emitFromIndex) : [];
    const prevIgnoreUserTranscriptUntil = this.ignoreUserTranscriptUntil;
    const prevInputStartedAt = this._inputStartedAt;
    this.resetInterruptionDetection();
    for (const event of eventsToEmit) {
      let addedDelay = 0;
      const firstAlternative = (_a = event.alternatives) == null ? void 0 : _a[0];
      if (firstAlternative && firstAlternative.endTime > 0 && prevIgnoreUserTranscriptUntil !== void 0 && prevInputStartedAt !== void 0) {
        addedDelay = Math.max(
          0,
          firstAlternative.endTime * 1e3 + prevInputStartedAt - prevIgnoreUserTranscriptUntil + cooldown
        );
      }
      this.logger.trace(
        { event: event.type, cooldown, addedDelay },
        "re-emitting held user transcript"
      );
      await this.onSTTEvent(event);
    }
  }
  resetInterruptionDetection() {
    this.transcriptBuffer = [];
    this.ignoreUserTranscriptUntil = void 0;
  }
  #alternativeEndsBeforeIgnoreWindow(alternative) {
    if (this.ignoreUserTranscriptUntil === void 0 || !this._inputStartedAt || alternative.endTime <= 0) {
      return false;
    }
    return alternative.endTime * 1e3 + this._inputStartedAt < this.ignoreUserTranscriptUntil;
  }
  shouldHoldSttEvent(ev) {
    if (!this.isInterruptionEnabled) {
      return false;
    }
    if (this.isAgentSpeaking) {
      return true;
    }
    if (ev.type === import_stt.SpeechEventType.START_OF_SPEECH) {
      this.ignoreUserTranscriptUntil = void 0;
      this.transcriptBuffer = [];
      return false;
    }
    if (this.ignoreUserTranscriptUntil === void 0) {
      return false;
    }
    if (!ev.alternatives || ev.alternatives.length === 0) {
      return true;
    }
    const alternative = ev.alternatives[0];
    if (alternative.startTime !== alternative.endTime && this.#alternativeEndsBeforeIgnoreWindow(alternative)) {
      return true;
    }
    return false;
  }
  async trySendInterruptionSentinel(frame) {
    if (this.isInterruptionEnabled && this.interruptionStreamChannel && !this.interruptionStreamChannel.closed) {
      try {
        await this.interruptionStreamChannel.write(frame);
        return true;
      } catch (e) {
        this.logger.warn(
          `could not forward interruption sentinel: ${e instanceof Error ? e.message : String(e)}`
        );
      }
    }
    return false;
  }
  ensureUserTurnSpan(startTime) {
    var _a;
    if (this.userTurnSpan && this.userTurnSpan.isRecording()) {
      return this.userTurnSpan;
    }
    startTime ??= Date.now();
    if (this.userTurnStart === void 0) {
      this.userTurnStart = startTime;
    }
    this.userTurnSpan = import_telemetry.tracer.startSpan({
      name: "user_turn",
      context: this.rootSpanContext,
      startTime
    });
    const participant = (_a = this.getLinkedParticipant) == null ? void 0 : _a.call(this);
    if (participant) {
      (0, import_utils2.setParticipantSpanAttributes)(this.userTurnSpan, participant);
    }
    if (this.sttModel) {
      this.userTurnSpan.setAttribute(import_telemetry.traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.sttModel);
    }
    if (this.sttProvider) {
      this.userTurnSpan.setAttribute(import_telemetry.traceTypes.ATTR_GEN_AI_PROVIDER_NAME, this.sttProvider);
    }
    return this.userTurnSpan;
  }
  userTurnContext(span) {
    const base = this.rootSpanContext ?? import_api.ROOT_CONTEXT;
    return import_api.trace.setSpan(base, span);
  }
  async onSTTEvent(ev) {
    var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s;
    if (ev.requestId && !this.sttRequestIds.includes(ev.requestId)) {
      this.sttRequestIds.push(ev.requestId);
    }
    if (this.turnDetectionMode === "manual" && this.userTurnCommitted && (this.bounceEOUTask === void 0 || this.bounceEOUTask.done || ev.type == import_stt.SpeechEventType.INTERIM_TRANSCRIPT)) {
      this.logger.debug(
        {
          userTurnCommitted: this.userTurnCommitted,
          eouTaskDone: (_a = this.bounceEOUTask) == null ? void 0 : _a.done,
          evType: ev.type,
          turnDetectionMode: this.turnDetectionMode
        },
        "ignoring stt event"
      );
      return;
    }
    if (ev.type !== import_stt.SpeechEventType.RECOGNITION_USAGE && this.isInterruptionEnabled) {
      if (this.shouldHoldSttEvent(ev)) {
        this.logger.trace(
          { event: ev.type, ignoreUserTranscriptUntil: this.ignoreUserTranscriptUntil },
          "holding STT event until ignore_user_transcript_until expires"
        );
        this.transcriptBuffer.push(ev);
        return;
      } else if (this.transcriptBuffer.length > 0) {
        const endCooldown = this.backchannelBoundary ? this.backchannelBoundary[1] : 0;
        await this.flushHeldTranscripts(endCooldown);
      }
    }
    const firstAlternative = (_b = ev.alternatives) == null ? void 0 : _b[0];
    const inputStartedAt = this._inputStartedAt;
    const hasSTTEndTime = firstAlternative !== void 0 && firstAlternative.endTime > 0 && inputStartedAt !== void 0;
    const sttLastSpeakingTime = hasSTTEndTime ? Math.min(firstAlternative.endTime * 1e3 + inputStartedAt, Date.now()) : Date.now();
    switch (ev.type) {
      case import_stt.SpeechEventType.FINAL_TRANSCRIPT:
        const transcript = (_d = (_c = ev.alternatives) == null ? void 0 : _c[0]) == null ? void 0 : _d.text;
        const confidence = ((_f = (_e = ev.alternatives) == null ? void 0 : _e[0]) == null ? void 0 : _f.confidence) ?? 0;
        this.lastLanguage = (_h = (_g = ev.alternatives) == null ? void 0 : _g[0]) == null ? void 0 : _h.language;
        if (!transcript) {
          return;
        }
        this.hooks.onFinalTranscript(
          ev,
          this.hasUserVad || this.turnDetectionMode === "stt" ? this.speaking : void 0
        );
        this.logger.debug(
          {
            user_transcript: transcript,
            language: this.lastLanguage
          },
          "received user transcript"
        );
        this.lastFinalTranscriptTime = Date.now();
        this.audioTranscript += ` ${transcript}`;
        this.audioTranscript = this.audioTranscript.trimStart();
        this.finalTranscriptConfidence.push(confidence);
        const transcriptChanged = this.audioTranscript !== this.audioPreflightTranscript;
        this.audioInterimTranscript = "";
        this.audioPreflightTranscript = "";
        if (!this.hasUserVad || this.lastSpeakingTime === void 0) {
          this.lastSpeakingTime = sttLastSpeakingTime;
        }
        this.checkUserTurnLimit(transcript);
        if (this.vadBaseTurnDetection || this.userTurnCommitted) {
          if (transcriptChanged) {
            this.logger.debug(
              { transcript: this.audioTranscript },
              "triggering preemptive generation (FINAL_TRANSCRIPT)"
            );
            this.hooks.onPreemptiveGeneration({
              newTranscript: this.audioTranscript,
              transcriptConfidence: this.finalTranscriptConfidence.length > 0 ? this.finalTranscriptConfidence.reduce((a, b) => a + b, 0) / this.finalTranscriptConfidence.length : 0,
              startedSpeakingAt: this.speechStartTime
            });
          }
          if (!this.speaking) {
            const chatCtx = this.hooks.retrieveChatCtx();
            this.logger.debug("running EOU detection on stt FINAL_TRANSCRIPT");
            this.runEOUDetection(chatCtx, "stt");
          }
        }
        break;
      case import_stt.SpeechEventType.PREFLIGHT_TRANSCRIPT:
        this.hooks.onInterimTranscript(
          ev,
          this.hasUserVad || this.turnDetectionMode === "stt" ? this.speaking : void 0
        );
        const preflightTranscript = ((_j = (_i = ev.alternatives) == null ? void 0 : _i[0]) == null ? void 0 : _j.text) ?? "";
        const preflightConfidence = ((_l = (_k = ev.alternatives) == null ? void 0 : _k[0]) == null ? void 0 : _l.confidence) ?? 0;
        const preflightLanguage = (_n = (_m = ev.alternatives) == null ? void 0 : _m[0]) == null ? void 0 : _n.language;
        const MIN_LANGUAGE_DETECTION_LENGTH = 5;
        if (!this.lastLanguage || preflightLanguage && preflightTranscript.length > MIN_LANGUAGE_DETECTION_LENGTH) {
          this.lastLanguage = preflightLanguage;
        }
        if (!preflightTranscript) {
          return;
        }
        this.logger.debug(
          {
            user_transcript: preflightTranscript,
            language: this.lastLanguage
          },
          "received user preflight transcript"
        );
        this.lastFinalTranscriptTime = Date.now();
        this.audioPreflightTranscript = `${this.audioTranscript} ${preflightTranscript}`.trimStart();
        this.audioInterimTranscript = preflightTranscript;
        if (!this.hasUserVad || this.lastSpeakingTime === void 0) {
          this.lastSpeakingTime = sttLastSpeakingTime;
        }
        if (this.turnDetectionMode !== "manual" || this.userTurnCommitted) {
          const confidenceVals = [...this.finalTranscriptConfidence, preflightConfidence];
          this.logger.debug(
            {
              transcript: this.audioPreflightTranscript.length > 100 ? this.audioPreflightTranscript.slice(0, 100) + "..." : this.audioPreflightTranscript
            },
            "triggering preemptive generation (PREFLIGHT_TRANSCRIPT)"
          );
          this.hooks.onPreemptiveGeneration({
            newTranscript: this.audioPreflightTranscript,
            transcriptConfidence: confidenceVals.length > 0 ? confidenceVals.reduce((a, b) => a + b, 0) / confidenceVals.length : 0,
            startedSpeakingAt: this.speechStartTime
          });
        }
        break;
      case import_stt.SpeechEventType.INTERIM_TRANSCRIPT:
        this.logger.debug({ transcript: (_p = (_o = ev.alternatives) == null ? void 0 : _o[0]) == null ? void 0 : _p.text }, "interim transcript");
        this.hooks.onInterimTranscript(
          ev,
          this.hasUserVad || this.turnDetectionMode === "stt" ? this.speaking : void 0
        );
        this.audioInterimTranscript = ((_r = (_q = ev.alternatives) == null ? void 0 : _q[0]) == null ? void 0 : _r.text) ?? "";
        break;
      case import_stt.SpeechEventType.START_OF_SPEECH:
        if (this.turnDetectionMode !== "stt") break;
        {
          const speechStartTime = Date.now();
          const span = this.ensureUserTurnSpan(speechStartTime);
          const ctx = this.userTurnContext(span);
          this.endpointing.onStartOfSpeech(speechStartTime, this.isAgentSpeaking);
          this.interruptionDetected = void 0;
          import_api.context.with(ctx, () => {
            this.hooks.onStartOfSpeech({
              type: import_vad.VADEventType.START_OF_SPEECH,
              samplesIndex: 0,
              timestamp: Date.now(),
              speechDuration: 0,
              silenceDuration: 0,
              frames: [],
              probability: 0,
              inferenceDuration: 0,
              speaking: true,
              rawAccumulatedSilence: 0,
              rawAccumulatedSpeech: 0
            });
          });
        }
        this.speaking = true;
        this.lastSpeakingTime = sttLastSpeakingTime;
        this.userSpeakingEvent.set();
        (_s = this.bounceEOUTask) == null ? void 0 : _s.cancel();
        break;
      case import_stt.SpeechEventType.END_OF_SPEECH:
        if (this.turnDetectionMode !== "stt") break;
        {
          const speechEndTime = Date.now();
          const span = this.ensureUserTurnSpan();
          const ctx = this.userTurnContext(span);
          if (this.speaking) {
            this.endpointing.onEndOfSpeech(
              speechEndTime,
              this.interruptionDetected === false && this.isAgentSpeaking
            );
          }
          import_api.context.with(ctx, () => {
            this.hooks.onEndOfSpeech({
              type: import_vad.VADEventType.END_OF_SPEECH,
              samplesIndex: 0,
              timestamp: Date.now(),
              speechDuration: 0,
              silenceDuration: 0,
              frames: [],
              probability: 0,
              inferenceDuration: 0,
              speaking: false,
              rawAccumulatedSilence: 0,
              rawAccumulatedSpeech: 0
            });
          });
        }
        if (this.hasUserVad && this.vadSpeechStarted) {
          if (this.vadStream) {
            this.vadStream.flush();
          } else {
            this.resetVad();
          }
          this.logger.warn(
            {
              vadSpeechStartTime: this.speechStartTime,
              flushed: this.vadStream !== void 0
            },
            "stt end of speech received while vad is still in a speech segment, flushing vad"
          );
        }
        this.speaking = false;
        this.userSpeakingEvent.clear();
        this.userTurnCommitted = true;
        if (!this.hasUserVad || this.lastSpeakingTime === void 0) {
          this.lastSpeakingTime = sttLastSpeakingTime;
        }
        if (!this.speaking) {
          const chatCtx = this.hooks.retrieveChatCtx();
          this.logger.debug("running EOU detection on stt END_OF_SPEECH");
          this.runEOUDetection(chatCtx, "stt");
        }
    }
  }
  onOverlapSpeechEvent(ev) {
    if (this.backchannelBoundaryActive && !ev.isInterruption) {
      this.logger.trace(
        "ignoring backchannel event during backchannel boundary cooldown, falling back to vad"
      );
      return;
    }
    this.interruptionDetected = ev.isInterruption;
    if (ev.isInterruption) {
      this.hooks.onInterruption(ev);
    }
  }
  onMissingEotPrediction() {
    if (this.turnDetectorFlushed) {
      if (!this.turnDetectorLatePredictionWarned) {
        this.turnDetectorLatePredictionWarned = true;
        this.logger.warn(
          "eou detection ran after the audio eot turn was already flushed (likely a late stt final). consider raising `minDelay` in the endpointing options to accommodate slow stt. subsequent occurrences will log at debug level."
        );
      } else {
        this.logger.debug("stt transcript arrived after a turn flush, skipping eot prediction");
      }
    } else {
      this.logger.debug("no eot inference request in flight, skipping eot prediction");
    }
  }
  runEOUDetection(chatCtx, trigger = "vad") {
    var _a;
    this.logger.debug(
      {
        stt: this.stt,
        audioTranscript: this.audioTranscript,
        turnDetectionMode: this.turnDetectionMode
      },
      "running EOU detection"
    );
    if (this.stt && !this.audioTranscript && this.turnDetectionMode !== "manual") {
      this.logger.debug("skipping EOU detection");
      return;
    }
    chatCtx = chatCtx.copy();
    if (this.audioTranscript) {
      chatCtx.addMessage({ role: "user", content: this.audioTranscript });
    }
    const hasAudioDetector = this.turnDetector instanceof import_base.BaseStreamingTurnDetector;
    const useDetector = this.turnDetectionMode !== "manual" && (this.audioTranscript || hasAudioDetector);
    let turnDetector;
    if (!useDetector) {
      turnDetector = void 0;
    } else if (hasAudioDetector) {
      turnDetector = this.turnDetectorStream;
    } else {
      turnDetector = this.turnDetector;
    }
    const bounceEOUTask = (lastSpeakingTime2, lastFinalTranscriptTime2, speechStartTime2) => async (controller) => {
      let endpointingDelay = this.endpointing.minDelay;
      const userTurnSpan = this.ensureUserTurnSpan();
      const userTurnCtx = this.userTurnContext(userTurnSpan);
      if (turnDetector) {
        if (!await turnDetector.supportsLanguage(this.lastLanguage)) {
          this.logger.debug(`Turn detector does not support language ${this.lastLanguage}`);
        } else {
          await import_telemetry.tracer.startActiveSpan(
            async (span) => {
              this.logger.debug("Running turn detector model");
              let endOfTurnProbability;
              let unlikelyThreshold;
              let backchannelThreshold;
              let fromCache = false;
              let predictionEvent;
              if (turnDetector instanceof import_base.BaseStreamingTurnDetectorStream) {
                const fut = this.turnDetectorPredictionFut;
                if (fut === void 0) {
                  this.onMissingEotPrediction();
                } else {
                  fromCache = fut.done;
                  let timeoutId;
                  const winner = await Promise.race([
                    fut.await.then((ev) => ({ kind: "value", ev })),
                    new Promise((resolve) => {
                      timeoutId = setTimeout(
                        () => resolve({ kind: "timeout" }),
                        endpointingDelay
                      );
                    })
                  ]);
                  if (timeoutId !== void 0) clearTimeout(timeoutId);
                  if (controller.signal.aborted) return;
                  if (winner.kind === "value") {
                    predictionEvent = winner.ev;
                    endOfTurnProbability = predictionEvent.endOfTurnProbability;
                    unlikelyThreshold = await turnDetector.unlikelyThreshold(this.lastLanguage);
                    backchannelThreshold = await turnDetector.backchannelThreshold(
                      this.lastLanguage
                    );
                  } else {
                    this.logger.warn(
                      { timeoutMs: endpointingDelay },
                      "eot prediction timed out, committing without a prediction"
                    );
                    turnDetector.cancelInference({ timedOut: true });
                    this.turnDetectorPredictionFut = void 0;
                  }
                }
              } else {
                try {
                  endOfTurnProbability = await turnDetector.predictEndOfTurn(
                    chatCtx,
                    endpointingDelay
                  );
                  unlikelyThreshold = await turnDetector.unlikelyThreshold(this.lastLanguage);
                } catch (error) {
                  this.logger.error(error, "Error predicting end of turn");
                }
                if (controller.signal.aborted) return;
              }
              if (endOfTurnProbability !== void 0 && unlikelyThreshold !== void 0 && endOfTurnProbability < unlikelyThreshold) {
                endpointingDelay = this.endpointing.maxDelay;
              }
              this.logger.debug(
                {
                  endOfTurnProbability,
                  unlikelyThreshold,
                  endpointingDelay,
                  language: this.lastLanguage,
                  trigger,
                  fromCache
                },
                "eot prediction"
              );
              const prediction = predictionEvent;
              span.setAttribute(
                import_telemetry.traceTypes.ATTR_CHAT_CTX,
                JSON.stringify(chatCtx.toJSON({ excludeTimestamp: false }))
              );
              if (endOfTurnProbability !== void 0) {
                span.setAttribute(import_telemetry.traceTypes.ATTR_EOU_PROBABILITY, endOfTurnProbability);
              }
              if (unlikelyThreshold !== void 0) {
                span.setAttribute(import_telemetry.traceTypes.ATTR_EOU_UNLIKELY_THRESHOLD, unlikelyThreshold);
              }
              span.setAttribute(import_telemetry.traceTypes.ATTR_EOU_DELAY, endpointingDelay);
              span.setAttribute(import_telemetry.traceTypes.ATTR_EOU_LANGUAGE, this.lastLanguage ?? "");
              span.setAttribute(import_telemetry.traceTypes.ATTR_EOU_FROM_CACHE, fromCache);
              span.setAttribute(import_telemetry.traceTypes.ATTR_EOU_SOURCE, trigger);
              if (endOfTurnProbability !== void 0 && unlikelyThreshold !== void 0 && (prediction === void 0 || prediction !== this.lastEmittedEotPrediction)) {
                this.lastEmittedEotPrediction = prediction;
                const inferenceDurationMs = (prediction == null ? void 0 : prediction.inferenceDuration) ?? 0;
                const delayMs = lastSpeakingTime2 !== void 0 ? Date.now() - lastSpeakingTime2 : 0;
                this.hooks.onEotPrediction(
                  (0, import_events.createEotPredictionEvent)({
                    probability: endOfTurnProbability,
                    threshold: unlikelyThreshold,
                    inferenceDurationMs,
                    delayMs
                  })
                );
                const backchannelProbability = prediction == null ? void 0 : prediction.backchannelProbability;
                if (backchannelProbability !== void 0 && backchannelThreshold !== void 0 && backchannelProbability >= backchannelThreshold) {
                  this.hooks.onAgentBackchannelOpportunity(
                    (0, import_events._createAgentBackchannelOpportunityEvent)({
                      probability: backchannelProbability,
                      threshold: backchannelThreshold,
                      endOfTurnProbability,
                      endOfTurnThreshold: unlikelyThreshold,
                      language: this.lastLanguage
                    })
                  );
                }
              }
              if ((prediction == null ? void 0 : prediction.detectionDelay) !== void 0) {
                span.setAttribute(import_telemetry.traceTypes.ATTR_EOU_DETECTION_DELAY, prediction.detectionDelay);
              }
            },
            {
              name: "eou_detection",
              context: userTurnCtx
            }
          );
        }
      }
      let extraSleep = endpointingDelay;
      if (lastSpeakingTime2 !== void 0) {
        extraSleep += lastSpeakingTime2 - Date.now();
      }
      if (extraSleep > 0) {
        await (0, import_utils.delay)(Math.max(extraSleep, 0), { signal: controller.signal });
      }
      this.logger.debug({ transcript: this.audioTranscript }, "end of user turn");
      const confidenceAvg = this.finalTranscriptConfidence.length > 0 ? this.finalTranscriptConfidence.reduce((a, b) => a + b, 0) / this.finalTranscriptConfidence.length : 0;
      let startedSpeakingAt;
      let stoppedSpeakingAt;
      let transcriptionDelay;
      let endOfUtteranceDelay;
      if (lastFinalTranscriptTime2 !== 0 && lastSpeakingTime2 !== void 0 && speechStartTime2 !== void 0) {
        startedSpeakingAt = speechStartTime2;
        stoppedSpeakingAt = lastSpeakingTime2;
        transcriptionDelay = Math.max(lastFinalTranscriptTime2 - lastSpeakingTime2, 0);
        endOfUtteranceDelay = Date.now() - lastSpeakingTime2;
      }
      const committed = await this.hooks.onEndOfTurn({
        newTranscript: this.audioTranscript,
        transcriptConfidence: confidenceAvg,
        transcriptionDelay: transcriptionDelay ?? 0,
        endOfUtteranceDelay: endOfUtteranceDelay ?? 0,
        startedSpeakingAt,
        stoppedSpeakingAt
      });
      if (committed) {
        this._endUserTurnSpan({
          transcript: this.audioTranscript,
          confidence: confidenceAvg,
          transcriptionDelay: transcriptionDelay ?? 0,
          endOfUtteranceDelay: endOfUtteranceDelay ?? 0
        });
        this.audioTranscript = "";
        this.finalTranscriptConfidence = [];
        this.lastFinalTranscriptTime = 0;
        if (this.lastSpeakingTime === lastSpeakingTime2) {
          this.speechStartTime = void 0;
          this.vadSpeechStarted = false;
          this.lastSpeakingTime = void 0;
        }
        if (this.turnDetectorStream !== void 0) {
          this.turnDetectorStream.flush("turn committed");
          this.turnDetectorPredictionFut = void 0;
          this.turnDetectorFlushed = true;
        }
      }
      this.userTurnCommitted = false;
    };
    (_a = this.bounceEOUTask) == null ? void 0 : _a.cancel();
    const lastSpeakingTime = this.lastSpeakingTime;
    const lastFinalTranscriptTime = this.lastFinalTranscriptTime;
    const speechStartTime = this.userTurnStart;
    const factory = hasAudioDetector ? (controller) => this.bounceEOUTaskWithSpeakingGuard(
      controller,
      bounceEOUTask(lastSpeakingTime, lastFinalTranscriptTime, speechStartTime),
      { lastSpeakingTime, lastFinalTranscriptTime, speechStartTime }
    ) : bounceEOUTask(lastSpeakingTime, lastFinalTranscriptTime, speechStartTime);
    this.bounceEOUTask = import_utils.Task.from(factory);
    this.bounceEOUTask.result.then(() => {
      this.logger.debug("EOU detection task completed");
    }).catch((err) => {
      if (err instanceof Error && err.name === "AbortError") {
        return;
      }
      this.logger.error(err, "Error in EOU detection task:");
    });
  }
  async waitForEndOfTurnTask() {
    if (!this.bounceEOUTask || this.bounceEOUTask.done) {
      return;
    }
    try {
      await this.bounceEOUTask.result;
    } catch (error) {
      if (error instanceof Error && error.name === "AbortError") {
        return;
      }
      throw error;
    }
  }
  checkUserTurnLimit(transcript) {
    var _a, _b;
    const maxWords = ((_a = this.userTurnLimit) == null ? void 0 : _a.maxWords) ?? null;
    const maxDuration = ((_b = this.userTurnLimit) == null ? void 0 : _b.maxDuration) ?? null;
    if (maxWords === null && maxDuration === null) {
      return;
    }
    const now = Date.now();
    this.userTurnTracker.startedAt ??= this.speechStartTime ?? now;
    this.userTurnTracker.words += (0, import_word.splitWords)(transcript, true).length;
    this.userTurnTracker.transcript = `${this.userTurnTracker.transcript} ${transcript}`.trim();
    const duration = now - this.userTurnTracker.startedAt;
    const timeExceeded = maxDuration !== null && duration >= maxDuration;
    const wordsExceeded = maxWords !== null && this.userTurnTracker.words >= maxWords;
    if (!timeExceeded && !wordsExceeded) {
      return;
    }
    this.hooks.onUserTurnExceeded(
      (0, import_events.createUserTurnExceededEvent)({
        transcript: this.currentTranscript,
        accumulatedTranscript: this.userTurnTracker.transcript,
        accumulatedWordCount: this.userTurnTracker.words,
        duration
      })
    );
  }
  startSttTasks(reusePipeline, inputStartedAt) {
    if (!this.stt) return;
    this.sttPipeline = reusePipeline ?? new STTPipeline(this.stt);
    this.transcriptBuffer = [];
    this.ignoreUserTranscriptUntil = void 0;
    this._inputStartedAt = inputStartedAt;
    this.sttOwnershipTransferred = false;
    const pipeline = this.sttPipeline;
    this.sttForwardTask = import_utils.Task.from(({ signal }) => this.forwardInputAudioToStt(pipeline, signal));
    this.sttForwardTask.result.catch((err) => {
      this.logger.error(`Error forwarding audio to STT pipeline: ${err}`);
    });
    this.sttConsumerTask = import_utils.Task.from(({ signal }) => this.consumeSttEvents(pipeline, signal));
    this.sttConsumerTask.result.catch((err) => {
      this.logger.error(`Error running STT task: ${err}`);
    });
  }
  async stopSttTasks() {
    var _a, _b;
    await ((_a = this.sttConsumerTask) == null ? void 0 : _a.cancelAndWait());
    this.sttConsumerTask = void 0;
    await ((_b = this.sttForwardTask) == null ? void 0 : _b.cancelAndWait());
    this.sttForwardTask = void 0;
  }
  async detachSttPipeline() {
    var _a;
    const unlock = await this.sttLifecycleLock.lock();
    try {
      const pipeline = this.sttPipeline;
      this.sttPipeline = void 0;
      this.sttOwnershipTransferred = pipeline !== void 0;
      await ((_a = this.sttConsumerTask) == null ? void 0 : _a.cancelAndWait());
      this.sttConsumerTask = void 0;
      return pipeline;
    } finally {
      unlock();
    }
  }
  async forwardInputAudioToStt(pipeline, signal) {
    for await (const frame of (0, import_utils.readStream)(this.sttInputStream, signal)) {
      await pipeline.audioChannel.write(frame);
    }
  }
  async consumeSttEvents(pipeline, signal) {
    for await (const ev of (0, import_utils.readStream)(pipeline.eventChannel.stream(), signal)) {
      await this.onSTTEvent(ev);
    }
  }
  async createVadTask(vad, signal) {
    var _a, _b, _c;
    if (!vad) return;
    const vadStream = vad.stream();
    this.vadStream = vadStream;
    vadStream.updateInputStream(this.vadInputStream);
    const abortHandler = () => {
      vadStream.detachInputStream();
      vadStream.close();
      signal.removeEventListener("abort", abortHandler);
    };
    signal.addEventListener("abort", abortHandler);
    try {
      for await (const ev of vadStream) {
        if (signal.aborted) break;
        switch (ev.type) {
          case import_vad.VADEventType.START_OF_SPEECH:
            this.logger.debug("VAD task: START_OF_SPEECH");
            {
              const startTime = Date.now() - ev.speechDuration - ev.inferenceDuration;
              if (!this.vadSpeechStarted) {
                this.speechStartTime = startTime;
                this.vadSpeechStarted = true;
              }
              const span = this.ensureUserTurnSpan(startTime);
              const ctx = this.userTurnContext(span);
              this.endpointing.onStartOfSpeech(startTime, this.isAgentSpeaking);
              this.interruptionDetected = void 0;
              import_api.context.with(ctx, () => this.hooks.onStartOfSpeech(ev));
            }
            this.speaking = true;
            this.userSpeakingEvent.set();
            (_a = this.turnDetectorStream) == null ? void 0 : _a.cancelInference();
            this.turnDetectorPredictionFut = void 0;
            this.turnDetectorFlushed = false;
            if (ev.frames.length > 0 && ev.frames[0]) {
              this.sampleRate = ev.frames[0].sampleRate;
            }
            (_b = this.bounceEOUTask) == null ? void 0 : _b.cancel();
            break;
          case import_vad.VADEventType.INFERENCE_DONE:
            this.hooks.onVADInferenceDone(ev);
            if (ev.rawAccumulatedSpeech > 0) {
              this.lastSpeakingTime = Date.now();
              if (this.speechStartTime === void 0) {
                this.speechStartTime = Date.now() - ev.rawAccumulatedSpeech;
              }
              this.userSpeakingEvent.set();
              if (this.speaking && this.turnDetectorPredictionFut !== void 0) {
                (_c = this.turnDetectorStream) == null ? void 0 : _c.cancelInference();
                this.turnDetectorPredictionFut = void 0;
              }
            } else if (!this.speaking) {
              this.userSpeakingEvent.clear();
            }
            if (ev.rawAccumulatedSilence >= import_base.MIN_SILENCE_DURATION_MS && this.speaking && this.turnDetectorStream !== void 0 && this.turnDetectorPredictionFut === void 0) {
              this.turnDetectorPredictionFut = this.turnDetectorStream.predict();
            }
            break;
          case import_vad.VADEventType.END_OF_SPEECH:
            this.logger.debug("VAD task: END_OF_SPEECH");
            {
              const endTime = Date.now() - ev.silenceDuration - ev.inferenceDuration;
              const span = this.ensureUserTurnSpan();
              const ctx = this.userTurnContext(span);
              if (this.speaking) {
                this.endpointing.onEndOfSpeech(
                  endTime,
                  this.interruptionDetected === false && this.isAgentSpeaking
                );
              }
              import_api.context.with(ctx, () => this.hooks.onEndOfSpeech(ev));
            }
            this.vadSpeechStarted = false;
            this.speaking = false;
            this.userSpeakingEvent.clear();
            this.lastSpeakingTime = Date.now() - ev.silenceDuration - ev.inferenceDuration;
            if (this.vadBaseTurnDetection || this.turnDetectionMode === "stt" && this.userTurnCommitted) {
              const chatCtx = this.hooks.retrieveChatCtx();
              this.runEOUDetection(chatCtx, "vad");
            }
            break;
        }
      }
    } catch (e) {
      this.logger.error(e, "Error in VAD task");
    } finally {
      this.logger.debug("VAD task closed");
      if (this.vadStream === vadStream) {
        this.vadStream = void 0;
      }
    }
  }
  async createInterruptionTask(interruptionDetection, signal) {
    if (!interruptionDetection || !this.interruptionStreamChannel) return;
    let numRetries = 0;
    const maxRetries = import_defaults.apiConnectDefaults.maxRetries;
    while (!signal.aborted) {
      const stream = interruptionDetection.createStream();
      const eventReader = stream.stream().getReader();
      const cleanup = async () => {
        try {
          signal.removeEventListener("abort", cleanup);
          eventReader.releaseLock();
          await stream.close();
        } catch (e) {
          this.logger.debug("createInterruptionTask: error during cleanup:", e);
        }
      };
      signal.addEventListener("abort", cleanup, { once: true });
      let forwardTask;
      try {
        if (numRetries > 0 && this.isAgentSpeaking) {
          await stream.pushFrame(import_interruption_stream.InterruptionStreamSentinel.agentSpeechStarted());
        }
        forwardTask = (async () => {
          const inputReader = this.interruptionStreamChannel.stream().getReader();
          const abortPromise2 = (0, import_utils.waitForAbort)(signal);
          try {
            while (!signal.aborted) {
              const res = await import_throws.ThrowsPromise.race([inputReader.read(), abortPromise2]);
              if (!res) break;
              const { value, done } = res;
              if (done) break;
              if (value instanceof import_rtc_node.AudioFrame) {
                const frameDurationMs = value.samplesPerChannel / value.sampleRate * 1e3;
                this._inputStartedAt ??= Date.now() - frameDurationMs;
              } else {
                this._inputStartedAt ??= Date.now();
              }
              await stream.pushFrame(value);
            }
          } finally {
            inputReader.releaseLock();
          }
        })();
        const abortPromise = (0, import_utils.waitForAbort)(signal);
        while (!signal.aborted) {
          const res = await import_throws.ThrowsPromise.race([eventReader.read(), abortPromise]);
          if (!res) break;
          const { done, value: ev } = res;
          if (done) break;
          numRetries = 0;
          this.onOverlapSpeechEvent(ev);
        }
        break;
      } catch (e) {
        if (signal.aborted) break;
        if ((0, import_exceptions.isAPIError)(e)) {
          if (maxRetries === 0 || !e.retryable) {
            interruptionDetection.emitError(
              new import_errors.InterruptionDetectionError(
                e.message,
                Date.now(),
                interruptionDetection.label,
                false
              )
            );
            break;
          } else if (numRetries >= maxRetries) {
            interruptionDetection.emitError(
              new import_errors.InterruptionDetectionError(
                `failed to detect interruption after ${numRetries} attempts`,
                Date.now(),
                interruptionDetection.label,
                false
              )
            );
            break;
          } else {
            const retryInterval = (0, import_defaults.intervalForRetry)(numRetries);
            interruptionDetection.emitError(
              new import_errors.InterruptionDetectionError(
                e.message,
                Date.now(),
                interruptionDetection.label,
                true
              )
            );
            this.logger.warn(
              { model: interruptionDetection.label, attempt: numRetries },
              `failed to detect interruption, retrying in ${retryInterval}ms`
            );
            numRetries++;
            await (0, import_utils.delay)(retryInterval, { signal });
          }
        } else {
          const msg = e instanceof Error ? e.message : String(e);
          interruptionDetection.emitError(
            new import_errors.InterruptionDetectionError(msg, Date.now(), interruptionDetection.label, false)
          );
          this.logger.error(e, "Error in interruption task");
          break;
        }
      } finally {
        await cleanup();
        await (forwardTask == null ? void 0 : forwardTask.catch((e) => {
          this.logger.debug({ err: e }, "interruption task exited with error");
        }));
      }
    }
    this.logger.debug("Interruption task closed");
  }
  setInputAudioStream(audioStream) {
    this.deferredInputStream.setSource(audioStream);
  }
  detachInputAudioStream() {
    this.deferredInputStream.detachSource();
  }
  /**
   * Returns an independent ReadableStream of input audio frames. Each call
   * returns a fresh branch fed by the broadcast transform inserted into the
   * deferred input pipeline — frames written after the subscription point
   * will be delivered, but earlier frames are not replayed.
   *
   * Used by AMD when its private STT needs the same participant audio that
   * the pipeline is processing without consuming it (see python AMD
   * detector pushing audio into `_AMDClassifier.push_audio`).
   */
  subscribeAudioStream() {
    const transform = new import_identity_transform.IdentityTransform();
    const writer = transform.writable.getWriter();
    this.subscriberWriters.push(writer);
    writer.closed.catch(() => {
    }).finally(() => {
      const idx = this.subscriberWriters.indexOf(writer);
      if (idx >= 0) this.subscriberWriters.splice(idx, 1);
    });
    return transform.readable;
  }
  clearUserTurn() {
    var _a;
    this.audioTranscript = "";
    this.audioInterimTranscript = "";
    this.audioPreflightTranscript = "";
    this.finalTranscriptConfidence = [];
    this.lastFinalTranscriptTime = 0;
    this.speechStartTime = void 0;
    this.userTurnStart = void 0;
    this.lastSpeakingTime = void 0;
    this.vadSpeechStarted = false;
    this.speaking = false;
    this.userTurnCommitted = false;
    this.userTurnTracker = { words: 0, transcript: "" };
    this.userSpeakingEvent.clear();
    this.lastEmittedEotPrediction = void 0;
    if (this.turnDetectorStream !== void 0) {
      this.turnDetectorStream.flush("clear_user_turn");
      this.turnDetectorPredictionFut = void 0;
      this.turnDetectorFlushed = true;
    }
    if ((_a = this.userTurnSpan) == null ? void 0 : _a.isRecording()) {
      this.userTurnSpan.end();
    }
    this.userTurnSpan = void 0;
    this.sttRequestIds = [];
    const restartStt = async () => {
      var _a2;
      const unlock = await this.sttLifecycleLock.lock();
      try {
        if (!this.stt || this.sttOwnershipTransferred) {
          return;
        }
        await this.stopSttTasks();
        await ((_a2 = this.sttPipeline) == null ? void 0 : _a2.close());
        this.sttPipeline = void 0;
        if (this.sttOwnershipTransferred) {
          return;
        }
        this.startSttTasks();
      } finally {
        unlock();
      }
    };
    void restartStt().catch((err) => {
      this.logger.error(`Error resetting STT task: ${err}`);
    });
  }
  /**
   * Reset the VAD by restarting its task. This is needed when STT sends a premature
   * end-of-turn signal while the user is still speaking, so VAD can detect new speech
   * and trigger interruptions correctly.
   */
  resetVad() {
    var _a;
    if (!this.vad) return;
    (_a = this.vadTask) == null ? void 0 : _a.cancelAndWait().finally(() => {
      if (this.closed) return;
      this.vadTask = import_utils.Task.from(({ signal }) => this.createVadTask(this.vad, signal));
      this.vadTask.result.catch((err) => {
        this.logger.error(`Error running VAD task: ${err}`);
      });
    });
  }
  commitUserTurn(audioDetached) {
    var _a;
    const commitUserTurnTask = (delayDuration = 500) => async (controller) => {
      if (Date.now() - this.lastFinalTranscriptTime > delayDuration) {
        if (audioDetached && this.sampleRate !== void 0) {
          const silenceFrame = (0, import_utils2.createSilenceFrame)(delayDuration, this.sampleRate);
          this.silenceAudioWriter.write(silenceFrame);
        }
        await (0, import_utils.delay)(delayDuration, { signal: controller.signal });
      }
      if (this.audioInterimTranscript) {
        this.audioTranscript = `${this.audioTranscript} ${this.audioInterimTranscript}`.trim();
      }
      this.audioInterimTranscript = "";
      const chatCtx = this.hooks.retrieveChatCtx();
      this.logger.debug("running EOU detection on commitUserTurn");
      this.runEOUDetection(chatCtx, "manual");
      this.userTurnCommitted = true;
    };
    (_a = this.commitUserTurnTask) == null ? void 0 : _a.cancel();
    this.commitUserTurnTask = import_utils.Task.from(commitUserTurnTask());
    this.commitUserTurnTask.result.then(() => {
      this.logger.debug("User turn committed");
    }).catch((err) => {
      if (err instanceof Error && err.name === "AbortError") {
        this.logger.debug("User turn commit task cancelled");
        return;
      }
      this.logger.error(err, "Error in user turn commit task:");
    });
  }
  async close() {
    var _a, _b, _c, _d, _e;
    this.closed = true;
    this.detachInputAudioStream();
    this.silenceAudioWriter.releaseLock();
    await ((_a = this.commitUserTurnTask) == null ? void 0 : _a.cancelAndWait());
    await this.stopSttTasks();
    if (this.sttPipeline) {
      await this.sttPipeline.close();
      this.sttPipeline = void 0;
    }
    for (const writer of this.subscriberWriters) {
      try {
        await writer.close();
      } catch {
      }
    }
    this.subscriberWriters = [];
    await ((_b = this.vadTask) == null ? void 0 : _b.cancelAndWait());
    await ((_c = this.bounceEOUTask) == null ? void 0 : _c.cancelAndWait());
    await ((_d = this.interruptionTask) == null ? void 0 : _d.cancelAndWait());
    if (this.turnDetectorStream !== void 0) {
      const stream = this.turnDetectorStream;
      this.turnDetectorStream = void 0;
      await stream.aclose().catch(() => void 0);
    }
    await ((_e = this.interruptionStreamChannel) == null ? void 0 : _e.close());
    this.cancelBackchannelBoundary();
  }
  _endUserTurnSpan({
    transcript,
    confidence,
    transcriptionDelay,
    endOfUtteranceDelay
  }) {
    if (this.userTurnSpan) {
      this.userTurnSpan.setAttributes({
        [import_telemetry.traceTypes.ATTR_USER_TRANSCRIPT]: transcript,
        [import_telemetry.traceTypes.ATTR_TRANSCRIPT_CONFIDENCE]: confidence,
        [import_telemetry.traceTypes.ATTR_TRANSCRIPTION_DELAY]: transcriptionDelay,
        [import_telemetry.traceTypes.ATTR_END_OF_TURN_DELAY]: endOfUtteranceDelay
      });
      if (this.sttRequestIds.length) {
        this.userTurnSpan.setAttribute(import_telemetry.traceTypes.ATTR_PROVIDER_REQUEST_IDS, this.sttRequestIds);
      }
      this.userTurnSpan.end();
      this.userTurnSpan = void 0;
      this.userTurnStart = void 0;
    }
    this.sttRequestIds = [];
  }
  get vadBaseTurnDetection() {
    if (typeof this.turnDetectionMode === "object") {
      return false;
    }
    if (this.turnDetectionMode === void 0 || this.turnDetectionMode === "vad") {
      return true;
    }
    return false;
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  AudioRecognition,
  STTPipeline
});
//# sourceMappingURL=audio_recognition.cjs.map