/** * Cloud Function: generateTTS * Génère de l'audio TTS avec Google Cloud Text-to-Speech * Avec système de cache dans Firebase Storage */ const textToSpeech = require('@google-cloud/text-to-speech'); const crypto = require('crypto'); const logger = require('firebase-functions/logger'); /** * Génère un hash MD5 pour le texte (utilisé comme clé de cache) * @param {string} text - Texte à hasher * @return {string} Hash MD5 */ function generateCacheKey(text, voiceConfig = {}) { const cacheString = JSON.stringify({ text, lang: voiceConfig.languageCode || 'fr-FR', voice: voiceConfig.name || 'fr-FR-Standard-B', }); return crypto.createHash('md5').update(cacheString).digest('hex'); } /** * Génère l'audio TTS et le stocke dans Firebase Storage * @param {string} text - Texte à synthétiser * @param {object} storage - Instance Firebase Storage * @param {object} bucket - Bucket Firebase Storage * @param {object} voiceConfig - Configuration de la voix (optionnel) * @return {Promise<{audioUrl: string, cached: boolean}>} */ async function generateTTS(text, storage, bucket, voiceConfig = {}) { try { // Validation du texte if (!text || text.trim().length === 0) { throw new Error('Text cannot be empty'); } if (text.length > 5000) { throw new Error('Text too long (max 5000 characters)'); } // Configuration par défaut de la voix const defaultVoiceConfig = { languageCode: 'fr-FR', name: 'fr-FR-Standard-B', // Voix masculine française (Standard = gratuit) ssmlGender: 'MALE', }; const finalVoiceConfig = { ...defaultVoiceConfig, ...voiceConfig }; // Générer la clé de cache const cacheKey = generateCacheKey(text, finalVoiceConfig); const fileName = `tts-cache/${cacheKey}.mp3`; const file = bucket.file(fileName); // Vérifier si le fichier existe déjà dans le cache const [exists] = await file.exists(); if (exists) { logger.info('[generateTTS] ✓ Cache HIT', { cacheKey, text: text.substring(0, 50) }); // Générer une URL signée valide 7 jours const [url] = await file.getSignedUrl({ action: 'read', expires: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 jours }); return { audioUrl: url, cached: true, cacheKey, }; } logger.info('[generateTTS] ○ Cache MISS - Generating audio', { cacheKey, text: text.substring(0, 50), voice: finalVoiceConfig.name, }); // Créer le client Text-to-Speech const client = new textToSpeech.TextToSpeechClient(); // Configuration de la requête const request = { input: { text: text }, voice: finalVoiceConfig, audioConfig: { audioEncoding: 'MP3', speakingRate: 0.9, // Légèrement plus lent pour meilleure compréhension pitch: -2.0, // Voix un peu plus grave volumeGainDb: 0.0, }, }; // Appeler l'API Google Cloud TTS const [response] = await client.synthesizeSpeech(request); if (!response.audioContent) { throw new Error('No audio content returned from TTS API'); } logger.info('[generateTTS] ✓ Audio generated', { size: response.audioContent.length, }); // Sauvegarder dans Firebase Storage await file.save(response.audioContent, { metadata: { contentType: 'audio/mpeg', metadata: { text: text.substring(0, 100), // Premier 100 caractères pour debug voice: finalVoiceConfig.name, generatedAt: new Date().toISOString(), }, }, }); logger.info('[generateTTS] ✓ Audio cached', { fileName }); // Générer une URL signée const [url] = await file.getSignedUrl({ action: 'read', expires: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 jours }); return { audioUrl: url, cached: false, cacheKey, }; } catch (error) { logger.error('[generateTTS] ✗ Error', { error: error.message, code: error.code, text: text?.substring(0, 50), }); throw error; } } module.exports = { generateTTS, generateCacheKey };