"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 tts_exports = {};
__export(tts_exports, {
  ChunkedStream: () => ChunkedStream,
  SynthesizeStream: () => SynthesizeStream,
  TTS: () => TTS
});
module.exports = __toCommonJS(tts_exports);
var import_throws = require("@livekit/throws-transformer/throws");
var import_node_events = require("node:events");
var import_exceptions = require("../_exceptions.cjs");
var import_log = require("../log.cjs");
var import_deferred_stream = require("../stream/deferred_stream.cjs");
var import_telemetry = require("../telemetry/index.cjs");
var import_types = require("../types.cjs");
var import_utils = require("../utils.cjs");
class TTS extends import_node_events.EventEmitter {
  #capabilities;
  #sampleRate;
  #numChannels;
  constructor(sampleRate, numChannels, capabilities) {
    super();
    this.#capabilities = capabilities;
    this.#sampleRate = sampleRate;
    this.#numChannels = numChannels;
  }
  /** Returns this TTS's capabilities */
  get capabilities() {
    return this.#capabilities;
  }
  /** Returns the sample rate of audio frames returned by this TTS */
  get sampleRate() {
    return this.#sampleRate;
  }
  /** Returns the channel count of audio frames returned by this TTS */
  get numChannels() {
    return this.#numChannels;
  }
  /**
   * Get the model name/identifier for this TTS instance.
   *
   * @returns The model name if available, "unknown" otherwise.
   *
   * @remarks
   * Plugins should override this property to provide their model information.
   */
  get model() {
    return "unknown";
  }
  /**
   * Get the provider name for this TTS instance.
   *
   * @returns The provider name if available, "unknown" otherwise.
   *
   * @remarks
   * Plugins should override this property to provide their provider information.
   */
  get provider() {
    return "unknown";
  }
  async close() {
    return;
  }
}
class SynthesizeStream {
  static FLUSH_SENTINEL = Symbol("FLUSH_SENTINEL");
  static END_OF_STREAM = Symbol("END_OF_STREAM");
  input = new import_utils.AsyncIterableQueue();
  queue = new import_utils.AsyncIterableQueue();
  output = new import_utils.AsyncIterableQueue();
  closed = false;
  connOptions;
  abortController = new AbortController();
  deferredInputStream;
  logger = (0, import_log.log)();
  #tts;
  #metricsPendingTexts = [];
  #metricsText = "";
  #monitorMetricsTask;
  #ttsRequestSpan;
  #inputTokens = 0;
  #outputTokens = 0;
  // Deduped provider-known segment ids for the current attempt.
  #providerRequestIds = [];
  // Current `tts_request_run` span – noteProviderRequestId() updates the
  // attribute on this span as new provider ids become known.
  #currentAttemptSpan;
  constructor(tts, connOptions = import_types.DEFAULT_API_CONNECT_OPTIONS) {
    this.#tts = tts;
    this.connOptions = connOptions;
    this.deferredInputStream = new import_deferred_stream.DeferredReadableStream();
    this.pumpInput();
    this.abortController.signal.addEventListener("abort", () => {
      this.deferredInputStream.detachSource();
      if (!this.input.closed) this.input.close();
      if (!this.output.closed) this.output.close();
      this.closed = true;
    });
    (0, import_utils.startSoon)(async () => {
      try {
        await this.mainTask();
      } catch {
      } finally {
        this.queue.close();
        if (this.#monitorMetricsTask) {
          await this.#monitorMetricsTask.catch(() => {
          });
        }
        if (!this.output.closed) {
          this.output.close();
        }
      }
    });
  }
  _mainTaskImpl = async (span) => {
    this.#ttsRequestSpan = span;
    span.setAttributes({
      [import_telemetry.traceTypes.ATTR_TTS_STREAMING]: true,
      [import_telemetry.traceTypes.ATTR_TTS_LABEL]: this.#tts.label
    });
    for (let i = 0; i < this.connOptions.maxRetry + 1; i++) {
      try {
        return await import_telemetry.tracer.startActiveSpan(
          async (attemptSpan) => {
            attemptSpan.setAttribute(import_telemetry.traceTypes.ATTR_RETRY_COUNT, i);
            this.#providerRequestIds = [];
            this.#currentAttemptSpan = attemptSpan;
            try {
              return await this.run();
            } catch (error) {
              (0, import_telemetry.recordException)(attemptSpan, (0, import_utils.toError)(error));
              throw error;
            } finally {
              this.#currentAttemptSpan = void 0;
            }
          },
          { name: "tts_request_run" }
        );
      } catch (error) {
        if (error instanceof import_exceptions.APIError) {
          const retryInterval = (0, import_types.intervalForRetry)(this.connOptions, i);
          if (this.connOptions.maxRetry === 0 || !error.retryable) {
            this.emitError({ error, recoverable: false });
            throw error;
          } else if (i === this.connOptions.maxRetry) {
            this.emitError({ error, recoverable: false });
            throw new import_exceptions.APIConnectionError({
              message: `failed to generate TTS completion after ${this.connOptions.maxRetry + 1} attempts`,
              options: { retryable: false }
            });
          } else {
            this.logger.warn(
              { tts: this.#tts.label, attempt: i + 1, error },
              `failed to synthesize speech, retrying in ${retryInterval}ms`
            );
          }
          if (retryInterval > 0) {
            await (0, import_utils.delay)(retryInterval);
          }
        } else {
          this.emitError({ error: (0, import_utils.toError)(error), recoverable: false });
          throw error;
        }
      }
    }
  };
  mainTask = async () => import_telemetry.tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {
    name: "tts_request",
    endOnExit: false
  });
  emitError({ error, recoverable }) {
    this.#tts.emit("error", {
      type: "tts_error",
      timestamp: Date.now(),
      label: this.#tts.label,
      error,
      recoverable
    });
  }
  /**
   * Record a provider-known id for this stream on the current `tts_request_run`
   * span.
   *
   * Exposed on the span as `lk.provider_request_ids` so users can correlate
   * traces with the provider's server-side logs for debugging. Plugins should
   * call this when the provider-known id becomes available – either the
   * outbound `context_id`/`segment_id` (echoed back by the provider) or a
   * `request_id` / `session_id` returned in a response message. Deduped
   * internally, so calling on every message is fine.
   */
  noteProviderRequestId(contextId) {
    if (!contextId || this.#providerRequestIds.includes(contextId)) {
      return;
    }
    this.#providerRequestIds.push(contextId);
    if (this.#currentAttemptSpan && this.#currentAttemptSpan.isRecording()) {
      this.#currentAttemptSpan.setAttribute(
        import_telemetry.traceTypes.ATTR_PROVIDER_REQUEST_IDS,
        this.#providerRequestIds
      );
    }
  }
  // NOTE(AJS-37): The implementation below uses an AsyncIterableQueue (`this.input`)
  // bridged from a DeferredReadableStream (`this.deferredInputStream`) rather than
  // consuming the stream directly.
  //
  // A full refactor to native Web Streams was considered but is currently deferred.
  //
  // For more context, see the discussion in GitHub issue # 844.
  async pumpInput() {
    const reader = this.deferredInputStream.stream.getReader();
    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done || value === SynthesizeStream.FLUSH_SENTINEL) {
          break;
        }
        this.pushText(value);
      }
      this.endInput();
    } catch (error) {
      this.logger.error(error, "Error reading deferred input stream");
    } finally {
      reader.releaseLock();
      if (!this.#monitorMetricsTask) {
        this.output.close();
      }
    }
  }
  /**
   * Set token usage for token-based TTS billing (e.g., OpenAI TTS).
   * Plugins should call this method to report token usage.
   */
  setTokenUsage({
    inputTokens = 0,
    outputTokens = 0
  } = {}) {
    this.#inputTokens = inputTokens;
    this.#outputTokens = outputTokens;
  }
  async monitorMetrics() {
    const startTime = process.hrtime.bigint();
    let audioDurationMs = 0;
    let ttfb = BigInt(-1);
    let requestId = "";
    const emit = () => {
      if (this.#metricsPendingTexts.length) {
        const text = this.#metricsPendingTexts.shift();
        const duration = process.hrtime.bigint() - startTime;
        const roundedAudioDurationMs = Math.round(audioDurationMs);
        const metrics = {
          type: "tts_metrics",
          timestamp: Date.now(),
          requestId,
          ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1e6))),
          durationMs: Math.trunc(Number(duration / BigInt(1e6))),
          charactersCount: text.length,
          audioDurationMs: roundedAudioDurationMs,
          cancelled: this.abortController.signal.aborted,
          label: this.#tts.label,
          inputTokens: this.#inputTokens,
          outputTokens: this.#outputTokens,
          streamed: true,
          metadata: {
            modelProvider: this.#tts.provider,
            modelName: this.#tts.model
          }
        };
        if (this.#ttsRequestSpan) {
          this.#ttsRequestSpan.setAttribute(import_telemetry.traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));
        }
        this.#tts.emit("metrics_collected", metrics);
        this.#inputTokens = 0;
        this.#outputTokens = 0;
      }
    };
    for await (const audio of this.queue) {
      if (this.abortController.signal.aborted) {
        break;
      }
      this.output.put(audio);
      if (audio === SynthesizeStream.END_OF_STREAM) continue;
      requestId = audio.requestId;
      if (audio.segmentId) {
        this.noteProviderRequestId(audio.segmentId);
      }
      if (ttfb === BigInt(-1)) {
        ttfb = process.hrtime.bigint() - startTime;
      }
      audioDurationMs += audio.frame.samplesPerChannel / audio.frame.sampleRate * 1e3;
      if (audio.final) {
        emit();
      }
    }
    if (requestId) {
      emit();
    }
    if (this.#ttsRequestSpan) {
      this.#ttsRequestSpan.end();
      this.#ttsRequestSpan = void 0;
    }
  }
  updateInputStream(text) {
    this.deferredInputStream.setSource(text);
  }
  /** Push a string of text to the TTS */
  /** @deprecated Use `updateInputStream` instead */
  pushText(text) {
    if (!this.#monitorMetricsTask) {
      this.#monitorMetricsTask = this.monitorMetrics();
      this.#monitorMetricsTask.finally(() => this.output.close());
    }
    this.#metricsText += text;
    if (this.input.closed || this.closed) {
      return;
    }
    this.input.put(text);
  }
  /** Flush the TTS, causing it to process all pending text */
  flush() {
    if (this.#metricsText) {
      this.#metricsPendingTexts.push(this.#metricsText);
      this.#metricsText = "";
    }
    if (this.input.closed || this.closed) {
      return;
    }
    this.input.put(SynthesizeStream.FLUSH_SENTINEL);
  }
  /** Mark the input as ended and forbid additional pushes */
  endInput() {
    this.flush();
    if (this.input.closed || this.closed) {
      return;
    }
    this.input.close();
  }
  next() {
    return this.output.next();
  }
  get abortSignal() {
    return this.abortController.signal;
  }
  /** Close both the input and output of the TTS stream */
  close() {
    this.abortController.abort();
  }
  [Symbol.asyncIterator]() {
    return this;
  }
}
class ChunkedStream {
  queue = new import_utils.AsyncIterableQueue();
  output = new import_utils.AsyncIterableQueue();
  closed = false;
  #text;
  #tts;
  #ttsRequestSpan;
  _connOptions;
  logger = (0, import_log.log)();
  #inputTokens = 0;
  #outputTokens = 0;
  abortController = new AbortController();
  constructor(text, tts, connOptions = import_types.DEFAULT_API_CONNECT_OPTIONS, abortSignal) {
    this.#text = text;
    this.#tts = tts;
    this._connOptions = connOptions;
    if (abortSignal) {
      abortSignal.addEventListener("abort", () => this.abortController.abort(), { once: true });
    }
    this.monitorMetrics();
    import_throws.ThrowsPromise.resolve().then(() => this.mainTask().finally(() => this.queue.close()));
  }
  _mainTaskImpl = async (span) => {
    this.#ttsRequestSpan = span;
    span.setAttributes({
      [import_telemetry.traceTypes.ATTR_TTS_STREAMING]: false,
      [import_telemetry.traceTypes.ATTR_TTS_LABEL]: this.#tts.label
    });
    for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {
      try {
        return await import_telemetry.tracer.startActiveSpan(
          async (attemptSpan) => {
            attemptSpan.setAttribute(import_telemetry.traceTypes.ATTR_RETRY_COUNT, i);
            try {
              return await this.run();
            } catch (error) {
              (0, import_telemetry.recordException)(attemptSpan, (0, import_utils.toError)(error));
              throw error;
            }
          },
          { name: "tts_request_run" }
        );
      } catch (error) {
        if (error instanceof import_exceptions.APIError) {
          const retryInterval = (0, import_types.intervalForRetry)(this._connOptions, i);
          if (this._connOptions.maxRetry === 0 || !error.retryable) {
            this.emitError({ error, recoverable: false });
            throw error;
          } else if (i === this._connOptions.maxRetry) {
            this.emitError({ error, recoverable: false });
            throw new import_exceptions.APIConnectionError({
              message: `failed to generate TTS completion after ${this._connOptions.maxRetry + 1} attempts`,
              options: { retryable: false }
            });
          } else {
            this.logger.warn(
              { tts: this.#tts.label, attempt: i + 1, error },
              `failed to generate TTS completion, retrying in ${retryInterval}ms`
            );
          }
          if (retryInterval > 0) {
            await (0, import_utils.delay)(retryInterval);
          }
        } else {
          this.emitError({ error: (0, import_utils.toError)(error), recoverable: false });
          throw error;
        }
      }
    }
  };
  async mainTask() {
    return import_telemetry.tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {
      name: "tts_request",
      endOnExit: false
    });
  }
  emitError({ error, recoverable }) {
    this.#tts.emit("error", {
      type: "tts_error",
      timestamp: Date.now(),
      label: this.#tts.label,
      error,
      recoverable
    });
  }
  get inputText() {
    return this.#text;
  }
  get abortSignal() {
    return this.abortController.signal;
  }
  /**
   * Set token usage for token-based TTS billing (e.g., OpenAI TTS).
   * Plugins should call this method to report token usage.
   */
  setTokenUsage({
    inputTokens = 0,
    outputTokens = 0
  } = {}) {
    this.#inputTokens = inputTokens;
    this.#outputTokens = outputTokens;
  }
  async monitorMetrics() {
    const startTime = process.hrtime.bigint();
    let audioDurationMs = 0;
    let ttfb = BigInt(-1);
    let requestId = "";
    for await (const audio of this.queue) {
      this.output.put(audio);
      requestId = audio.requestId;
      if (ttfb === BigInt(-1)) {
        ttfb = process.hrtime.bigint() - startTime;
      }
      audioDurationMs += audio.frame.samplesPerChannel / audio.frame.sampleRate * 1e3;
    }
    this.output.close();
    const duration = process.hrtime.bigint() - startTime;
    const metrics = {
      type: "tts_metrics",
      timestamp: Date.now(),
      requestId,
      ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1e6))),
      durationMs: Math.trunc(Number(duration / BigInt(1e6))),
      charactersCount: this.#text.length,
      audioDurationMs: Math.round(audioDurationMs),
      cancelled: false,
      // TODO(AJS-186): support ChunkedStream with 1.0 - add this.abortController.signal.aborted here
      label: this.#tts.label,
      inputTokens: this.#inputTokens,
      outputTokens: this.#outputTokens,
      streamed: false,
      metadata: {
        modelProvider: this.#tts.provider,
        modelName: this.#tts.model
      }
    };
    if (this.#ttsRequestSpan) {
      this.#ttsRequestSpan.setAttribute(import_telemetry.traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));
      this.#ttsRequestSpan.end();
      this.#ttsRequestSpan = void 0;
    }
    this.#tts.emit("metrics_collected", metrics);
  }
  /** Collect every frame into one in a single call */
  async collect() {
    const frames = [];
    for await (const event of this) {
      frames.push(event.frame);
    }
    return (0, import_utils.mergeFrames)(frames);
  }
  next() {
    return this.output.next();
  }
  /** Close both the input and output of the TTS stream */
  close() {
    if (!this.queue.closed) this.queue.close();
    if (!this.output.closed) this.output.close();
    if (!this.abortController.signal.aborted) this.abortController.abort();
    this.closed = true;
  }
  [Symbol.asyncIterator]() {
    return this;
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  ChunkedStream,
  SynthesizeStream,
  TTS
});
//# sourceMappingURL=tts.cjs.map