"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertCsvToAudio = convertCsvToAudio;
exports.cleanupTempFiles = cleanupTempFiles;
const adm_zip_1 = __importDefault(require("adm-zip"));
const sync_1 = require("csv-parse/sync");
const dedent_1 = __importDefault(require("dedent"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const uuid_1 = require("uuid");
const logger_1 = __importDefault(require("../../logger"));
const defaults_1 = require("../../providers/defaults");
const uploadFilesToAzure_1 = require("../../util/uploadFilesToAzure");
const findWorkspaceProvider_1 = require("../../util/findWorkspaceProvider/findWorkspaceProvider");
const constants_1 = require("../../constants");
const convertTextToAudioWithProvider_1 = require("../../util/convertTextToAudioWithProvider/convertTextToAudioWithProvider");
// Configuration constants
const TTS_BATCH_SIZE = 3; // Number of texts to process in parallel (adjust based on API rate limits)
const BATCH_DELAY_MS = 500; // Delay between batches in milliseconds to avoid rate limiting
/**
 * Parse CSV file and extract text data
 */
async function parseCsvFile(filePath) {
    try {
        const fileContent = fs_1.default.readFileSync(filePath, 'utf8');
        // Parse CSV file
        const records = (0, sync_1.parse)(fileContent, {
            columns: true,
            skip_empty_lines: true,
            trim: true,
        });
        // Extract all text values from all columns
        const textArray = [];
        for (const record of records) {
            const values = Object.values(record);
            for (const value of values) {
                if (typeof value === 'string' && value.trim().length > 0) {
                    textArray.push(value.trim());
                }
            }
        }
        return textArray;
    }
    catch (error) {
        logger_1.default.error(`Error parsing CSV file: ${error}`);
        throw new Error('Failed to parse CSV file');
    }
}
/**
 * Generate filler words using LLM provider
 */
async function generateFillerWords(text, llmProvider) {
    try {
        const prompt = (0, dedent_1.default) `
      Add natural filler words to the following text to make it sound more like human speech.
      Add filler words like "um", "uh", "you know", "like", "well", "so", "I mean" at appropriate places.
      Make it sound natural and conversational. Don't overdo it - add fillers to about 5-10% of the sentences.
      
      Original text:
      ${text}
      
      Return only the text with filler words added, without any explanations or additional formatting.
    `;
        logger_1.default.debug(`Generating filler words for text: ${text.substring(0, 100)}...`);
        const response = await llmProvider.callApi(prompt);
        if (response.error) {
            logger_1.default.error(`Error generating filler words: ${response.error}`);
            // Return original text if generation fails
            return {
                text,
                tokenUsage: {},
                cost: 0,
            };
        }
        const processedText = typeof response.output === 'string' ? response.output.trim() : text;
        logger_1.default.debug(`Generated text with fillers: ${processedText.substring(0, 100)}...`);
        const tokenUsage = {
            prompt: response.tokenUsage?.prompt ?? 0,
            completion: response.tokenUsage?.completion ?? 0,
            total: response.tokenUsage?.total ?? 0,
            cached: response.tokenUsage?.cached ?? 0,
        };
        const cost = response.cost ?? 0;
        return {
            text: processedText,
            tokenUsage,
            cost,
        };
    }
    catch (error) {
        logger_1.default.error(`Error in generateFillerWords: ${error}`);
        // Return original text if generation fails
        return {
            text,
            tokenUsage: {},
            cost: 0,
        };
    }
}
/**
 * Add emotional tags to text for Eleven v3 model
 * @param text - Original text
 * @param emotions - Array of emotion values (e.g., ['happy', 'sad'])
 * @returns Text with emotional tags prepended (e.g., "[happy] This is great!")
 */
function addEmotionalTags(text, emotions) {
    if (!emotions || emotions.length === 0) {
        return text;
    }
    // Pick a random emotion from the array
    const primaryEmotion = emotions[Math.floor(Math.random() * emotions.length)];
    // Wrap text with emotional tag
    return `[${primaryEmotion}] ${text}`;
}
/**
 * Convert text to speech using convertTextToAudioWithProvider with emotional tags support
 */
async function convertTextToSpeech(text, workspaceId, outputFormat, sampleRate, language, projectId, emotions) {
    try {
        // Add emotional tags if emotions are provided (for ElevenLabs only)
        let processedText = text;
        if (emotions && emotions.length > 0) {
            processedText = addEmotionalTags(text, emotions);
            logger_1.default.debug(`Added emotional tags: ${processedText.substring(0, 100)}...`);
        }
        logger_1.default.debug(`Converting text to speech: ${processedText.substring(0, 100)}... (format: ${outputFormat}, sampleRate: ${sampleRate})`);
        // Use convertTextToAudioWithProvider with billing enabled
        const base64Audio = await (0, convertTextToAudioWithProvider_1.convertTextToAudioWithProvider)(processedText, workspaceId, outputFormat, sampleRate, language, {
            projectId,
            sendBillingLogs: true,
        });
        if (!base64Audio) {
            throw new Error('Failed to generate audio');
        }
        // Convert base64 to buffer
        const audioBuffer = Buffer.from(base64Audio, 'base64');
        logger_1.default.debug(`Successfully generated audio buffer of size ${audioBuffer.length} bytes`);
        // Determine provider based on workspace config (default to elevenlabs if not configured)
        const providerConfig = await (0, findWorkspaceProvider_1.findProvider)(workspaceId, constants_1.PROVIDER_TYPE.TTS);
        const provider = providerConfig?.providerName === 'azure-speech' ? 'azure' : 'elevenlabs';
        return {
            buffer: audioBuffer,
            characterCount: processedText.length,
            provider,
        };
    }
    catch (error) {
        logger_1.default.error(`TTS error: ${error}`);
        throw new Error(`Failed to convert text to speech: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
}
/**
 * Create a ZIP file containing all audio files
 * @param audioFiles - Array of objects with filename and buffer
 * @returns Path to the created ZIP file
 */
async function createAudioZipFile(audioFiles) {
    try {
        const tempDir = path_1.default.resolve('temp_audio_zip');
        if (!fs_1.default.existsSync(tempDir)) {
            fs_1.default.mkdirSync(tempDir, { recursive: true });
        }
        const zipFilePath = path_1.default.join(tempDir, `audio_${(0, uuid_1.v4)()}.zip`);
        const zip = new adm_zip_1.default();
        // Add each audio file to the ZIP
        for (const audioFile of audioFiles) {
            zip.addFile(audioFile.filename, audioFile.buffer);
            logger_1.default.debug(`Added ${audioFile.filename} to ZIP`);
        }
        // Write ZIP file
        zip.writeZip(zipFilePath);
        logger_1.default.info(`Created ZIP file with ${audioFiles.length} audio files: ${zipFilePath}`);
        return zipFilePath;
    }
    catch (error) {
        logger_1.default.error(`Error creating audio ZIP file: ${error}`);
        throw new Error('Failed to create ZIP file with audio files');
    }
}
/**
 * Upload ZIP file to Azure Blob Storage
 */
async function uploadZipFile(zipFilePath, zipFileName, token, workspaceId, projectId) {
    try {
        // Upload to Azure
        const folderName = `workspace/${workspaceId}/project/${projectId}/text-to-audio`;
        const url = await (0, uploadFilesToAzure_1.uploadDocumentToAzure)(zipFilePath, zipFileName, token, folderName, 'application/zip');
        // Clean up temporary ZIP file
        if (fs_1.default.existsSync(zipFilePath)) {
            fs_1.default.unlinkSync(zipFilePath);
        }
        logger_1.default.info(`Successfully uploaded ZIP file: ${zipFileName}`);
        return url;
    }
    catch (error) {
        logger_1.default.error(`Error uploading ZIP file ${zipFileName}: ${error}`);
        throw new Error(`Failed to upload ZIP file: ${zipFileName}`);
    }
}
/**
 * Main service function to convert CSV text to audio
 */
async function convertCsvToAudio(csvFilePath, token, workspaceId, projectId, options = {}) {
    try {
        // Parse CSV file to extract text
        logger_1.default.info(`Parsing CSV file: ${csvFilePath}`);
        const textArray = await parseCsvFile(csvFilePath);
        if (textArray.length === 0) {
            return {
                success: false,
                zipFileUrl: '',
                zipFileName: '',
                audioFiles: [],
                totalFiles: 0,
                error: 'No text found in CSV file',
                totalCost: 0,
            };
        }
        logger_1.default.info(`Found ${textArray.length} text entries to convert`);
        // Get output format and sample rate from options, with defaults
        const outputFormat = options.outputFormat || 'wav';
        const sampleRate = options.sampleRate || '44100';
        logger_1.default.info(`Using output format: ${outputFormat}, sample rate: ${sampleRate}`);
        // Determine language from accent option
        let language = 'en-US';
        if (options.accent) {
            const accentValue = typeof options.accent === 'object' && 'value' in options.accent
                ? options.accent.value
                : options.accent;
            language = accentValue || 'en-US';
        }
        // Note: Voice selection and provider initialization is now handled by convertTextToAudioWithProvider
        // The function will use the workspace provider config and handle fallbacks automatically
        // Get LLM provider for filler words generation
        let llmProvider = null;
        if (options.addFillerWords === 'yes') {
            try {
                const defaultProviders = await (0, defaults_1.getDefaultProviders)(undefined, workspaceId);
                llmProvider = defaultProviders.synthesizeProvider;
                logger_1.default.info('LLM provider initialized for filler words generation');
            }
            catch (error) {
                logger_1.default.warn(`Failed to initialize LLM provider: ${error}`);
                // Continue without filler words if LLM initialization fails
            }
        }
        const audioFiles = [];
        const audioBuffers = [];
        // Track usage for billing
        const totalTokenUsage = {
            prompt: 0,
            completion: 0,
            total: 0,
            cached: 0,
        };
        const providerUsage = {
            elevenlabs: 0,
            azure: 0,
        };
        const providerCharacterCounts = {
            elevenlabs: 0,
            azure: 0,
        };
        // Process texts in parallel batches for better performance
        // Benefits: 100 texts take ~40-100s instead of 200-500s (sequential)
        const batches = [];
        // Split texts into batches
        for (let i = 0; i < textArray.length; i += TTS_BATCH_SIZE) {
            batches.push(textArray.slice(i, i + TTS_BATCH_SIZE));
        }
        logger_1.default.info(`Processing ${textArray.length} texts in ${batches.length} batches of ${TTS_BATCH_SIZE}`);
        // Process each batch in parallel
        for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
            const batch = batches[batchIndex];
            const batchStartIndex = batchIndex * TTS_BATCH_SIZE;
            logger_1.default.info(`Processing batch ${batchIndex + 1}/${batches.length} (${batch.length} texts)...`);
            // Process all texts in the current batch in parallel
            const batchPromises = batch.map(async (text, indexInBatch) => {
                const textIndex = batchStartIndex + indexInBatch + 1;
                try {
                    logger_1.default.info(`Converting text ${textIndex}/${textArray.length}: ${text.substring(0, 50)}...`);
                    // Add filler words using LLM if requested
                    let processedText = text;
                    let fillerTokenUsage = {};
                    let fillerCost = 0;
                    if (options.addFillerWords === 'yes' && llmProvider) {
                        logger_1.default.info(`Generating filler words using LLM for text ${textIndex}...`);
                        const fillerResult = await generateFillerWords(text, llmProvider);
                        processedText = fillerResult.text;
                        fillerTokenUsage = fillerResult.tokenUsage;
                        fillerCost = fillerResult.cost;
                    }
                    // Convert text to speech using convertTextToAudioWithProvider
                    const ttsResult = await convertTextToSpeech(processedText, workspaceId, outputFormat, sampleRate, language, projectId, options.emotions);
                    // Generate filename with index to maintain order
                    const fileExtension = outputFormat === 'mp3' ? 'mp3' : outputFormat === 'opus' ? 'webm' : 'wav';
                    const filename = `audio_${String(textIndex).padStart(3, '0')}_${Date.now()}.${fileExtension}`;
                    logger_1.default.info(`Successfully converted audio ${textIndex}/${textArray.length}`);
                    return {
                        success: true,
                        filename,
                        buffer: ttsResult.buffer,
                        originalText: text,
                        index: textIndex,
                        fillerTokenUsage,
                        fillerCost,
                        characterCount: ttsResult.characterCount,
                        provider: ttsResult.provider,
                    };
                }
                catch (error) {
                    logger_1.default.error(`Error processing text entry ${textIndex}: ${error}`);
                    return {
                        success: false,
                        filename: '',
                        buffer: null,
                        originalText: text,
                        index: textIndex,
                        error: error instanceof Error ? error.message : 'Unknown error',
                        fillerTokenUsage: {},
                        fillerCost: 0,
                        characterCount: 0,
                        provider: null,
                    };
                }
            });
            // Wait for all texts in this batch to complete
            const batchResults = await Promise.all(batchPromises);
            // Collect successful results and accumulate usage
            for (const result of batchResults) {
                if (result.success && result.buffer) {
                    audioBuffers.push({
                        filename: result.filename,
                        buffer: result.buffer,
                    });
                    audioFiles.push({
                        filename: result.filename,
                        originalText: result.originalText,
                    });
                    // Accumulate usage
                    totalTokenUsage.prompt += result.fillerTokenUsage?.prompt ?? 0;
                    totalTokenUsage.completion += result.fillerTokenUsage?.completion ?? 0;
                    totalTokenUsage.total += result.fillerTokenUsage?.total ?? 0;
                    totalTokenUsage.cached += result.fillerTokenUsage?.cached ?? 0;
                    if (result.provider) {
                        providerUsage[result.provider] = (providerUsage[result.provider] || 0) + 1;
                        providerCharacterCounts[result.provider] =
                            (providerCharacterCounts[result.provider] || 0) + (result.characterCount ?? 0);
                    }
                }
            }
            logger_1.default.info(`Batch ${batchIndex + 1}/${batches.length} completed. Total successful: ${audioBuffers.length}`);
            // Add a small delay between batches to avoid rate limiting
            if (batchIndex < batches.length - 1) {
                await new Promise((resolve) => setTimeout(resolve, BATCH_DELAY_MS));
            }
        }
        if (audioBuffers.length === 0) {
            return {
                success: false,
                zipFileUrl: '',
                zipFileName: '',
                audioFiles: [],
                totalFiles: 0,
                error: 'Failed to convert any text to audio',
            };
        }
        // Create ZIP file with all audio files
        logger_1.default.info(`Creating ZIP file with ${audioBuffers.length} audio files...`);
        const zipFilePath = await createAudioZipFile(audioBuffers);
        // Upload ZIP file to Azure
        const zipFileName = `audio_files_${Date.now()}.zip`;
        const zipFileUrl = await uploadZipFile(zipFilePath, zipFileName, token, workspaceId, projectId);
        return {
            success: true,
            zipFileUrl,
            zipFileName,
            audioFiles,
            totalFiles: audioBuffers.length,
        };
    }
    catch (error) {
        logger_1.default.error(`Error in convertCsvToAudio: ${error}`);
        return {
            success: false,
            zipFileUrl: '',
            zipFileName: '',
            audioFiles: [],
            totalFiles: 0,
            error: error instanceof Error ? error.message : 'Unknown error occurred',
        };
    }
}
/**
 * Cleanup function for temporary files
 */
function cleanupTempFiles(filePaths) {
    filePaths.forEach((filePath) => {
        try {
            if (fs_1.default.existsSync(filePath)) {
                fs_1.default.unlinkSync(filePath);
            }
        }
        catch (error) {
            logger_1.default.error(`Error cleaning up file ${filePath}: ${error}`);
        }
    });
}
//# sourceMappingURL=textToAudioService.js.map