Files
EM2_ERP/em2rp/functions/generateTTS.js

147 lines
4.1 KiB
JavaScript

/**
* 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 };