// 日语 sherpa Offline Recognizer。AdaptiveSegmenter 保证单段最多 4 秒；等待
// 解码的段最多保留 1 个，过载时丢弃陈旧段而不积累实时延迟。
import { AdaptiveSegmenter } from "./adaptive-segmenter.js";
import { emptyAsrMetrics } from "./asr-metrics.js";
const SAMPLE_RATE = 16_000;
const MAX_CONSECUTIVE_DECODE_FAILURES = 3;
export class SherpaSegmentedEngine {
    createRecognizer;
    now;
    emit;
    backend = "sherpa-wasm";
    mode = "segmented";
    recognizer;
    config;
    segmenter = new AdaptiveSegmenter();
    pending;
    decoding = false;
    paused = false;
    receivedAudioDiagnostic = false;
    consecutiveDecodeFailures = 0;
    metric = emptyAsrMetrics();
    constructor(createRecognizer, now = () => performance.now(), emit = () => undefined) {
        this.createRecognizer = createRecognizer;
        this.now = now;
        this.emit = emit;
    }
    async init(config) { if (config.sourceLanguage === "auto")
        throw new Error("language_unsupported"); this.dispose(); this.metric = emptyAsrMetrics(); this.config = config; this.recognizer = await this.createRecognizer(); this.paused = false; this.receivedAudioDiagnostic = false; this.consecutiveDecodeFailures = 0; this.emit({ type: "diagnostic", line: "sherpa segmented initialized sampleRate=16000 hardMaxMs=4000" }); }
    pushAudio(pcm16, startMs) {
        if (!this.recognizer || !this.config || this.paused)
            return;
        if (!this.receivedAudioDiagnostic) {
            this.receivedAudioDiagnostic = true;
            this.emit({ type: "diagnostic", line: `sherpa segmented audio received samples=${pcm16.length}` });
        }
        this.metric.audioReceivedMs += (pcm16.length / SAMPLE_RATE) * 1000;
        const audio = new Float32Array(pcm16.length);
        for (let index = 0; index < pcm16.length; index++)
            audio[index] = pcm16[index] / 0x7fff;
        for (const segment of this.segmenter.push(audio, startMs))
            this.enqueue(segment);
        // Segment assembly/VAD is realtime work even before the offline recognizer
        // runs.  Count only a segment waiting behind an active decode as lag; a
        // normal two-second Japanese segment must not trip the 2s overload reset.
        const queuedDecodeMs = this.pending ? (this.pending.audio.length / SAMPLE_RATE) * 1000 : 0;
        this.metric.audioProcessedMs = Math.max(this.metric.audioProcessedMs, this.metric.audioReceivedMs - queuedDecodeMs);
    }
    flush(_endMs) { const segment = this.segmenter.flush(); if (segment)
        this.enqueue(segment); }
    reset(reason) { this.segmenter.reset(); this.pending = undefined; this.consecutiveDecodeFailures = 0; this.metric.audioProcessedMs = this.metric.audioReceivedMs; this.metric.resetCount += 1; this.emit({ type: "diagnostic", line: `sherpa segmented reset reason=${reason}` }); }
    setPaused(paused) { this.paused = paused; if (paused)
        this.reset("pause"); }
    idlePause() { this.setPaused(true); }
    resumeFromIdle() { this.paused = false; }
    recordDroppedAudio(durationMs) { if (Number.isFinite(durationMs) && durationMs > 0)
        this.metric.droppedAudioMs += durationMs; }
    metrics() { const processed = this.metric.audioProcessedMs; return { ...this.metric, rtf: processed > 0 ? this.metric.inferenceMs / processed : 0, audioLagMs: Math.max(0, this.metric.audioReceivedMs - processed) }; }
    dispose() { this.segmenter.reset(); this.pending = undefined; this.consecutiveDecodeFailures = 0; this.config = undefined; this.recognizer?.free(); this.recognizer = undefined; this.decoding = false; }
    enqueue(segment) {
        if (this.decoding) {
            if (this.pending)
                this.metric.droppedAudioMs += (this.pending.audio.length / SAMPLE_RATE) * 1000;
            this.pending = segment;
            return;
        }
        this.decode(segment);
    }
    decode(segment) {
        const recognizer = this.recognizer;
        if (!recognizer)
            return;
        this.decoding = true;
        const segmentMs = Math.round((segment.audio.length / SAMPLE_RATE) * 1000);
        let stream;
        let stage = "create_stream";
        let started;
        let text = "";
        let failure;
        try {
            stream = recognizer.createStream();
            started = this.now();
            stage = "accept_waveform";
            stream.acceptWaveform(SAMPLE_RATE, segment.audio);
            stage = "decode";
            recognizer.decode(stream);
            stage = "result";
            text = recognizer.getResult(stream).text?.trim() ?? "";
        }
        catch {
            failure = stage;
        }
        finally {
            if (stream) {
                try {
                    stream.free();
                }
                catch {
                    failure ??= "stream_free";
                }
            }
        }
        if (started !== undefined)
            this.metric.inferenceMs += Math.max(0, this.now() - started);
        try {
            if (failure) {
                this.consecutiveDecodeFailures += 1;
                this.emit({ type: "diagnostic", line: `sherpa segmented decode failed stage=${failure} segmentMs=${segmentMs} consecutive=${this.consecutiveDecodeFailures}` });
                // A malformed or too-short segment must not tear down an otherwise live
                // capture session.  Stop only after repeated failures, which is the
                // bounded signal that the runtime itself is no longer usable.
                if (this.consecutiveDecodeFailures >= MAX_CONSECUTIVE_DECODE_FAILURES) {
                    this.paused = true;
                    this.pending = undefined;
                    this.config?.onError({ code: "inference_failed" });
                }
            }
            else {
                this.consecutiveDecodeFailures = 0;
                // Segment assembly may already have advanced the processed timeline, so
                // decoding an assembled segment must not count the same audio twice.
                this.metric.audioProcessedMs = Math.min(this.metric.audioReceivedMs, this.metric.audioProcessedMs + (segment.audio.length / SAMPLE_RATE) * 1000);
                this.emit({ type: "diagnostic", line: `sherpa segmented decoded segmentMs=${segmentMs} result=${text ? "nonempty" : "empty"}` });
                if (text) {
                    this.metric.finalLatencyMs = Math.max(0, this.now() - segment.endMs);
                    this.config?.onStable(text, segment.startMs, segment.endMs);
                }
            }
        }
        finally {
            this.decoding = false;
            const next = this.pending;
            this.pending = undefined;
            if (next)
                this.decode(next);
        }
    }
}
