"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 interruption_stream_exports = {};
__export(interruption_stream_exports, {
  InterruptionStreamBase: () => InterruptionStreamBase,
  InterruptionStreamSentinel: () => InterruptionStreamSentinel
});
module.exports = __toCommonJS(interruption_stream_exports);
var import_rtc_node = require("@livekit/rtc-node");
var import_web = require("stream/web");
var import_log = require("../../log.cjs");
var import_stream_channel = require("../../stream/stream_channel.cjs");
var import_telemetry = require("../../telemetry/index.cjs");
var import_defaults = require("./defaults.cjs");
var import_interruption_cache_entry = require("./interruption_cache_entry.cjs");
var import_types = require("./types.cjs");
var import_utils = require("./utils.cjs");
var import_ws_transport = require("./ws_transport.cjs");
class InterruptionStreamSentinel {
  static agentSpeechStarted() {
    return { type: "agent-speech-started" };
  }
  static agentSpeechEnded() {
    return { type: "agent-speech-ended" };
  }
  static overlapSpeechStarted(speechDuration, startedAt, userSpeakingSpan) {
    return { type: "overlap-speech-started", speechDuration, startedAt, userSpeakingSpan };
  }
  static overlapSpeechEnded(endedAt) {
    return { type: "overlap-speech-ended", endedAt };
  }
  static flush() {
    return { type: "flush" };
  }
}
function updateUserSpeakingSpan(span, entry) {
  span.setAttribute(
    import_telemetry.traceTypes.ATTR_IS_INTERRUPTION,
    (entry.isInterruption ?? false).toString().toLowerCase()
  );
  span.setAttribute(import_telemetry.traceTypes.ATTR_INTERRUPTION_PROBABILITY, entry.probability);
  span.setAttribute(import_telemetry.traceTypes.ATTR_INTERRUPTION_TOTAL_DURATION, entry.totalDurationInS);
  span.setAttribute(import_telemetry.traceTypes.ATTR_INTERRUPTION_PREDICTION_DURATION, entry.predictionDurationInS);
  span.setAttribute(import_telemetry.traceTypes.ATTR_INTERRUPTION_DETECTION_DELAY, entry.detectionDelayInS);
}
class InterruptionStreamBase {
  inputStream;
  eventStream;
  resampler;
  numRequests = 0;
  userSpeakingSpan;
  overlapSpeechStartedAt;
  options;
  apiOptions;
  model;
  logger = (0, import_log.log)();
  // Store reconnect function for WebSocket transport
  wsReconnect;
  wsClose;
  // Mutable transport options that can be updated via updateOptions()
  transportOptions;
  constructor(model, apiOptions) {
    this.inputStream = (0, import_stream_channel.createStreamChannel)();
    this.model = model;
    this.options = { ...model.options };
    this.apiOptions = { ...import_defaults.apiConnectDefaults, ...apiOptions };
    this.transportOptions = {
      baseUrl: this.options.baseUrl,
      apiKey: this.options.apiKey,
      apiSecret: this.options.apiSecret,
      sampleRate: this.options.sampleRate,
      threshold: this.options.threshold,
      minFrames: this.options.minFrames,
      timeout: this.options.inferenceTimeout,
      connectTimeout: this.apiOptions.timeout,
      maxRetries: this.apiOptions.maxRetries
    };
    this.eventStream = this.setupTransform();
  }
  /**
   * Update stream options. For WebSocket transport, this triggers a reconnection.
   */
  async updateOptions(options) {
    if (options.threshold !== void 0) {
      this.options.threshold = options.threshold;
      this.transportOptions.threshold = options.threshold;
    }
    if (options.minInterruptionDurationInS !== void 0) {
      this.options.minInterruptionDurationInS = options.minInterruptionDurationInS;
      this.options.minFrames = Math.ceil(options.minInterruptionDurationInS * import_defaults.FRAMES_PER_SECOND);
      this.transportOptions.minFrames = this.options.minFrames;
    }
    if (this.wsReconnect) {
      await this.wsReconnect();
    }
  }
  setupTransform() {
    let agentSpeechStarted = false;
    let startIdx = 0;
    let accumulatedSamples = 0;
    let overlapSpeechStarted = false;
    let overlapCount = 0;
    const cache = new import_utils.BoundedCache(10);
    const inferenceS16Data = new Int16Array(
      Math.ceil(this.options.maxAudioDurationInS * this.options.sampleRate)
    ).fill(0);
    const getState = () => ({
      overlapSpeechStarted,
      overlapSpeechStartedAt: this.overlapSpeechStartedAt,
      cache,
      overlapCount
    });
    const setState = (partial) => {
      if (partial.overlapSpeechStarted !== void 0) {
        overlapSpeechStarted = partial.overlapSpeechStarted;
      }
    };
    const handleSpanUpdate = (entry) => {
      if (this.userSpeakingSpan) {
        updateUserSpeakingSpan(this.userSpeakingSpan, entry);
        this.userSpeakingSpan = void 0;
      }
    };
    const onRequestSent = () => {
      this.numRequests++;
    };
    const getAndResetNumRequests = () => {
      const n = this.numRequests;
      this.numRequests = 0;
      return n;
    };
    const audioTransformer = new import_web.TransformStream(
      {
        transform: (chunk, controller) => {
          if (chunk instanceof import_rtc_node.AudioFrame) {
            if (!agentSpeechStarted) {
              return;
            }
            if (this.options.sampleRate !== chunk.sampleRate) {
              controller.error("the sample rate of the input frames must be consistent");
              this.logger.error("the sample rate of the input frames must be consistent");
              return;
            }
            const result = writeToInferenceS16Data(
              chunk,
              startIdx,
              inferenceS16Data,
              this.options.maxAudioDurationInS
            );
            startIdx = result.startIdx;
            accumulatedSamples += result.samplesWritten;
            if (accumulatedSamples >= Math.floor(this.options.detectionIntervalInS * this.options.sampleRate) && overlapSpeechStarted) {
              const audioSlice = inferenceS16Data.slice(0, startIdx);
              accumulatedSamples = 0;
              controller.enqueue(audioSlice);
            }
          } else if (chunk.type === "agent-speech-started") {
            this.logger.debug("agent speech started");
            agentSpeechStarted = true;
            overlapSpeechStarted = false;
            this.overlapSpeechStartedAt = void 0;
            accumulatedSamples = 0;
            overlapCount = 0;
            startIdx = 0;
            this.numRequests = 0;
            cache.clear();
          } else if (chunk.type === "agent-speech-ended") {
            this.logger.debug("agent speech ended");
            agentSpeechStarted = false;
            overlapSpeechStarted = false;
            this.overlapSpeechStartedAt = void 0;
            accumulatedSamples = 0;
            overlapCount = 0;
            startIdx = 0;
            this.numRequests = 0;
            cache.clear();
          } else if (chunk.type === "overlap-speech-started" && agentSpeechStarted) {
            this.overlapSpeechStartedAt = chunk.startedAt;
            this.userSpeakingSpan = chunk.userSpeakingSpan;
            this.logger.debug("overlap speech started, starting interruption inference");
            overlapSpeechStarted = true;
            accumulatedSamples = 0;
            overlapCount += 1;
            if (overlapCount <= 1) {
              const keepSize = Math.round(chunk.speechDuration / 1e3 * this.options.sampleRate) + Math.round(this.options.audioPrefixDurationInS * this.options.sampleRate);
              const shiftCount = Math.max(0, startIdx - keepSize);
              inferenceS16Data.copyWithin(0, shiftCount, startIdx);
              startIdx -= shiftCount;
            }
            cache.clear();
          } else if (chunk.type === "overlap-speech-ended") {
            this.logger.debug("overlap speech ended");
            if (overlapSpeechStarted) {
              this.userSpeakingSpan = void 0;
              let latestEntry = cache.pop(
                (entry) => entry.totalDurationInS !== void 0 && entry.totalDurationInS > 0
              );
              if (!latestEntry) {
                this.logger.debug("no request made for overlap speech");
                latestEntry = import_interruption_cache_entry.InterruptionCacheEntry.default();
              }
              const e = latestEntry ?? import_interruption_cache_entry.InterruptionCacheEntry.default();
              const event = {
                type: "overlapping_speech",
                detectedAt: chunk.endedAt,
                isInterruption: false,
                overlapStartedAt: this.overlapSpeechStartedAt,
                speechInput: e.speechInput,
                probabilities: e.probabilities,
                totalDurationInS: e.totalDurationInS,
                detectionDelayInS: e.detectionDelayInS,
                predictionDurationInS: e.predictionDurationInS,
                probability: e.probability,
                numRequests: getAndResetNumRequests()
              };
              controller.enqueue(event);
              overlapSpeechStarted = false;
              accumulatedSamples = 0;
            }
            this.overlapSpeechStartedAt = void 0;
          } else if (chunk.type === "flush") {
          }
        }
      },
      { highWaterMark: 32 },
      { highWaterMark: 32 }
    );
    const transportOptions = this.transportOptions;
    const wsResult = (0, import_ws_transport.createWsTransport)(
      transportOptions,
      getState,
      setState,
      handleSpanUpdate,
      onRequestSent,
      getAndResetNumRequests
    );
    const transport = wsResult.transport;
    this.wsReconnect = wsResult.reconnect;
    this.wsClose = wsResult.close;
    const eventEmitter = new import_web.TransformStream({
      transform: (chunk, controller) => {
        this.model.emit("overlapping_speech", chunk);
        const metrics = {
          type: "interruption_metrics",
          timestamp: chunk.detectedAt,
          totalDuration: chunk.totalDurationInS * 1e3,
          predictionDuration: chunk.predictionDurationInS * 1e3,
          detectionDelay: chunk.detectionDelayInS * 1e3,
          numInterruptions: chunk.isInterruption ? 1 : 0,
          numBackchannels: chunk.isInterruption ? 0 : 1,
          numRequests: chunk.numRequests,
          metadata: {
            modelProvider: this.model.provider,
            modelName: this.model.model
          }
        };
        this.model.emit("metrics_collected", metrics);
        controller.enqueue(chunk);
      }
    });
    return this.inputStream.stream().pipeThrough(audioTransformer).pipeThrough(transport).pipeThrough(eventEmitter);
  }
  ensureInputNotEnded() {
    if (this.inputStream.closed) {
      throw new Error("input stream is closed");
    }
  }
  ensureStreamsNotEnded() {
    this.ensureInputNotEnded();
  }
  getResamplerFor(inputSampleRate) {
    if (!this.resampler) {
      this.resampler = new import_rtc_node.AudioResampler(inputSampleRate, this.options.sampleRate);
    }
    return this.resampler;
  }
  stream() {
    return this.eventStream;
  }
  async pushFrame(frame) {
    this.ensureStreamsNotEnded();
    if (!(frame instanceof import_rtc_node.AudioFrame)) {
      return this.inputStream.write(frame);
    } else if (this.options.sampleRate !== frame.sampleRate) {
      const resampler = this.getResamplerFor(frame.sampleRate);
      if (resampler.inputRate !== frame.sampleRate) {
        throw new Error("the sample rate of the input frames must be consistent");
      }
      for (const resampledFrame of resampler.push(frame)) {
        await this.inputStream.write(resampledFrame);
      }
    } else {
      await this.inputStream.write(frame);
    }
  }
  async flush() {
    this.ensureStreamsNotEnded();
    await this.inputStream.write(InterruptionStreamSentinel.flush());
  }
  async endInput() {
    await this.flush();
    await this.inputStream.close();
  }
  async close() {
    var _a, _b;
    try {
      if (!this.inputStream.closed) await this.inputStream.close();
    } finally {
      (_a = this.wsClose) == null ? void 0 : _a.call(this);
      (_b = this.resampler) == null ? void 0 : _b.close();
      this.model.removeStream(this);
    }
  }
}
function writeToInferenceS16Data(frame, startIdx, outData, maxAudioDuration) {
  const maxWindowSize = Math.floor(maxAudioDuration * frame.sampleRate);
  if (frame.samplesPerChannel > outData.length) {
    throw new Error("frame samples are greater than the max window size");
  }
  const shift = startIdx + frame.samplesPerChannel - maxWindowSize;
  if (shift > 0) {
    outData.copyWithin(0, shift, startIdx);
    startIdx -= shift;
  }
  const frameData = new Int16Array(
    frame.data.buffer,
    frame.data.byteOffset,
    frame.samplesPerChannel * frame.channels
  );
  if (frame.channels > 1) {
    for (let i = 0; i < frame.samplesPerChannel; i++) {
      let sum = 0;
      for (let ch = 0; ch < frame.channels; ch++) {
        sum += frameData[i * frame.channels + ch] ?? 0;
      }
      outData[startIdx + i] = Math.floor(sum / frame.channels);
    }
  } else {
    outData.set(frameData, startIdx);
  }
  startIdx += frame.samplesPerChannel;
  return { startIdx, samplesWritten: frame.samplesPerChannel };
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  InterruptionStreamBase,
  InterruptionStreamSentinel
});
//# sourceMappingURL=interruption_stream.cjs.map