import { Readable } from "node:stream";
import { existsSync } from "node:fs";
import { chunkBuffer, float32ToPcm16le } from "../../../audio.js";
import { loadSherpaOnnxNode } from "./sherpa-onnx-node-loader.js";
function assertFileExists(filePath, label) {
    if (!existsSync(filePath)) {
        throw new Error(`Missing ${label}: ${filePath}`);
    }
}
export class SherpaOnnxTTS {
    constructor(config, logger) {
        if (config.preset !== "kokoro-en-v0_19" && config.preset !== "kitten-nano-en-v0_1-fp16") {
            throw new Error(`Unsupported Sherpa TTS preset: ${config.preset}`);
        }
        this.logger = logger.child({ module: "speech", provider: "local", component: "tts" });
        this.speakerId = config.speakerId ?? 0;
        this.speed = config.speed ?? 1.0;
        const sherpa = loadSherpaOnnxNode();
        if (typeof sherpa.OfflineTts !== "function") {
            throw new Error("sherpa-onnx-node OfflineTts is unavailable");
        }
        const modelFile = config.preset === "kokoro-en-v0_19" ? "model.onnx" : "model.fp16.onnx";
        const modelPath = `${config.modelDir}/${modelFile}`;
        const voicesPath = `${config.modelDir}/voices.bin`;
        const tokensPath = `${config.modelDir}/tokens.txt`;
        const dataDir = `${config.modelDir}/espeak-ng-data`;
        assertFileExists(modelPath, "TTS model");
        assertFileExists(voicesPath, "TTS voices");
        assertFileExists(tokensPath, "TTS tokens");
        assertFileExists(dataDir, "TTS espeak-ng dataDir");
        const modelConfig = config.preset === "kokoro-en-v0_19"
            ? {
                kokoro: {
                    model: modelPath,
                    voices: voicesPath,
                    tokens: tokensPath,
                    dataDir,
                    lengthScale: config.lengthScale ?? 1.0,
                },
            }
            : {
                kitten: {
                    model: modelPath,
                    voices: voicesPath,
                    tokens: tokensPath,
                    dataDir,
                    lengthScale: config.lengthScale ?? 1.0,
                },
            };
        const offlineTtsConfig = {
            model: modelConfig,
            numThreads: config.numThreads ?? 2,
            provider: "cpu",
            maxNumSentences: 1,
        };
        this.tts = new sherpa.OfflineTts(offlineTtsConfig);
        this.logger.info({ preset: config.preset, modelDir: config.modelDir }, "Sherpa offline TTS initialized");
    }
    async synthesizeSpeech(text) {
        const trimmed = text.trim();
        if (!trimmed) {
            throw new Error("Cannot synthesize empty text");
        }
        const audio = this.tts.generate({ text: trimmed, sid: this.speakerId, speed: this.speed });
        const samples = audio && audio.samples instanceof Float32Array
            ? audio.samples
            : audio && Array.isArray(audio.samples)
                ? Float32Array.from(audio.samples)
                : null;
        const sampleRate = audio &&
            typeof audio.sampleRate === "number" &&
            Number.isFinite(audio.sampleRate) &&
            audio.sampleRate > 0
            ? audio.sampleRate
            : typeof this.tts.sampleRate === "number"
                ? this.tts.sampleRate
                : 24000;
        if (!samples) {
            throw new Error("Unexpected sherpa TTS output: missing Float32 samples");
        }
        const pcm16 = float32ToPcm16le(samples);
        const chunkBytes = Math.max(2, Math.round(sampleRate * 0.05) * 2); // ~50ms
        const chunks = chunkBuffer(pcm16, chunkBytes);
        return {
            stream: Readable.from(chunks),
            format: `pcm;rate=${sampleRate}`,
        };
    }
    free() {
        try {
            this.tts?.free?.();
        }
        catch {
            // ignore
        }
    }
}
//# sourceMappingURL=sherpa-tts.js.map