"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, {
  SynthesizeStream: () => SynthesizeStream,
  TTS: () => TTS,
  hasAlignedTranscript: () => hasAlignedTranscript,
  normalizeTTSFallback: () => normalizeTTSFallback,
  parseTTSModelString: () => parseTTSModelString
});
module.exports = __toCommonJS(tts_exports);
var import_throws = require("@livekit/throws-transformer/throws");
var import_ws = require("ws");
var import_exceptions = require("../_exceptions.cjs");
var import_audio = require("../audio.cjs");
var import_connection_pool = require("../connection_pool.cjs");
var import_language = require("../language.cjs");
var import_log = require("../log.cjs");
var import_stream_channel = require("../stream/stream_channel.cjs");
var import_tokenize = require("../tokenize/index.cjs");
var import_tts = require("../tts/index.cjs");
var import_types = require("../types.cjs");
var import_utils = require("../utils.cjs");
var import_io = require("../voice/io.cjs");
var import_api_protos = require("./api_protos.cjs");
var import_utils2 = require("./utils.cjs");
function parseTTSModelString(model) {
  const idx = model.lastIndexOf(":");
  if (idx !== -1) {
    return [model.slice(0, idx), model.slice(idx + 1)];
  }
  return [model, void 0];
}
function hasAlignedTranscript(model, modelOptions) {
  if (!model) return false;
  const provider = model.split("/")[0];
  const opts = modelOptions ?? {};
  if (provider === "cartesia") {
    return Boolean(opts.add_timestamps);
  }
  if (provider === "elevenlabs") {
    return Boolean(opts.sync_alignment);
  }
  if (provider === "inworld") {
    return opts.timestamp_type === "WORD" || opts.timestamp_type === "CHARACTER";
  }
  return false;
}
function normalizeTTSFallback(fallback) {
  const makeFallback = (model) => {
    if (typeof model === "string") {
      const [name, voice] = parseTTSModelString(model);
      return { model: name, voice: voice ?? "" };
    }
    return model;
  };
  if (Array.isArray(fallback)) {
    return fallback.map(makeFallback);
  }
  return [makeFallback(fallback)];
}
const DEFAULT_ENCODING = "pcm_s16le";
const DEFAULT_SAMPLE_RATE = 16e3;
const NUM_CHANNELS = 1;
const DEFAULT_LANGUAGE = "en";
class TTS extends import_tts.TTS {
  opts;
  streams = /* @__PURE__ */ new Set();
  pool;
  #logger = (0, import_log.log)();
  #alignedTranscript;
  constructor(opts) {
    const sampleRate = (opts == null ? void 0 : opts.sampleRate) ?? DEFAULT_SAMPLE_RATE;
    const initialAligned = hasAlignedTranscript(
      opts == null ? void 0 : opts.model,
      opts == null ? void 0 : opts.modelOptions
    );
    super(sampleRate, 1, { streaming: true, alignedTranscript: initialAligned });
    const {
      model,
      voice,
      language = DEFAULT_LANGUAGE,
      baseURL,
      encoding = DEFAULT_ENCODING,
      apiKey,
      apiSecret,
      modelOptions = {},
      fallback,
      connOptions
    } = opts || {};
    const lkBaseURL = baseURL || (0, import_utils2.getDefaultInferenceUrl)();
    const lkApiKey = apiKey || process.env.LIVEKIT_INFERENCE_API_KEY || process.env.LIVEKIT_API_KEY;
    if (!lkApiKey) {
      throw new Error("apiKey is required: pass apiKey or set LIVEKIT_API_KEY");
    }
    const lkApiSecret = apiSecret || process.env.LIVEKIT_INFERENCE_API_SECRET || process.env.LIVEKIT_API_SECRET;
    if (!lkApiSecret) {
      throw new Error("apiSecret is required: pass apiSecret or set LIVEKIT_API_SECRET");
    }
    let nextModel = model;
    let nextVoice = voice;
    if (typeof nextModel === "string") {
      const idx = nextModel.lastIndexOf(":");
      if (idx !== -1) {
        const voiceFromModel = nextModel.slice(idx + 1);
        if (nextVoice && nextVoice !== voiceFromModel) {
          this.#logger.warn(
            "`voice` is provided via both argument and model, using the one from the argument",
            { voice: nextVoice, model: nextModel }
          );
        } else {
          nextVoice = voiceFromModel;
        }
        nextModel = nextModel.slice(0, idx);
      }
    }
    const normalizedFallback = fallback ? normalizeTTSFallback(fallback) : void 0;
    this.opts = {
      model: nextModel,
      voice: nextVoice,
      language: (0, import_language.normalizeLanguage)(language),
      encoding,
      sampleRate,
      baseURL: lkBaseURL,
      apiKey: lkApiKey,
      apiSecret: lkApiSecret,
      modelOptions,
      fallback: normalizedFallback,
      connOptions: connOptions ?? import_types.DEFAULT_API_CONNECT_OPTIONS
    };
    this.#alignedTranscript = hasAlignedTranscript(
      nextModel,
      modelOptions
    );
    this.pool = new import_connection_pool.ConnectionPool({
      connectCb: (timeout) => this.connectWs(timeout),
      closeCb: (ws) => this.closeWs(ws),
      maxSessionDuration: 3e5,
      markRefreshedOnGet: true,
      connectTimeout: 1e4
      // 10 seconds default
    });
  }
  get label() {
    return "inference.TTS";
  }
  get model() {
    return this.opts.model ?? "unknown";
  }
  get provider() {
    return "livekit";
  }
  get capabilities() {
    return { streaming: true, alignedTranscript: this.#alignedTranscript };
  }
  static fromModelString(modelString) {
    const [model, voice] = parseTTSModelString(modelString);
    return new TTS({ model, voice: voice || void 0 });
  }
  updateOptions(opts) {
    const mergedModelOptions = opts.modelOptions !== void 0 ? { ...this.opts.modelOptions, ...opts.modelOptions } : this.opts.modelOptions;
    const sessionAffectingChange = opts.model !== void 0 || opts.voice !== void 0 || opts.language !== void 0 || opts.modelOptions !== void 0;
    this.opts = {
      ...this.opts,
      ...opts,
      modelOptions: mergedModelOptions,
      language: opts.language !== void 0 ? (0, import_language.normalizeLanguage)(opts.language) : this.opts.language
    };
    this.#alignedTranscript = hasAlignedTranscript(
      this.opts.model,
      this.opts.modelOptions
    );
    if (sessionAffectingChange) {
      this.pool.invalidate();
    }
    for (const stream of this.streams) {
      stream.updateOptions(this.opts);
    }
  }
  synthesize(_) {
    throw new Error("ChunkedStream is not implemented");
  }
  stream(options) {
    const { connOptions = this.opts.connOptions ?? import_types.DEFAULT_API_CONNECT_OPTIONS } = options || {};
    const stream = new SynthesizeStream(this, { ...this.opts }, connOptions);
    this.streams.add(stream);
    return stream;
  }
  async connectWs(timeout) {
    var _a;
    let baseURL = this.opts.baseURL;
    if (baseURL.startsWith("http://") || baseURL.startsWith("https://")) {
      baseURL = baseURL.replace("http", "ws");
    }
    const token = await (0, import_utils2.createAccessToken)(this.opts.apiKey, this.opts.apiSecret);
    const url = `${baseURL}/tts`;
    const headers = { Authorization: `Bearer ${token}` };
    const params = {
      type: "session.create",
      sample_rate: String(this.opts.sampleRate),
      encoding: this.opts.encoding,
      extra: this.opts.modelOptions
    };
    if (this.opts.voice) params.voice = this.opts.voice;
    if (this.opts.model) params.model = this.opts.model;
    if (this.opts.language) params.language = this.opts.language;
    if ((_a = this.opts.fallback) == null ? void 0 : _a.length) {
      params.fallback = {
        models: this.opts.fallback.map((m) => ({
          model: m.model,
          voice: m.voice,
          extra: m.extraKwargs ?? {}
        }))
      };
    }
    if (this.opts.connOptions) {
      params.connection = {
        timeout: this.opts.connOptions.timeoutMs / 1e3,
        retries: this.opts.connOptions.maxRetry
      };
    }
    this.#logger.debug({ url }, "inference.TTS creating new websocket connection (pool miss)");
    const socket = await (0, import_utils2.connectWs)(url, headers, timeout);
    socket.send(JSON.stringify(params));
    return socket;
  }
  async closeWs(ws) {
    await ws.close();
  }
  prewarm() {
    this.pool.prewarm();
  }
  async close() {
    for (const stream of this.streams) {
      await stream.close();
    }
    this.streams.clear();
    await this.pool.close();
  }
}
class SynthesizeStream extends import_tts.SynthesizeStream {
  opts;
  tts;
  #logger = (0, import_log.log)();
  constructor(tts, opts, connOptions) {
    super(tts, connOptions);
    this.opts = opts;
    this.tts = tts;
  }
  get label() {
    return "inference.SynthesizeStream";
  }
  updateOptions(opts) {
    const mergedModelOptions = opts.modelOptions !== void 0 ? { ...this.opts.modelOptions, ...opts.modelOptions } : this.opts.modelOptions;
    this.opts = {
      ...this.opts,
      ...opts,
      modelOptions: mergedModelOptions,
      language: opts.language !== void 0 ? (0, import_language.normalizeLanguage)(opts.language) : this.opts.language
    };
  }
  async run() {
    let closing = false;
    let lastFrame;
    let pendingTimedTranscripts = [];
    const sendTokenizerStream = new import_tokenize.basic.SentenceTokenizer().stream();
    const eventChannel = (0, import_stream_channel.createStreamChannel)();
    const requestId = (0, import_utils.shortuuid)("tts_request_");
    const inputSentEvent = new import_utils.Event();
    const completionFuture = new import_utils.Future();
    const resourceCleanup = async () => {
      if (closing) return;
      closing = true;
      sendTokenizerStream.close();
      await eventChannel.close();
    };
    const sendClientEvent = async (event, ws, signal) => {
      if (signal.aborted || closing) return;
      const validatedEvent = await import_api_protos.ttsClientEventSchema.parseAsync(event);
      if (ws.readyState !== import_ws.WebSocket.OPEN) {
        this.#logger.warn("Trying to send client TTS event to a closed WebSocket");
        return;
      }
      ws.send(JSON.stringify(validatedEvent));
    };
    const sendLastFrame = (segmentId, final) => {
      if (lastFrame) {
        this.queue.put({
          requestId,
          segmentId,
          frame: lastFrame,
          final,
          timedTranscripts: pendingTimedTranscripts.length > 0 ? pendingTimedTranscripts : void 0
        });
        lastFrame = void 0;
        pendingTimedTranscripts = [];
      }
    };
    const createInputTask = async (signal) => {
      for await (const data of this.input) {
        if (signal.aborted || closing) break;
        if (data === SynthesizeStream.FLUSH_SENTINEL) {
          sendTokenizerStream.flush();
          continue;
        }
        sendTokenizerStream.pushText(data);
      }
      if (!closing) {
        sendTokenizerStream.endInput();
      }
    };
    const createSentenceStreamTask = async (ws, signal) => {
      for await (const ev of sendTokenizerStream) {
        if (signal.aborted || closing) break;
        const generationConfig = {};
        if (this.opts.voice) generationConfig.voice = this.opts.voice;
        if (this.opts.model) generationConfig.model = this.opts.model;
        if (this.opts.language) generationConfig.language = this.opts.language;
        await sendClientEvent(
          {
            type: "input_transcript",
            transcript: ev.token + " ",
            generation_config: generationConfig,
            extra: this.opts.modelOptions ?? {}
          },
          ws,
          signal
        );
        inputSentEvent.set();
      }
      await sendClientEvent({ type: "session.flush" }, ws, signal);
      inputSentEvent.set();
    };
    const createWsListenerTask = async (ws, signal) => {
      const onMessage = (data) => {
        try {
          const eventJson = JSON.parse(data.toString());
          const validatedEvent = import_api_protos.ttsServerEventSchema.parse(eventJson);
          void eventChannel.write(validatedEvent).catch((error) => {
            this.#logger.debug(
              { error },
              "Failed writing TTS event to stream channel (likely closed)"
            );
          });
        } catch (e) {
          this.#logger.error({ error: e }, "Error parsing WebSocket message");
        }
      };
      const onError = (e) => {
        var _a;
        this.#logger.error({ error: e }, "WebSocket error");
        void resourceCleanup();
        try {
          (_a = ws.terminate) == null ? void 0 : _a.call(ws);
        } catch {
        }
        this.tts.pool.remove(ws);
        completionFuture.reject(e);
      };
      const onClose = () => {
        if (!closing) {
          this.#logger.error("WebSocket closed unexpectedly");
          void resourceCleanup();
          this.tts.pool.remove(ws);
          completionFuture.reject(
            new import_exceptions.APIStatusError({
              message: "Gateway connection closed unexpectedly",
              options: { requestId }
            })
          );
        }
      };
      const onAbort = () => {
        var _a;
        void resourceCleanup();
        try {
          (_a = ws.terminate) == null ? void 0 : _a.call(ws);
        } catch {
        }
        this.tts.pool.remove(ws);
        inputSentEvent.set();
        completionFuture.resolve();
      };
      ws.on("message", onMessage);
      ws.on("error", onError);
      ws.on("close", onClose);
      signal.addEventListener("abort", onAbort);
      try {
        await completionFuture.await;
      } finally {
        ws.off("message", onMessage);
        ws.off("error", onError);
        ws.off("close", onClose);
        signal.removeEventListener("abort", onAbort);
      }
    };
    const createRecvTask = async (signal) => {
      let currentSessionId = null;
      const recvTimeoutMs = this.connOptions.timeoutMs;
      const bstream = new import_audio.AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);
      const serverEventStream = eventChannel.stream();
      const reader = serverEventStream.getReader();
      try {
        await inputSentEvent.wait();
        while (!this.closed && !signal.aborted) {
          const result = await (0, import_utils.waitUntilTimeout)(
            reader.read(),
            recvTimeoutMs,
            () => new import_exceptions.APITimeoutError({ message: "TTS recv idle timeout" })
          );
          if (signal.aborted) return;
          if (result.done) return;
          const parsedServerEvent = import_api_protos.ttsKnownServerEventSchema.safeParse(result.value);
          if (!parsedServerEvent.success) {
            continue;
          }
          const serverEvent = parsedServerEvent.data;
          const sessionIdFromEvent = serverEvent.session_id;
          if (currentSessionId === null && sessionIdFromEvent) {
            currentSessionId = sessionIdFromEvent;
          }
          switch (serverEvent.type) {
            case "session.created":
              currentSessionId = serverEvent.session_id;
              break;
            case "output_audio":
              const base64Data = new Int8Array(Buffer.from(serverEvent.audio, "base64"));
              for (const frame of bstream.write(base64Data.buffer)) {
                sendLastFrame(currentSessionId, false);
                lastFrame = frame;
              }
              break;
            case "output_alignment":
              if (serverEvent.words && serverEvent.words.length > 0) {
                for (const w of serverEvent.words) {
                  pendingTimedTranscripts.push(
                    (0, import_io.createTimedString)({
                      text: w.word,
                      startTime: w.start,
                      endTime: w.end
                    })
                  );
                }
              } else if (serverEvent.chars && serverEvent.chars.length > 0) {
                for (const c of serverEvent.chars) {
                  pendingTimedTranscripts.push(
                    (0, import_io.createTimedString)({
                      text: c.char,
                      startTime: c.start,
                      endTime: c.end
                    })
                  );
                }
              }
              break;
            case "done":
              for (const frame of bstream.flush()) {
                sendLastFrame(currentSessionId, false);
                lastFrame = frame;
              }
              sendLastFrame(currentSessionId, true);
              this.queue.put(SynthesizeStream.END_OF_STREAM);
              await resourceCleanup();
              completionFuture.resolve();
              return;
            case "session.closed":
              await resourceCleanup();
              completionFuture.resolve();
              return;
            case "error":
              this.#logger.error(
                { serverEvent },
                "Received error message from LiveKit TTS WebSocket"
              );
              await resourceCleanup();
              completionFuture.reject(
                new import_exceptions.APIError(`LiveKit TTS returned error: ${serverEvent.message}`)
              );
              return;
          }
        }
      } catch (e) {
        if (e instanceof import_exceptions.APITimeoutError) {
          this.#logger.warn("TTS recv task timed out waiting for server message");
          await resourceCleanup();
          completionFuture.reject(e);
          return;
        }
        throw e;
      } finally {
        reader.releaseLock();
        try {
          await serverEventStream.cancel();
        } catch (e) {
          this.#logger.debug("Error cancelling serverEventStream (may already be cancelled):", e);
        }
      }
    };
    try {
      await this.tts.pool.withConnection(
        async (ws) => {
          try {
            const runController = new AbortController();
            const onStreamAbort = () => runController.abort(this.abortController.signal.reason);
            this.abortController.signal.addEventListener("abort", onStreamAbort, { once: true });
            const tasks = [
              import_utils.Task.from(
                async (controller) => {
                  const combined = (0, import_utils.combineSignals)(runController.signal, controller.signal);
                  await createInputTask(combined);
                },
                void 0,
                "inference-tts-input"
              ),
              import_utils.Task.from(
                async (controller) => {
                  const combined = (0, import_utils.combineSignals)(runController.signal, controller.signal);
                  await createSentenceStreamTask(ws, combined);
                },
                void 0,
                "inference-tts-sentence"
              ),
              import_utils.Task.from(
                async (controller) => {
                  const combined = (0, import_utils.combineSignals)(runController.signal, controller.signal);
                  await createWsListenerTask(ws, combined);
                },
                void 0,
                "inference-tts-ws-listener"
              ),
              import_utils.Task.from(
                async (controller) => {
                  const combined = (0, import_utils.combineSignals)(runController.signal, controller.signal);
                  await createRecvTask(combined);
                },
                void 0,
                "inference-tts-recv"
              )
            ];
            try {
              await import_throws.ThrowsPromise.all(tasks.map((t) => t.result));
            } finally {
              inputSentEvent.set();
              await resourceCleanup();
              await (0, import_utils.cancelAndWait)(tasks, 5e3);
              this.abortController.signal.removeEventListener("abort", onStreamAbort);
            }
          } catch (e) {
            if (e instanceof Error && e.name === "AbortError") {
              return;
            }
            throw e;
          }
        },
        {
          timeout: this.connOptions.timeoutMs
        }
      );
    } catch (e) {
      if (e instanceof Error && e.name === "AbortError") {
        return;
      }
      throw e;
    } finally {
      await resourceCleanup();
    }
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  SynthesizeStream,
  TTS,
  hasAlignedTranscript,
  normalizeTTSFallback,
  parseTTSModelString
});
//# sourceMappingURL=tts.cjs.map