147 lines
4.1 KiB
JavaScript
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};
|
|
|