import {} from "@livekit/rtc-node";
import { APIError, APIStatusError } from "../_exceptions.js";
import { AudioByteStream } from "../audio.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 { createTimedString } from "../voice/io.js";
import {
  sttServerEventSchema
} from "./api_protos.js";
import { connectWs, createAccessToken } from "./utils.js";
const DEFAULT_ENCODING = "pcm_s16le";
const DEFAULT_SAMPLE_RATE = 16e3;
const DEFAULT_BASE_URL = "wss://agent-gateway.livekit.cloud/v1";
const DEFAULT_CANCEL_TIMEOUT = 5e3;
class STT extends BaseSTT {
  opts;
  streams = /* @__PURE__ */ new Set();
  #logger = log();
  constructor(opts) {
    super({ streaming: true, interimResults: true, alignedTranscript: "word" });
    const {
      model,
      language,
      baseURL,
      encoding = DEFAULT_ENCODING,
      sampleRate = DEFAULT_SAMPLE_RATE,
      apiKey,
      apiSecret,
      modelOptions = {}
    } = opts || {};
    const lkBaseURL = baseURL || process.env.LIVEKIT_INFERENCE_URL || DEFAULT_BASE_URL;
    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");
    }
    this.opts = {
      model,
      language,
      encoding,
      sampleRate,
      baseURL: lkBaseURL,
      apiKey: lkApiKey,
      apiSecret: lkApiSecret,
      modelOptions
    };
  }
  get label() {
    return "inference.STT";
  }
  static fromModelString(modelString) {
    if (modelString.includes(":")) {
      const [model, language] = modelString.split(":");
      return new STT({ model, language });
    }
    return new STT({ model: modelString });
  }
  async _recognize(_) {
    throw new Error("LiveKit STT does not support batch recognition, use stream() instead");
  }
  updateOptions(opts) {
    this.opts = { ...this.opts, ...opts };
    for (const stream of this.streams) {
      stream.updateOptions(opts);
    }
  }
  stream(options) {
    const { language, connOptions = DEFAULT_API_CONNECT_OPTIONS } = options || {};
    const streamOpts = {
      ...this.opts,
      language: language ?? this.opts.language
    };
    const stream = new SpeechStream(this, streamOpts, connOptions);
    this.streams.add(stream);
    return stream;
  }
  async connectWs(timeout) {
    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;
    }
    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;
  #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) {
    this.opts = { ...this.opts, ...opts };
    this.reconnectEvent.set();
  }
  async run() {
    while (true) {
      let ws = null;
      let closing = false;
      let finalReceived = false;
      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 Promise((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 Promise((_, 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 Promise.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;
              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;
          socket.send(JSON.stringify({ type: "session.finalize" }));
        } catch (e) {
          if (e.message === "Send 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 event = parseResult.data;
            switch (event.type) {
              case "session.created":
              case "session.finalized":
                break;
              case "session.closed":
                finalReceived = true;
                resourceCleanup();
                break;
              case "interim_transcript":
                this.processTranscript(event, false);
                break;
              case "final_transcript":
                this.processTranscript(event, true);
                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);
        const controller = this.abortController;
        const sendTask = Task.from(({ signal }) => send(ws, signal), controller);
        const wsListenerTask = Task.from(({ signal }) => createWsListener(ws, signal), controller);
        const recvTask = Task.from(({ signal }) => recv(signal), controller);
        const waitReconnectTask = Task.from(
          ({ signal }) => Promise.race([this.reconnectEvent.wait(), waitForAbort(signal)]),
          controller
        );
        try {
          await Promise.race([
            Promise.all([sendTask.result, wsListenerTask.result, recvTask.result]),
            waitReconnectTask.result
          ]);
          if (!waitReconnectTask.done) break;
          this.reconnectEvent.clear();
        } finally {
          await cancelAndWait(
            [sendTask, wsListenerTask, recvTask, waitReconnectTask],
            DEFAULT_CANCEL_TIMEOUT
          );
          resourceCleanup();
        }
      } finally {
        resourceCleanup();
      }
    }
  }
  processTranscript(data, isFinal) {
    if (this.queue.closed) return;
    const requestId = data.session_id || this.requestId;
    const text = data.transcript;
    const language = data.language || this.opts.language || "en";
    if (!text && !isFinal) return;
    try {
      if (!this.speaking) {
        this.speaking = true;
        this.queue.put({ type: SpeechEventType.START_OF_SPEECH });
      }
      const speechData = {
        language,
        startTime: this.startTimeOffset + data.start,
        endTime: this.startTimeOffset + data.start + data.duration,
        confidence: data.confidence,
        text,
        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
          })
        )
      };
      if (isFinal) {
        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: SpeechEventType.INTERIM_TRANSCRIPT,
          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
};
//# sourceMappingURL=stt.js.map