import {} from "@livekit/rtc-node";
import { ThrowsPromise } from "@livekit/throws-transformer/throws";
import { APIError, APIStatusError } from "../_exceptions.js";
import { AudioByteStream } from "../audio.js";
import { areLanguagesEquivalent, normalizeLanguage } from "../language.js";
import { log } from "../log.js";
import { createStreamChannel } from "../stream/stream_channel.js";
import {
  STT as BaseSTT,
  SpeechStream as BaseSpeechStream,
  SpeechEventType
} from "../stt/index.js";
import { DEFAULT_API_CONNECT_OPTIONS } from "../types.js";
import { Event, Task, cancelAndWait, shortuuid, waitForAbort } from "../utils.js";
import { VADEventType } from "../vad.js";
import { createTimedString } from "../voice/io.js";
import {
  sttKnownServerEventSchema,
  sttServerEventSchema
} from "./api_protos.js";
import { connectWs, createAccessToken, getDefaultInferenceUrl } from "./utils.js";
import { VAD as InferenceVAD } from "./vad.js";
const DIARIZATION_EXTRA_KEYS = ["diarize", "speaker_labels", "diarization"];
function diarizationEnabled(extraKwargs) {
  if (!extraKwargs) return false;
  return DIARIZATION_EXTRA_KEYS.some((key) => {
    const value = extraKwargs[key];
    if (!value) return false;
    return !(typeof value === "string" && value.toLowerCase() === "none");
  });
}
function parseSTTModelString(model) {
  const idx = model.lastIndexOf(":");
  if (idx !== -1) {
    return [model.slice(0, idx), normalizeLanguage(model.slice(idx + 1))];
  }
  return [model, void 0];
}
function normalizeSTTFallback(fallback) {
  const makeFallback = (model) => {
    if (typeof model === "string") {
      const [name] = parseSTTModelString(model);
      return { model: name };
    }
    return model;
  };
  if (Array.isArray(fallback)) {
    return fallback.map(makeFallback);
  }
  return [makeFallback(fallback)];
}
function isSpeechmaticsModel(model) {
  return (model == null ? void 0 : model.startsWith("speechmatics/")) ?? false;
}
function resolveVADForModel(model, vad) {
  const speechmatics = isSpeechmaticsModel(model);
  if (vad && !speechmatics) {
    log().warn({ model }, "`vad` will be ignored: model handles endpointing server-side");
    return void 0;
  }
  if (speechmatics && vad === void 0) {
    return new InferenceVAD();
  }
  return vad;
}
const DEFAULT_ENCODING = "pcm_s16le";
const DEFAULT_SAMPLE_RATE = 16e3;
const DEFAULT_CANCEL_TIMEOUT = 5e3;
class STT extends BaseSTT {
  opts;
  streams = /* @__PURE__ */ new Set();
  vad;
  _vadPromise;
  /**
   * Resolves to the VAD instance for the current model, or `undefined` if the model
   * handles endpointing server-side. Lazily computed on first read.
   */
  get vadPromise() {
    if (this._vadPromise === void 0) {
      this._vadPromise = Promise.resolve(this.vad);
    }
    return this._vadPromise;
  }
  #logger = log();
  constructor(opts) {
    const modelOptions = (opts == null ? void 0 : opts.modelOptions) ?? {};
    super({
      streaming: true,
      interimResults: true,
      alignedTranscript: "word",
      diarization: diarizationEnabled(modelOptions)
    });
    const {
      model,
      language,
      baseURL,
      encoding = DEFAULT_ENCODING,
      sampleRate = DEFAULT_SAMPLE_RATE,
      apiKey,
      apiSecret,
      fallback,
      connOptions,
      vad
    } = opts || {};
    const lkBaseURL = baseURL || 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 nextLanguage = language;
    if (typeof nextModel === "string") {
      const [parsedModel, parsedLanguage] = parseSTTModelString(nextModel);
      if (parsedLanguage !== void 0) {
        if (nextLanguage && !areLanguagesEquivalent(nextLanguage, parsedLanguage)) {
          this.#logger.warn(
            "`language` is provided via both argument and model, using the one from the argument",
            { language: nextLanguage, model: nextModel }
          );
        } else {
          nextLanguage = parsedLanguage;
        }
        nextModel = parsedModel;
      }
    }
    const normalizedFallback = fallback ? normalizeSTTFallback(fallback) : void 0;
    this.vad = resolveVADForModel(nextModel, vad);
    this.opts = {
      model: nextModel,
      language: nextLanguage ? normalizeLanguage(nextLanguage) : void 0,
      encoding,
      sampleRate,
      baseURL: lkBaseURL,
      apiKey: lkApiKey,
      apiSecret: lkApiSecret,
      modelOptions,
      fallback: normalizedFallback,
      connOptions: connOptions ?? DEFAULT_API_CONNECT_OPTIONS
    };
  }
  get label() {
    return "inference.STT";
  }
  get model() {
    return this.opts.model ?? "auto";
  }
  get provider() {
    return "livekit";
  }
  static fromModelString(modelString) {
    const [model, language] = parseSTTModelString(modelString);
    return new STT({ model, language });
  }
  async _recognize(_) {
    throw new Error("LiveKit STT does not support batch recognition, use stream() instead");
  }
  updateOptions(opts) {
    const nextOpts = { ...opts };
    if (typeof nextOpts.model === "string") {
      const [parsedModel, parsedLanguage] = parseSTTModelString(nextOpts.model);
      nextOpts.model = parsedModel;
      if (parsedLanguage !== void 0 && nextOpts.language === void 0) {
        nextOpts.language = parsedLanguage;
      }
    }
    const mergedModelOptions = opts.modelOptions ? { ...this.opts.modelOptions, ...opts.modelOptions } : this.opts.modelOptions;
    this.opts = {
      ...this.opts,
      ...nextOpts,
      language: nextOpts.language !== void 0 ? normalizeLanguage(nextOpts.language) : this.opts.language,
      modelOptions: mergedModelOptions
    };
    if (nextOpts.model !== void 0) {
      this.vad = resolveVADForModel(nextOpts.model, this.vad);
      this._vadPromise = void 0;
    }
    if (nextOpts.modelOptions) {
      this.updateCapabilities({
        diarization: diarizationEnabled(this.opts.modelOptions)
      });
    }
    for (const stream of this.streams) {
      stream.updateOptions(nextOpts);
    }
  }
  stream(options) {
    const { language, connOptions = this.opts.connOptions ?? DEFAULT_API_CONNECT_OPTIONS } = options || {};
    const streamOpts = {
      ...this.opts,
      language: language !== void 0 ? normalizeLanguage(language) : this.opts.language
    };
    const stream = new SpeechStream(this, streamOpts, connOptions);
    this.streams.add(stream);
    return stream;
  }
  async connectWs(timeout) {
    var _a;
    const params = {
      settings: {
        sample_rate: String(this.opts.sampleRate),
        encoding: this.opts.encoding,
        extra: this.opts.modelOptions
      }
    };
    if (this.opts.model && this.opts.model !== "auto") {
      params.model = this.opts.model;
    }
    if (this.opts.language) {
      params.settings.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,
          extra: m.extraKwargs ?? {}
        }))
      };
    }
    if (this.opts.connOptions) {
      params.connection = {
        timeout: this.opts.connOptions.timeoutMs / 1e3,
        retries: this.opts.connOptions.maxRetry
      };
    }
    let baseURL = this.opts.baseURL;
    if (baseURL.startsWith("http://") || baseURL.startsWith("https://")) {
      baseURL = baseURL.replace("http", "ws");
    }
    const token = await createAccessToken(this.opts.apiKey, this.opts.apiSecret);
    const url = `${baseURL}/stt`;
    const headers = { Authorization: `Bearer ${token}` };
    const socket = await connectWs(url, headers, timeout);
    const msg = { ...params, type: "session.create" };
    socket.send(JSON.stringify(msg));
    return socket;
  }
}
class SpeechStream extends BaseSpeechStream {
  opts;
  requestId = shortuuid("stt_request_");
  speaking = false;
  speechDuration = 0;
  reconnectEvent = new Event();
  stt;
  connOptions;
  activeWs;
  #logger = log();
  constructor(sttImpl, opts, connOptions) {
    super(sttImpl, opts.sampleRate, connOptions);
    this.opts = opts;
    this.stt = sttImpl;
    this.connOptions = connOptions;
  }
  get label() {
    return "inference.SpeechStream";
  }
  updateOptions(opts) {
    const mergedModelOptions = opts.modelOptions ? { ...this.opts.modelOptions, ...opts.modelOptions } : this.opts.modelOptions;
    this.opts = {
      ...this.opts,
      ...opts,
      language: opts.language !== void 0 ? normalizeLanguage(opts.language) : this.opts.language,
      modelOptions: mergedModelOptions
    };
    if (this.activeWs && this.activeWs.readyState === 1) {
      const settings = {};
      if (opts.model !== void 0) settings.model = opts.model;
      if (opts.language !== void 0) settings.language = normalizeLanguage(opts.language);
      if (opts.modelOptions !== void 0) settings.extra = opts.modelOptions;
      if (Object.keys(settings).length > 0) {
        try {
          this.activeWs.send(JSON.stringify({ type: "session.update", settings }));
        } catch (e) {
          this.#logger.debug({ err: e }, "failed to send session.update, ws may be closing");
        }
      }
    }
  }
  async run() {
    while (true) {
      const vad = await this.stt.vadPromise;
      let ws = null;
      let closing = false;
      let finalReceived = false;
      let vadStream = null;
      const eventChannel = createStreamChannel();
      const resourceCleanup = () => {
        if (closing) return;
        closing = true;
        eventChannel.close();
        ws == null ? void 0 : ws.removeAllListeners();
        ws == null ? void 0 : ws.close();
      };
      const createWsListener = async (ws2, signal) => {
        return new ThrowsPromise((resolve, reject) => {
          const onAbort = () => {
            resourceCleanup();
            reject(new Error("WebSocket connection aborted"));
          };
          signal.addEventListener("abort", onAbort, { once: true });
          ws2.on("message", (data) => {
            const json = JSON.parse(data.toString());
            eventChannel.write(json);
          });
          ws2.on("error", (e) => {
            this.#logger.error({ error: e }, "WebSocket error");
            resourceCleanup();
            reject(e);
          });
          ws2.on("close", (code) => {
            resourceCleanup();
            if (!closing) return this.#logger.error("WebSocket closed unexpectedly");
            if (finalReceived) return resolve();
            reject(
              new APIStatusError({
                message: "LiveKit STT connection closed unexpectedly",
                options: { statusCode: code }
              })
            );
          });
        });
      };
      const send = async (socket, signal) => {
        const audioStream = new AudioByteStream(
          this.opts.sampleRate,
          1,
          Math.floor(this.opts.sampleRate / 20)
          // 50ms
        );
        const abortPromise = new ThrowsPromise((_, reject) => {
          if (signal.aborted) {
            return reject(new Error("Send aborted"));
          }
          const onAbort = () => reject(new Error("Send aborted"));
          signal.addEventListener("abort", onAbort, { once: true });
        });
        const iterator = this.input[Symbol.asyncIterator]();
        try {
          while (true) {
            const result = await ThrowsPromise.race([iterator.next(), abortPromise]);
            if (result.done) break;
            const ev = result.value;
            let frames;
            if (ev === SpeechStream.FLUSH_SENTINEL) {
              frames = audioStream.flush();
            } else {
              const frame = ev;
              vadStream == null ? void 0 : vadStream.pushFrame(frame);
              frames = audioStream.write(new Int16Array(frame.data).buffer);
            }
            for (const frame of frames) {
              this.speechDuration += frame.samplesPerChannel / frame.sampleRate;
              const base64 = Buffer.from(frame.data.buffer).toString("base64");
              const msg = { type: "input_audio", audio: base64 };
              socket.send(JSON.stringify(msg));
            }
          }
          closing = true;
          vadStream == null ? void 0 : vadStream.endInput();
          socket.send(JSON.stringify({ type: "session.finalize" }));
        } catch (e) {
          if (e.message === "Send aborted") {
            return;
          }
          throw e;
        }
      };
      const processVAD = async (stream, socket, signal) => {
        const abortPromise = new ThrowsPromise((_, reject) => {
          if (signal.aborted) {
            return reject(new Error("VAD aborted"));
          }
          const onAbort = () => reject(new Error("VAD aborted"));
          signal.addEventListener("abort", onAbort, { once: true });
        });
        const iterator = stream[Symbol.asyncIterator]();
        try {
          while (true) {
            const result = await ThrowsPromise.race([iterator.next(), abortPromise]);
            if (result.done) break;
            if (result.value.type !== VADEventType.END_OF_SPEECH) continue;
            if (socket.readyState !== 1) return;
            socket.send(JSON.stringify({ type: "session.finalize" }));
          }
        } catch (e) {
          if (e.message === "VAD aborted") return;
          throw e;
        }
      };
      const recv = async (signal) => {
        const serverEventStream = eventChannel.stream();
        const reader = serverEventStream.getReader();
        try {
          while (!this.closed && !signal.aborted) {
            const result = await reader.read();
            if (signal.aborted) return;
            if (result.done) return;
            const parseResult = await sttServerEventSchema.safeParseAsync(result.value);
            if (!parseResult.success) {
              this.#logger.warn(
                { error: parseResult.error, rawData: result.value },
                "Failed to parse STT server event"
              );
              continue;
            }
            const knownResult = sttKnownServerEventSchema.safeParse(parseResult.data);
            if (!knownResult.success) {
              continue;
            }
            const event = knownResult.data;
            switch (event.type) {
              case "session.created":
              case "session.finalized":
                break;
              case "session.closed":
                finalReceived = true;
                resourceCleanup();
                break;
              case "interim_transcript":
                this.processTranscript(event, SpeechEventType.INTERIM_TRANSCRIPT);
                break;
              case "final_transcript":
                this.processTranscript(event, SpeechEventType.FINAL_TRANSCRIPT);
                break;
              case "preflight_transcript":
                this.processTranscript(event, SpeechEventType.PREFLIGHT_TRANSCRIPT);
                break;
              case "error":
                this.#logger.error({ error: event }, "Received error from LiveKit STT");
                resourceCleanup();
                throw new APIError(`LiveKit STT returned error: ${JSON.stringify(event)}`);
            }
          }
        } finally {
          reader.releaseLock();
          try {
            await serverEventStream.cancel();
          } catch (e) {
            this.#logger.debug("Error cancelling serverEventStream (may already be cancelled):", e);
          }
        }
      };
      try {
        ws = await this.stt.connectWs(this.connOptions.timeoutMs);
        this.activeWs = ws;
        vadStream = (vad == null ? void 0 : vad.stream()) ?? null;
        const connController = new AbortController();
        const onStreamAbort = () => connController.abort();
        this.abortController.signal.addEventListener("abort", onStreamAbort);
        const sendTask = Task.from(({ signal }) => send(ws, signal), connController);
        const wsListenerTask = Task.from(
          ({ signal }) => createWsListener(ws, signal),
          connController
        );
        const recvTask = Task.from(({ signal }) => recv(signal), connController);
        const activeVADStream = vadStream;
        const vadTask = activeVADStream ? Task.from(({ signal }) => processVAD(activeVADStream, ws, signal), connController) : void 0;
        const waitReconnectTask = Task.from(
          ({ signal }) => ThrowsPromise.race([this.reconnectEvent.wait(), waitForAbort(signal)]),
          connController
        );
        try {
          const taskResults = [sendTask.result, wsListenerTask.result, recvTask.result];
          if (vadTask) taskResults.push(vadTask.result);
          await ThrowsPromise.race([ThrowsPromise.all(taskResults), waitReconnectTask.result]);
          if (!waitReconnectTask.done) break;
          this.reconnectEvent.clear();
        } finally {
          connController.abort();
          this.abortController.signal.removeEventListener("abort", onStreamAbort);
          this.activeWs = void 0;
          vadStream == null ? void 0 : vadStream.close();
          const tasks = [sendTask, wsListenerTask, recvTask, waitReconnectTask];
          if (vadTask) tasks.push(vadTask);
          await cancelAndWait(tasks, DEFAULT_CANCEL_TIMEOUT);
          resourceCleanup();
        }
        if (this.abortController.signal.aborted) break;
      } finally {
        this.activeWs = void 0;
        resourceCleanup();
      }
    }
  }
  processTranscript(data, eventType) {
    if (this.queue.closed) return;
    const requestId = data.session_id || this.requestId;
    const text = data.transcript;
    const language = normalizeLanguage(data.language || this.opts.language || "en");
    if (!text && eventType !== SpeechEventType.FINAL_TRANSCRIPT) return;
    try {
      if (!this.speaking) {
        this.speaking = true;
        this.queue.put({ type: SpeechEventType.START_OF_SPEECH });
      }
      const extra = data.extra;
      const metadata = extra && typeof extra === "object" && !Array.isArray(extra) && Object.keys(extra).length > 0 ? extra : void 0;
      const speechData = {
        language,
        startTime: this.startTimeOffset + data.start,
        endTime: this.startTimeOffset + data.start + data.duration,
        confidence: data.confidence,
        text,
        speakerId: data.speaker_id ?? void 0,
        words: data.words.map(
          (word) => createTimedString({
            text: word.word,
            startTime: word.start + this.startTimeOffset,
            endTime: word.end + this.startTimeOffset,
            startTimeOffset: this.startTimeOffset,
            confidence: word.confidence,
            speakerId: word.speaker_id ?? void 0
          })
        ),
        metadata
      };
      if (eventType === SpeechEventType.FINAL_TRANSCRIPT) {
        if (this.speechDuration > 0) {
          this.queue.put({
            type: SpeechEventType.RECOGNITION_USAGE,
            requestId,
            recognitionUsage: { audioDuration: this.speechDuration }
          });
          this.speechDuration = 0;
        }
        this.queue.put({
          type: SpeechEventType.FINAL_TRANSCRIPT,
          requestId,
          alternatives: [speechData]
        });
        if (this.speaking) {
          this.speaking = false;
          this.queue.put({ type: SpeechEventType.END_OF_SPEECH });
        }
      } else {
        this.queue.put({
          type: eventType,
          requestId,
          alternatives: [speechData]
        });
      }
    } catch (e) {
      if (e instanceof Error && e.message.includes("Queue is closed")) {
        this.#logger.warn(
          { err: e },
          "Queue closed during transcript processing (expected during disconnect)"
        );
      } else {
        this.#logger.error({ err: e }, "Error putting transcript to queue");
      }
    }
  }
}
export {
  STT,
  SpeechStream,
  normalizeSTTFallback,
  parseSTTModelString
};
//# sourceMappingURL=stt.js.map