feat: Ajout d'un service de synthèse vocale hybride et intégration de Google Cloud TTS
This commit is contained in:
146
em2rp/functions/generateTTS.js
Normal file
146
em2rp/functions/generateTTS.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 };
|
||||
|
||||
@@ -16,6 +16,7 @@ const { Storage } = require('@google-cloud/storage');
|
||||
// Utilitaires
|
||||
const auth = require('./utils/auth');
|
||||
const helpers = require('./utils/helpers');
|
||||
const { generateTTS } = require('./generateTTS');
|
||||
|
||||
// Initialisation sécurisée
|
||||
if (!admin.apps.length) {
|
||||
@@ -4193,3 +4194,72 @@ exports.quickSearch = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// TEXT-TO-SPEECH - Generate TTS Audio
|
||||
// ============================================================================
|
||||
// Options HTTP spécifiques pour TTS avec CORS activé
|
||||
const ttsHttpOptions = {
|
||||
cors: true, // Activer CORS automatique
|
||||
invoker: 'public',
|
||||
region: 'europe-west9',
|
||||
};
|
||||
|
||||
exports.generateTTSV2 = onRequest(ttsHttpOptions, async (req, res) => {
|
||||
try {
|
||||
// Authentification utilisateur
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
logger.info('[generateTTSV2] Request from user:', {
|
||||
uid: decodedToken.uid,
|
||||
email: decodedToken.email,
|
||||
});
|
||||
|
||||
// Récupération des paramètres
|
||||
const { text, voiceConfig } = req.body.data || {};
|
||||
|
||||
// Validation
|
||||
if (!text) {
|
||||
res.status(400).json({ error: 'Text parameter is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length > 5000) {
|
||||
res.status(400).json({ error: 'Text too long (max 5000 characters)' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Génération de l'audio avec cache
|
||||
const bucketName = admin.storage().bucket().name;
|
||||
const bucket = storage.bucket(bucketName);
|
||||
|
||||
const result = await generateTTS(text, storage, bucket, voiceConfig);
|
||||
|
||||
logger.info('[generateTTSV2] ✓ Success', {
|
||||
cached: result.cached,
|
||||
cacheKey: result.cacheKey,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
audioUrl: result.audioUrl,
|
||||
cached: result.cached,
|
||||
cacheKey: result.cacheKey,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[generateTTSV2] ✗ Error:', {
|
||||
error: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
|
||||
// Gestion des erreurs spécifiques
|
||||
if (error.code === 'PERMISSION_DENIED') {
|
||||
res.status(403).json({ error: 'Permission denied. Check Google Cloud TTS API is enabled.' });
|
||||
} else if (error.code === 'QUOTA_EXCEEDED') {
|
||||
res.status(429).json({ error: 'TTS quota exceeded. Try again later.' });
|
||||
} else {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
42
em2rp/functions/package-lock.json
generated
42
em2rp/functions/package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"name": "functions",
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "^7.18.0",
|
||||
"@google-cloud/text-to-speech": "^5.4.0",
|
||||
"axios": "^1.13.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"envdot": "^0.0.3",
|
||||
@@ -772,12 +773,23 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/text-to-speech": {
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/text-to-speech/-/text-to-speech-5.8.1.tgz",
|
||||
"integrity": "sha512-HXyZBtfQq+ETSLwWV/k3zFRWSzt+KEfiC5/OqXNNUed+lU/LEyN0CsqqEmkFfkL8BPsVIMAK2xiYCaDsKENukg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-gax": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
@@ -791,7 +803,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
@@ -1310,7 +1321,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
@@ -1631,8 +1641,7 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
@@ -1845,7 +1854,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -1855,7 +1863,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -2379,7 +2386,6 @@
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
@@ -2412,7 +2418,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -2425,7 +2430,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
@@ -2727,7 +2731,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
@@ -2831,7 +2834,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -3576,7 +3578,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
@@ -3715,7 +3716,6 @@
|
||||
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz",
|
||||
"integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.10.9",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
@@ -3743,7 +3743,6 @@
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
@@ -4093,7 +4092,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5110,8 +5108,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
@@ -5490,7 +5487,6 @@
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
@@ -5843,7 +5839,6 @@
|
||||
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
|
||||
"integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"protobufjs": "^7.2.5"
|
||||
},
|
||||
@@ -6035,7 +6030,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -6517,7 +6511,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -6532,7 +6525,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -6997,7 +6989,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -7035,7 +7026,6 @@
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -7052,7 +7042,6 @@
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
@@ -7071,7 +7060,6 @@
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "^7.18.0",
|
||||
"@google-cloud/text-to-speech": "^5.4.0",
|
||||
"axios": "^1.13.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"envdot": "^0.0.3",
|
||||
|
||||
Reference in New Issue
Block a user