"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ElevenLabsTtsProvider = void 0;
const elevenlabs_js_1 = require("@elevenlabs/elevenlabs-js");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const constants_1 = require("../../constants");
const logger_1 = __importDefault(require("../../logger"));
class ElevenLabsTtsProvider {
    constructor(modelName = 'eleven_v3', options = {}) {
        const { id, config } = options;
        this.modelName = modelName;
        this.id = id ? () => id : this.id;
        this.config = config || {
            voiceId: 'JBFqnCBsd6RMkjVDRZzb',
            modelId: 'eleven_v3',
            outputFormat: 'pcm_44100', // Default to WAV format (PCM)
            stability: 0.5,
            similarityBoost: 0.75,
        };
        // Initialize ElevenLabs client
        this.initializeClient();
    }
    initializeClient() {
        try {
            this.elevenlabs = new elevenlabs_js_1.ElevenLabsClient({
                apiKey: this.getApiKey(),
            });
            logger_1.default.debug('ElevenLabs client initialized successfully');
        }
        catch (err) {
            logger_1.default.warn('ElevenLabs SDK not found. Text-to-audio functionality will be limited.');
            if (err instanceof Error) {
                logger_1.default.debug(err.message);
            }
        }
    }
    id() {
        return `elevenlabs:tts:${this.modelName}`;
    }
    toString() {
        return `[ElevenLabs TTS Provider ${this.modelName}]`;
    }
    getApiKey() {
        return process.env.ELEVENLABS_API_KEY;
    }
    /**
     * Get all available voices from ElevenLabs API using SDK
     */
    async getAllVoices() {
        if (!this.elevenlabs) {
            logger_1.default.error('ElevenLabs client not initialized');
            return [];
        }
        try {
            const allVoices = await this.elevenlabs.voices.getAll();
            return allVoices.voices || [];
        }
        catch (error) {
            logger_1.default.error(`Failed to fetch voices: ${error}`);
            return [];
        }
    }
    /**
     * Fetch voices directly from ElevenLabs REST API
     * @param options - Optional filter parameters
     * @param options.language - Language code (e.g., 'es', 'en', 'fr')
     * @param options.gender - Gender filter (e.g., 'female', 'male')
     * @param options.category - Category filter (e.g., 'conversational', 'narrative')
     * @returns Array of voice objects
     */
    async fetchVoicesFromAPI(options) {
        const apiKey = this.getApiKey();
        if (!apiKey) {
            logger_1.default.error('ELEVENLABS_API_KEY not configured');
            return [];
        }
        try {
            // Build API URL with query parameters
            const queryParams = new URLSearchParams();
            if (options?.language) {
                queryParams.append('language', options.language);
            }
            if (options?.gender) {
                queryParams.append('gender', options.gender);
            }
            if (options?.category) {
                queryParams.append('category', options.category);
            }
            const apiUrl = `https://api.elevenlabs.io/v1/voices${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
            logger_1.default.debug(`Fetching voices from ElevenLabs API: ${apiUrl}`);
            const response = await fetch(apiUrl, {
                method: 'GET',
                headers: {
                    'xi-api-key': apiKey,
                    'Content-Type': 'application/json',
                },
            });
            if (!response.ok) {
                const errorText = await response.text();
                logger_1.default.error(`ElevenLabs API error: ${response.status} ${response.statusText} - ${errorText}`);
                return [];
            }
            const data = await response.json();
            logger_1.default.debug(`Fetched ${data.voices?.length || 0} voices from API`);
            return data.voices || [];
        }
        catch (error) {
            logger_1.default.error(`Error fetching voices from ElevenLabs API: ${error}`);
            return [];
        }
    }
    /**
     * Get voice ID by language using direct API call
     * @param language - Language code (e.g., 'es', 'en', 'fr') or language name
     * @param options - Optional filters
     * @param options.gender - Gender filter (e.g., 'female', 'male')
     * @param options.category - Category filter (e.g., 'conversational', 'narrative')
     * @returns Voice ID or null if not found
     */
    async getVoiceIdByLanguageAPI(language, options) {
        if (!language) {
            return null;
        }
        try {
            // Get language code from constants
            const languageCode = (0, constants_1.getLanguageCodeByValue)(language) || language.toLowerCase();
            const filters = {
                language: languageCode,
                gender: options?.gender,
                category: options?.category,
            };
            logger_1.default.info(`Fetching voices with filters: language=${languageCode}${options?.gender ? `, gender=${options.gender}` : ''}${options?.category ? `, category=${options.category}` : ''}`);
            // Fetch voices with all filters
            const voices = await this.fetchVoicesFromAPI(filters);
            if (voices.length === 0) {
                logger_1.default.warn(`No voices found for language: ${language} (${languageCode}) with filters: ${JSON.stringify(options)}`);
                return null;
            }
            // Filter voices by language label
            const matchingVoices = voices.filter((voice) => {
                if (!voice.labels) {
                    return false;
                }
                // Direct language code match
                if (voice.labels.language === languageCode) {
                    return true;
                }
                // Check if language matches in any label field
                const languageLower = language.toLowerCase();
                if (voice.labels.language && voice.labels.language.toLowerCase().includes(languageLower)) {
                    return true;
                }
                if (voice.labels.accent && voice.labels.accent.toLowerCase().includes(languageLower)) {
                    return true;
                }
                return false;
            });
            if (matchingVoices.length > 0) {
                const selectedVoice = matchingVoices[0];
                const filterInfo = `${options?.gender ? `, gender=${options.gender}` : ''}${options?.category ? `, category=${options.category}` : ''}`;
                logger_1.default.info(`Found voice via API for language '${language}': ${selectedVoice.name} (${selectedVoice.voice_id})${filterInfo}`);
                return selectedVoice.voice_id;
            }
            // If no exact match, return first voice from API response
            if (voices.length > 0) {
                const firstVoice = voices[0];
                logger_1.default.info(`Using first available voice for language '${language}': ${firstVoice.name} (${firstVoice.voice_id})`);
                return firstVoice.voice_id;
            }
            logger_1.default.warn(`No matching voice found for language: ${language}`);
            return null;
        }
        catch (error) {
            logger_1.default.error(`Error getting voice by language from API: ${error}`);
            return null;
        }
    }
    /**
     * Get voice ID for a specific language
     * @param language - Language code (e.g., 'en', 'es', 'fr', 'hi', 'de')
     * @returns Voice ID or null if not found
     */
    async getVoiceIdByLanguage(language) {
        if (!language) {
            return null;
        }
        try {
            const allVoices = await this.getAllVoices();
            if (allVoices.length === 0) {
                logger_1.default.warn('No voices available from ElevenLabs API');
                return null;
            }
            // Get search terms from constants helper
            const searchTerms = (0, constants_1.getLanguageSearchTerms)(language);
            // Filter voices by language labels
            const matchingVoices = allVoices.filter((voice) => {
                if (!voice.labels) {
                    return false;
                }
                // Check language label
                if (voice.labels.language && searchTerms.includes(voice.labels.language.toLowerCase())) {
                    return true;
                }
                // Check accent label
                if (voice.labels.accent &&
                    searchTerms.some((term) => voice.labels.accent.toLowerCase().includes(term))) {
                    return true;
                }
                // Check use case label
                if (voice.labels['use case'] &&
                    searchTerms.some((term) => voice.labels['use case'].toLowerCase().includes(term))) {
                    return true;
                }
                return false;
            });
            if (matchingVoices.length > 0) {
                const selectedVoice = matchingVoices[0];
                logger_1.default.info(`Found voice for language '${language}': ${selectedVoice.name} (${selectedVoice.voice_id})`);
                return selectedVoice.voice_id;
            }
            logger_1.default.warn(`No voice found for language: ${language}`);
            return null;
        }
        catch (error) {
            logger_1.default.error(`Error getting voice by language: ${error}`);
            return null;
        }
    }
    /**
     * Get voice ID for a specific accent
     * @param accent - Accent name (e.g., 'indian', 'american', 'british') or object with value property
     * @returns Voice ID or null if not found
     */
    async getVoiceIdByAccent(accent) {
        if (!accent) {
            return null;
        }
        try {
            const allVoices = await this.getAllVoices();
            if (allVoices.length === 0) {
                logger_1.default.warn('No voices available from ElevenLabs API');
                return null;
            }
            // Extract accent value if it's an object
            const accentValue = typeof accent === 'object' && accent !== null && 'value' in accent ? accent.value : accent;
            // Filter voices by accent
            const matchingVoices = allVoices.filter((voice) => {
                if (!voice.labels) {
                    return false;
                }
                // Check accent label
                if (voice.labels.accent &&
                    voice.labels.accent.toLowerCase().includes(accentValue.toLowerCase())) {
                    return true;
                }
                // Check description
                if (voice.labels.description &&
                    voice.labels.description.toLowerCase().includes(accentValue.toLowerCase())) {
                    return true;
                }
                return false;
            });
            if (matchingVoices.length > 0) {
                const selectedVoice = matchingVoices[0];
                logger_1.default.info(`Found voice for accent '${accentValue}': ${selectedVoice.name} (${selectedVoice.voice_id})`);
                return selectedVoice.voice_id;
            }
            logger_1.default.warn(`No voice found for accent: ${accentValue}`);
            return null;
        }
        catch (error) {
            logger_1.default.error(`Error getting voice by accent: ${error}`);
            return null;
        }
    }
    async callApi(text) {
        if (!this.elevenlabs) {
            return {
                error: 'ElevenLabs client not initialized. Please install elevenlabs package.',
            };
        }
        if (!this.getApiKey()) {
            return {
                error: 'ELEVENLABS_API_KEY environment variable is not set.',
            };
        }
        try {
            const voiceId = this.config.voiceId || 'JBFqnCBsd6RMkjVDRZzb';
            logger_1.default.debug(`ElevenLabs TTS request: voiceId=${voiceId}, model=${this.modelName}, text length=${text.length}, outputFormat=${this.config.outputFormat || 'not set'}`);
            // Prepare voice settings
            const voiceSettings = {};
            if (this.config.stability !== undefined) {
                voiceSettings.stability = this.config.stability;
            }
            if (this.config.similarityBoost !== undefined) {
                voiceSettings.similarity_boost = this.config.similarityBoost;
            }
            if (this.config.style !== undefined) {
                voiceSettings.style = this.config.style;
            }
            if (this.config.useSpeakerBoost !== undefined) {
                voiceSettings.use_speaker_boost = this.config.useSpeakerBoost;
            }
            // Ensure outputFormat is set, default to pcm_44100 if not provided
            const outputFormat = this.config.outputFormat || 'pcm_44100';
            // Determine audio format and file extension from outputFormat string
            // Format strings: mp3_44100_128, pcm_48000, opus_44100, etc.
            let audioFormat = 'wav'; // Default to wav
            let fileExtension = 'wav'; // Default extension
            if (outputFormat.startsWith('mp3_')) {
                audioFormat = 'mp3';
                fileExtension = 'mp3';
            }
            else if (outputFormat.startsWith('pcm_')) {
                audioFormat = 'wav'; // PCM from ElevenLabs is WAV format
                fileExtension = 'wav';
            }
            else if (outputFormat.startsWith('opus_')) {
                audioFormat = 'opus';
                fileExtension = 'opus';
            }
            else if (outputFormat.includes('ulaw')) {
                audioFormat = 'ulaw';
                fileExtension = 'ulaw';
            }
            logger_1.default.debug(`ElevenLabs TTS format: outputFormat=${outputFormat}, audioFormat=${audioFormat}, fileExtension=${fileExtension}`);
            const audio = await this.elevenlabs.textToSpeech.convert(voiceId, {
                text,
                modelId: this.config.modelId || this.modelName,
                output_format: outputFormat, // Use the actual format string (e.g., pcm_48000, mp3_48000_192)
                voice_settings: Object.keys(voiceSettings).length > 0 ? voiceSettings : undefined,
            });
            // Convert audio stream to buffer
            const chunks = [];
            for await (const chunk of audio) {
                chunks.push(Buffer.from(chunk));
            }
            const audioBuffer = Buffer.concat(chunks);
            logger_1.default.debug(`ElevenLabs TTS response: Generated audio buffer of size ${audioBuffer.length} bytes, format=${outputFormat}`);
            // Write audio buffer to temporary file
            const tempDir = os.tmpdir();
            const tempFileName = `elevenlabs_${Date.now()}_${Math.random().toString(36).substring(7)}.${fileExtension}`;
            const tempFilePath = path.join(tempDir, tempFileName);
            try {
                // Write buffer to file
                fs.writeFileSync(tempFilePath, audioBuffer);
                logger_1.default.debug(`Written audio buffer to temporary file: ${tempFilePath}`);
                // Read file back and convert to base64
                const fileBuffer = fs.readFileSync(tempFilePath);
                const base64Audio = fileBuffer.toString('base64');
                logger_1.default.debug(`Converted audio file to base64: ${fileBuffer.length} bytes, format: ${audioFormat}`);
                // Clean up temporary file
                try {
                    fs.unlinkSync(tempFilePath);
                    logger_1.default.debug(`Cleaned up temporary file: ${tempFilePath}`);
                }
                catch (unlinkError) {
                    logger_1.default.warn(`Failed to delete temporary file ${tempFilePath}: ${unlinkError}`);
                }
                // Return the audio as base64 string with format information
                return {
                    output: base64Audio,
                    audio: {
                        data: base64Audio,
                        format: audioFormat,
                    },
                };
            }
            catch (fileError) {
                // Clean up temporary file on error
                try {
                    if (fs.existsSync(tempFilePath)) {
                        fs.unlinkSync(tempFilePath);
                    }
                }
                catch (unlinkError) {
                    logger_1.default.warn(`Failed to delete temporary file ${tempFilePath}: ${unlinkError}`);
                }
                const errorMessage = fileError instanceof Error ? fileError.message : String(fileError);
                logger_1.default.error(`Error writing/reading audio file: ${errorMessage}`);
                // Fallback: return buffer directly as base64 if file operations fail
                logger_1.default.warn('Falling back to direct buffer-to-base64 conversion');
                return {
                    output: audioBuffer.toString('base64'),
                    audio: {
                        data: audioBuffer.toString('base64'),
                        format: audioFormat,
                    },
                };
            }
        }
        catch (err) {
            const errorMessage = err instanceof Error ? err.message : String(err);
            // Check for specific error types
            if (errorMessage.includes('voice_limit_reached')) {
                logger_1.default.warn(`ElevenLabs voice limit reached. Voice ID ${this.config.voiceId} may be a custom voice. Falling back to Azure Speech.`);
                return {
                    error: `ElevenLabs voice limit reached - using fallback provider`,
                };
            }
            else if (errorMessage.includes('quota_exceeded')) {
                logger_1.default.warn(`ElevenLabs quota exceeded. Falling back to Azure Speech.`);
                return {
                    error: `ElevenLabs quota exceeded - using fallback provider`,
                };
            }
            logger_1.default.error(`ElevenLabs API call error: ${errorMessage}`);
            return {
                error: `ElevenLabs API call error: ${errorMessage}`,
            };
        }
    }
    /**
     * Convert text to speech and return as Buffer
     * This is a convenience method that wraps callApi
     */
    async textToSpeechBuffer(text) {
        const response = await this.callApi(text);
        if (response.error) {
            logger_1.default.error(`Failed to convert text to speech: ${response.error}`);
            return null;
        }
        if (!response.output || typeof response.output !== 'string') {
            logger_1.default.error('Invalid response from ElevenLabs API');
            return null;
        }
        return Buffer.from(response.output, 'base64');
    }
}
exports.ElevenLabsTtsProvider = ElevenLabsTtsProvider;
//# sourceMappingURL=elevenlabs.js.map