"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 synchronizer_exports = {};
__export(synchronizer_exports, {
  SpeakingRateData: () => SpeakingRateData,
  TranscriptionSynchronizer: () => TranscriptionSynchronizer,
  defaultTextSyncOptions: () => defaultTextSyncOptions
});
module.exports = __toCommonJS(synchronizer_exports);
var import_log = require("../../log.cjs");
var import_identity_transform = require("../../stream/identity_transform.cjs");
var import_tokenize = require("../../tokenize/index.cjs");
var import_utils = require("../../utils.cjs");
var import_io = require("../io.cjs");
const STANDARD_SPEECH_RATE = 3.83;
class SpeakingRateData {
  /** Timestamps of the speaking rate. */
  timestamps = [];
  /** Speed at the timestamp. */
  speakingRate = [];
  /** Accumulated speaking units up to the timestamp. */
  speakIntegrals = [];
  /** Buffer for text without timing annotations yet. */
  textBuffer = [];
  /**
   * Add by speaking rate estimation.
   */
  addByRate(timestamp, speakingRate) {
    const integral = this.speakIntegrals.length > 0 ? this.speakIntegrals[this.speakIntegrals.length - 1] : 0;
    const dt = timestamp - this.pushedDuration;
    const newIntegral = integral + speakingRate * dt;
    this.timestamps.push(timestamp);
    this.speakingRate.push(speakingRate);
    this.speakIntegrals.push(newIntegral);
  }
  /**
   * Add annotation from TimedString with start_time/end_time.
   */
  addByAnnotation(text, startTime, endTime) {
    if (startTime !== void 0) {
      const integral = this.speakIntegrals.length > 0 ? this.speakIntegrals[this.speakIntegrals.length - 1] : 0;
      const dt = startTime - this.pushedDuration;
      const textLen = this.textBuffer.reduce((sum, t) => sum + t.length, 0);
      const newIntegral = integral + textLen;
      const rate = dt > 0 ? textLen / dt : 0;
      this.timestamps.push(startTime);
      this.speakingRate.push(rate);
      this.speakIntegrals.push(newIntegral);
      this.textBuffer = [];
    }
    this.textBuffer.push(text);
    if (endTime !== void 0) {
      this.addByAnnotation("", endTime, void 0);
    }
  }
  /**
   * Get accumulated speaking units up to the given timestamp.
   */
  accumulateTo(timestamp) {
    if (this.timestamps.length === 0) {
      return 0;
    }
    let idx = 0;
    for (let i = 0; i < this.timestamps.length; i++) {
      if (this.timestamps[i] <= timestamp) {
        idx = i + 1;
      } else {
        break;
      }
    }
    if (idx === 0) {
      return 0;
    }
    let integralT = this.speakIntegrals[idx - 1];
    const dt = timestamp - this.timestamps[idx - 1];
    const rate = idx < this.speakingRate.length ? this.speakingRate[idx] : this.speakingRate[idx - 1];
    integralT += rate * dt;
    if (idx < this.timestamps.length) {
      integralT = Math.min(integralT, this.speakIntegrals[idx]);
    }
    return integralT;
  }
  /** Get the last pushed timestamp. */
  get pushedDuration() {
    return this.timestamps.length > 0 ? this.timestamps[this.timestamps.length - 1] : 0;
  }
}
class SegmentSynchronizerImpl {
  constructor(options, nextInChain, seedFromPushAudio = false) {
    this.options = options;
    this.nextInChain = nextInChain;
    this.seedFromPushAudio = seedFromPushAudio;
    this.enabled = options.enabled;
    this.speed = options.speed * STANDARD_SPEECH_RATE;
    this.textData = {
      wordStream: options.wordTokenizer.stream(),
      pushedText: "",
      done: false,
      forwardedHyphens: 0,
      forwardedText: ""
    };
    this.audioData = {
      pushedDuration: 0,
      done: false,
      annotatedRate: null
    };
    this.outputStream = new import_identity_transform.IdentityTransform();
    this.outputStreamWriter = this.outputStream.writable.getWriter();
    this.outputEnabledFuture.resolve();
    if (this.enabled) {
      this.mainTask().then(() => {
        this.outputStreamWriter.close();
      }).catch((error) => {
        this.logger.error({ error }, "mainTask SegmentSynchronizerImpl");
      });
    }
    this.captureTask = this.captureTaskImpl();
  }
  enabled;
  textData;
  audioData;
  speed;
  // Emit TimedString objects so downstream outputs (e.g. RoomIO's json_format) can
  // attach `end_time` reflecting synchronized playback timing.
  outputStream;
  outputStreamWriter;
  captureTask;
  startWallTime;
  startFuture = new import_utils.Future();
  closedFuture = new import_utils.Future();
  playbackCompleted = false;
  interrupted = false;
  pausedWallTime;
  /** Accumulated paused time in milliseconds; subtracted from wall-clock elapsed. */
  pausedDuration = 0;
  /** Gate held closed while paused; the main task awaits it between words. */
  outputEnabledFuture = new import_utils.Future();
  logger = (0, import_log.log)();
  get closed() {
    return this.closedFuture.done;
  }
  get audioInputEnded() {
    return this.audioData.done;
  }
  get textInputEnded() {
    return this.textData.done;
  }
  get hasPendingText() {
    return this.textData.pushedText.length > this.textData.forwardedText.length;
  }
  get readable() {
    return this.outputStream.readable;
  }
  onPlaybackStarted(startTime) {
    if (this.closed) {
      this.logger.warn("SegmentSynchronizerImpl.onPlaybackStarted called after close");
      return;
    }
    if (this.startFuture.done) {
      this.logger.warn("SegmentSynchronizerImpl.onPlaybackStarted called after startFuture is set");
      return;
    }
    this.startWallTime = startTime;
    this.startFuture.resolve();
  }
  pushAudio(frame) {
    if (this.closed) {
      this.logger.warn("SegmentSynchronizerImpl.pushAudio called after close");
      return;
    }
    const frameDuration = frame.samplesPerChannel / frame.sampleRate;
    if (this.seedFromPushAudio && !this.startFuture.done && frameDuration > 0) {
      this.startWallTime = Date.now();
      this.startFuture.resolve();
    }
    this.audioData.pushedDuration += frameDuration;
  }
  endAudioInput() {
    if (this.closed) {
      this.logger.warn("SegmentSynchronizerImpl.endAudioInput called after close");
      return;
    }
    this.audioData.done = true;
  }
  pushText(text) {
    if (this.closed) {
      this.logger.warn("SegmentSynchronizerImpl.pushText called after close");
      return;
    }
    const textStr = (0, import_io.isTimedString)(text) ? text.text : text;
    let startTime;
    let endTime;
    if ((0, import_io.isTimedString)(text)) {
      startTime = text.startTime;
      endTime = text.endTime;
      if (!this.audioData.annotatedRate) {
        this.audioData.annotatedRate = new SpeakingRateData();
      }
      this.audioData.annotatedRate.addByAnnotation(textStr, startTime, endTime);
    }
    this.textData.wordStream.pushText(textStr);
    this.textData.pushedText += textStr;
  }
  endTextInput() {
    if (this.closed) {
      this.logger.warn("SegmentSynchronizerImpl.endTextInput called after close");
      return;
    }
    this.textData.done = true;
    if (!this.enabled) {
      this.outputStreamWriter.close();
    } else {
      this.textData.wordStream.endInput();
    }
  }
  pause() {
    if (this.closed) {
      this.logger.warn("SegmentSynchronizerImpl.pause called after close");
      return;
    }
    if (this.pausedWallTime === void 0) {
      this.pausedWallTime = Date.now();
    }
    if (this.outputEnabledFuture.done) {
      this.outputEnabledFuture = new import_utils.Future();
    }
  }
  resume() {
    if (this.closed) {
      this.logger.warn("SegmentSynchronizerImpl.resume called after close");
      return;
    }
    if (this.pausedWallTime !== void 0) {
      if (this.startWallTime !== void 0) {
        this.pausedDuration += Date.now() - Math.max(this.startWallTime, this.pausedWallTime);
      }
      this.pausedWallTime = void 0;
    }
    if (!this.outputEnabledFuture.done) {
      this.outputEnabledFuture.resolve();
    }
  }
  markPlaybackFinished(_playbackPosition, interrupted) {
    if (this.closed) {
      this.logger.warn("SegmentSynchronizerImpl.markPlaybackFinished called after close");
      return;
    }
    this.interrupted = interrupted;
    if (!this.textData.done || !this.audioData.done) {
      this.logger.warn(
        { textDone: this.textData.done, audioDone: this.audioData.done },
        "SegmentSynchronizerImpl.markPlaybackFinished called before text/audio input is done"
      );
      if (!interrupted) {
        this.playbackCompleted = true;
      }
      return;
    }
    if (!interrupted) {
      this.playbackCompleted = true;
    }
  }
  get synchronizedTranscript() {
    if (this.playbackCompleted) {
      return this.textData.pushedText;
    }
    return this.textData.forwardedText;
  }
  async captureTaskImpl() {
    const reader = this.outputStream.readable.getReader();
    while (true) {
      const { done, value: text } = await reader.read();
      if (done) {
        break;
      }
      await this.nextInChain.captureText(text);
    }
    reader.releaseLock();
    this.nextInChain.flush();
  }
  async mainTask() {
    await this.startFuture.await;
    if (this.closed && !this.playbackCompleted) {
      return;
    }
    if (!this.startWallTime) {
      throw new Error("startWallTime is not set when starting SegmentSynchronizerImpl.mainTask");
    }
    let pushedTextCursor = 0;
    for await (const wordToken of this.textData.wordStream) {
      const word = wordToken.token;
      if (!this.outputEnabledFuture.done) {
        await this.outputEnabledFuture.await;
        if (this.interrupted) {
          return;
        }
      }
      if (this.closed && !this.playbackCompleted) {
        return;
      }
      const wordIdx = this.textData.pushedText.indexOf(word, pushedTextCursor);
      if (wordIdx === -1) {
        continue;
      }
      let wordEnd = wordIdx + word.length;
      while (wordEnd < this.textData.pushedText.length && !/\s/.test(this.textData.pushedText[wordEnd])) {
        wordEnd++;
      }
      const forwardedWord = this.textData.pushedText.slice(pushedTextCursor, wordEnd);
      pushedTextCursor = wordEnd;
      if (this.playbackCompleted) {
        this.outputStreamWriter.write(
          (0, import_io.createTimedString)({
            text: forwardedWord,
            endTime: this.synchronizedElapsedSeconds()
          })
        );
        const cleanWords2 = this.options.splitWords(word);
        const cleanWord2 = cleanWords2.length > 0 ? cleanWords2[0][0] : word;
        this.textData.forwardedHyphens += this.options.hyphenateWord(cleanWord2).length;
        this.textData.forwardedText += forwardedWord;
        continue;
      }
      const cleanWords = this.options.splitWords(word);
      const cleanWord = cleanWords.length > 0 ? cleanWords[0][0] : word;
      const wordHyphens = this.options.hyphenateWord(cleanWord).length;
      const elapsedSeconds = this.synchronizedElapsedSeconds();
      let dHyphens = 0;
      const annotated = this.audioData.annotatedRate;
      if (annotated && annotated.pushedDuration >= elapsedSeconds) {
        const targetLen = Math.floor(annotated.accumulateTo(elapsedSeconds));
        const forwardedLen = this.textData.forwardedText.length;
        if (targetLen >= forwardedLen) {
          const dText = this.textData.pushedText.slice(forwardedLen, targetLen);
          dHyphens = this.calcHyphens(dText).length;
        } else {
          const dText = this.textData.pushedText.slice(targetLen, forwardedLen);
          dHyphens = -this.calcHyphens(dText).length;
        }
      } else {
        const targetHyphens = elapsedSeconds * this.speed;
        dHyphens = Math.max(0, targetHyphens - this.textData.forwardedHyphens);
      }
      let delayTime = Math.max(0, wordHyphens - dHyphens) / this.speed;
      if (this.playbackCompleted) {
        delayTime = 0;
      }
      await this.sleepIfNotClosed(delayTime / 2);
      this.outputStreamWriter.write(
        (0, import_io.createTimedString)({
          text: forwardedWord,
          endTime: this.synchronizedElapsedSeconds()
        })
      );
      await this.sleepIfNotClosed(delayTime / 2);
      this.textData.forwardedHyphens += wordHyphens;
      this.textData.forwardedText += forwardedWord;
    }
    if (pushedTextCursor < this.textData.pushedText.length) {
      const remaining = this.textData.pushedText.slice(pushedTextCursor);
      this.outputStreamWriter.write(
        (0, import_io.createTimedString)({
          text: remaining,
          endTime: this.synchronizedElapsedSeconds()
        })
      );
      this.textData.forwardedText += remaining;
    }
  }
  /** Elapsed audio-playback time in seconds — wall-clock minus paused time. */
  synchronizedElapsedSeconds() {
    if (this.startWallTime === void 0) {
      return void 0;
    }
    return (Date.now() - this.startWallTime - this.pausedDuration) / 1e3;
  }
  calcHyphens(text) {
    const words = this.options.splitWords(text);
    const hyphens = [];
    for (const [word] of words) {
      hyphens.push(...this.options.hyphenateWord(word));
    }
    return hyphens;
  }
  async sleepIfNotClosed(sleepTimeSeconds) {
    if (this.closed) {
      return;
    }
    await (0, import_utils.delay)(sleepTimeSeconds * 1e3);
  }
  async close() {
    if (this.closed) {
      return;
    }
    this.closedFuture.resolve();
    if (this.startWallTime === void 0) {
      this.startWallTime = Date.now();
    }
    this.startFuture.resolve();
    if (!this.outputEnabledFuture.done) {
      this.outputEnabledFuture.resolve();
    }
    if (!this.enabled && !this.textData.done) {
      this.outputStreamWriter.close();
    }
    this.textData.wordStream.close();
    await this.captureTask;
  }
}
const defaultTextSyncOptions = {
  speed: 1,
  hyphenateWord: import_tokenize.basic.hyphenateWord,
  splitWords: import_tokenize.basic.splitWords,
  wordTokenizer: new import_tokenize.basic.WordTokenizer(false),
  enabled: true
};
class TranscriptionSynchronizer {
  audioOutput;
  textOutput;
  options;
  rotateSegmentTask;
  _outputsAttached = true;
  closed = false;
  /** @internal */
  _impl;
  /** @internal */
  _audioAttached = true;
  /** @internal */
  _textAttached = true;
  // warn once per enabled cycle when only one of audio/text is detached; reset when
  // the synchronizer transitions back to enabled
  /** @internal */
  _warnedAsymmetricDetach = false;
  /** Pause state tracked at the synchronizer level so it can be reapplied across segment rotations.
   * @internal */
  _paused = false;
  logger = (0, import_log.log)();
  constructor(nextInChainAudio, nextInChainText, options = defaultTextSyncOptions) {
    this.audioOutput = new SyncedAudioOutput(this, nextInChainAudio);
    this.textOutput = new SyncedTextOutput(this, nextInChainText);
    this.options = {
      speed: options.speed,
      hyphenateWord: options.hyphenateWord,
      splitWords: options.splitWords,
      wordTokenizer: options.wordTokenizer,
      enabled: options.enabled
    };
    this._impl = new SegmentSynchronizerImpl(this.options, nextInChainText);
    this.rotateSegmentTask = import_utils.Task.from(
      (controller) => this.rotateSegmentTaskImpl(controller.signal)
    );
  }
  get outputsAttached() {
    return this._outputsAttached;
  }
  get enabled() {
    return this.options.enabled;
  }
  set enabled(value) {
    if (this.options.enabled === value) {
      return;
    }
    this.options.enabled = value;
    this.rotateSegment();
  }
  /** @internal */
  _onAttachmentChanged(args) {
    if (args.audioAttached !== void 0) {
      this._audioAttached = args.audioAttached;
    }
    if (args.textAttached !== void 0) {
      this._textAttached = args.textAttached;
    }
    const outputsAttached = this._audioAttached && this._textAttached;
    if (this._outputsAttached === outputsAttached) {
      return;
    }
    this._outputsAttached = outputsAttached;
    if (outputsAttached) {
      this._warnedAsymmetricDetach = false;
    }
    this.rotateSegment();
  }
  rotateSegment() {
    if (this.closed) {
      return;
    }
    if (!this.rotateSegmentTask.done) {
      this.logger.warn("rotateSegment called while previous segment is still being rotated");
    }
    this.rotateSegmentTask = import_utils.Task.from(
      (controller) => this.rotateSegmentTaskImpl(controller.signal, this.rotateSegmentTask)
    );
  }
  async close() {
    this.closed = true;
    await this.rotateSegmentTask.cancelAndWait();
    await this._impl.close();
  }
  async barrier() {
    if (this.rotateSegmentTask.done) {
      return;
    }
    await this.rotateSegmentTask.result;
  }
  async rotateSegmentTaskImpl(abort, oldTask) {
    if (oldTask) {
      await oldTask.result;
    }
    if (abort.aborted) {
      return;
    }
    await this._impl.close();
    this._impl = new SegmentSynchronizerImpl(this.options, this.textOutput.nextInChain, true);
    if (this._paused) {
      this._impl.pause();
    }
  }
}
class SyncedAudioOutput extends import_io.AudioOutput {
  constructor(synchronizer, nextInChainAudio) {
    super(nextInChainAudio.sampleRate, nextInChainAudio, { pause: true });
    this.synchronizer = synchronizer;
    this.nextInChainAudio = nextInChainAudio;
  }
  pushedDuration = 0;
  pause() {
    super.pause();
    this.synchronizer._paused = true;
    if (!this.synchronizer._impl.closed) {
      this.synchronizer._impl.pause();
    }
  }
  resume() {
    super.resume();
    this.synchronizer._paused = false;
    if (!this.synchronizer._impl.closed) {
      this.synchronizer._impl.resume();
    }
  }
  async captureFrame(frame) {
    await this.synchronizer.barrier();
    await super.captureFrame(frame);
    await this.nextInChainAudio.captureFrame(frame);
    this.pushedDuration += frame.samplesPerChannel / frame.sampleRate;
    if (!this.synchronizer.outputsAttached) {
      if (this.synchronizer._audioAttached && !this.synchronizer._textAttached && !this.synchronizer._warnedAsymmetricDetach) {
        this.synchronizer._warnedAsymmetricDetach = true;
        this.logger.warn(
          "TranscriptSynchronizer text output was detached while audio output is still active; transcription sync is disabled. This usually means session.output.transcription was replaced after AgentSession.start()."
        );
      }
      return;
    }
    if (!this.synchronizer.enabled) {
      return;
    }
    if (this.synchronizer._impl.audioInputEnded) {
      this.logger.warn(
        "SegmentSynchronizerImpl audio marked as ended in capture audio, rotating segment"
      );
      this.synchronizer.rotateSegment();
      await this.synchronizer.barrier();
    }
    this.synchronizer._impl.pushAudio(frame);
  }
  flush() {
    super.flush();
    this.nextInChainAudio.flush();
    if (!this.synchronizer.outputsAttached || !this.synchronizer.enabled) {
      return;
    }
    if (!this.pushedDuration) {
      if (this.synchronizer._impl.hasPendingText) {
        this.synchronizer._impl.endAudioInput();
        return;
      }
      this.synchronizer.rotateSegment();
      return;
    }
    this.synchronizer._impl.endAudioInput();
  }
  clearBuffer() {
    this.nextInChainAudio.clearBuffer();
  }
  async waitForPlayout() {
    const drift = this.pendingPlayoutSegments - this.nextInChainAudio.pendingPlayoutSegments;
    for (let i = 0; i < drift; i++) {
      this.onPlaybackFinished({ playbackPosition: 0, interrupted: true });
    }
    return super.waitForPlayout();
  }
  // this is going to be automatically called by the next_in_chain
  onPlaybackStarted(createdAt) {
    super.onPlaybackStarted(createdAt);
    if (this.synchronizer.outputsAttached && this.synchronizer.enabled) {
      this.synchronizer._impl.onPlaybackStarted(createdAt);
    }
  }
  // this is going to be automatically called by the next_in_chain
  onPlaybackFinished(ev) {
    if (!this.synchronizer.outputsAttached || !this.synchronizer.enabled) {
      super.onPlaybackFinished(ev);
      return;
    }
    this.synchronizer._impl.markPlaybackFinished(ev.playbackPosition, ev.interrupted);
    super.onPlaybackFinished({
      playbackPosition: ev.playbackPosition,
      interrupted: ev.interrupted,
      synchronizedTranscript: this.synchronizer._impl.synchronizedTranscript
    });
    this.synchronizer.rotateSegment();
    this.pushedDuration = 0;
  }
  onAttached() {
    super.onAttached();
    this.synchronizer._onAttachmentChanged({ audioAttached: true });
  }
  onDetached() {
    super.onDetached();
    this.synchronizer._onAttachmentChanged({ audioAttached: false });
  }
}
class SyncedTextOutput extends import_io.TextOutput {
  constructor(synchronizer, nextInChain) {
    super(nextInChain);
    this.synchronizer = synchronizer;
    this.nextInChain = nextInChain;
  }
  capturing = false;
  logger = (0, import_log.log)();
  async captureText(text) {
    await this.synchronizer.barrier();
    const textStr = (0, import_io.isTimedString)(text) ? text.text : text;
    if (!this.synchronizer.enabled) {
      await this.nextInChain.captureText(textStr);
      return;
    }
    if (!this.synchronizer.outputsAttached) {
      if (this.synchronizer._textAttached && !this.synchronizer._audioAttached && !this.synchronizer._warnedAsymmetricDetach) {
        this.synchronizer._warnedAsymmetricDetach = true;
        this.logger.warn(
          "TranscriptSynchronizer audio output was detached while text output is still active; transcription sync is disabled. This usually means session.output.audio was replaced after AgentSession.start()."
        );
      }
      await this.nextInChain.captureText(textStr);
      return;
    }
    this.capturing = true;
    if (this.synchronizer._impl.textInputEnded) {
      this.logger.warn(
        "SegmentSynchronizerImpl text marked as ended in capture text, rotating segment"
      );
      this.synchronizer.rotateSegment();
      await this.synchronizer.barrier();
    }
    this.synchronizer._impl.pushText(text);
  }
  async flush() {
    await this.synchronizer.barrier();
    if (!this.synchronizer.enabled || !this.synchronizer.outputsAttached) {
      this.capturing = false;
      this.nextInChain.flush();
      return;
    }
    if (!this.capturing) {
      return;
    }
    this.capturing = false;
    this.synchronizer._impl.endTextInput();
  }
  onAttached() {
    super.onAttached();
    this.synchronizer._onAttachmentChanged({ textAttached: true });
  }
  onDetached() {
    super.onDetached();
    this.synchronizer._onAttachmentChanged({ textAttached: false });
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  SpeakingRateData,
  TranscriptionSynchronizer,
  defaultTextSyncOptions
});
//# sourceMappingURL=synchronizer.cjs.map