Files
karibeo_api/scripts/generate-audios.js
ellecio2 8b6483aa7d Agregar campo username a User entity y DTO
- Columna username (unique, nullable) en auth.users
- Campo username en CreateUserDto y UpdateUserDto

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-17 11:47:37 -04:00

214 lines
6.4 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Script standalone para generar audios TTS para todos los monumentos
* Usa Piper TTS directamente sin pasar por el API
*/
const { Client } = require('pg');
const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const execAsync = promisify(exec);
// Configuración
const config = {
db: {
host: 'localhost',
port: 5432,
user: 'karibeo',
password: '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',
database: 'karibeo_db',
},
tts: {
piperPath: '/home/karibeo-api/karibeo-api/tts/piper/piper',
voicesPath: '/home/karibeo-api/karibeo-api/tts/voices',
cachePath: '/home/karibeo-api/karibeo-api/tts/cache',
baseUrl: 'https://api.karibeo.ai:8443',
},
voiceMap: {
es: 'es_ES-davefx-medium',
en: 'en_US-amy-medium',
fr: 'fr_FR-siwis-medium',
it: 'it_IT-riccardo-x_low',
de: 'de_DE-thorsten-medium',
},
languages: ['es', 'en', 'fr', 'it', 'de'],
};
// Limpiar texto para TTS
function cleanTextForSpeech(text) {
return text
.replace(/[\u4E00-\u9FFF]/g, '')
.replace(/[\u3400-\u4DBF]/g, '')
.replace(/[\u3040-\u309F]/g, '')
.replace(/[\u30A0-\u30FF]/g, '')
.replace(/[\uAC00-\uD7AF]/g, '')
.replace(/[\u{1F300}-\u{1F9FF}]/gu, '')
.replace(/[\u{2600}-\u{26FF}]/gu, '')
.replace(/[\u{2700}-\u{27BF}]/gu, '')
.replace(/[\u{1F600}-\u{1F64F}]/gu, '')
.replace(/[\u{1F680}-\u{1F6FF}]/gu, '')
.replace(/[\u{1F1E0}-\u{1F1FF}]/gu, '')
.replace(/[★☆✓✗✔✘●○◆◇▪▫►◄→←↑↓⇒⇐⇑⇓♠♣♥♦]/g, '')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
.replace(/__([^_]+)__/g, '$1')
.replace(/_([^_]+)_/g, '$1')
.replace(/\s+/g, ' ')
.trim();
}
function getAudioHash(text, language) {
const content = `${language}:${text}`;
return crypto.createHash('md5').update(content).digest('hex');
}
async function generateAudio(text, language) {
const voice = config.voiceMap[language] || config.voiceMap['en'];
const voiceModel = path.join(config.tts.voicesPath, `${voice}.onnx`);
if (!fs.existsSync(voiceModel)) {
console.log(` ⚠️ Modelo de voz no encontrado: ${voiceModel}`);
return null;
}
const cleanText = cleanTextForSpeech(text)
.replace(/[\n\r]/g, ' ')
.replace(/"/g, "'")
.substring(0, 3000);
const hash = getAudioHash(cleanText, language);
const fileName = `${hash}.wav`;
const filePath = path.join(config.tts.cachePath, fileName);
// Si ya existe, retornar URL
if (fs.existsSync(filePath)) {
console.log(` 📦 Cache hit: ${fileName}`);
return `${config.tts.baseUrl}/api/v1/tts/audio/${fileName}`;
}
// Crear directorio si no existe
if (!fs.existsSync(config.tts.cachePath)) {
fs.mkdirSync(config.tts.cachePath, { recursive: true });
}
// Guardar texto en archivo temporal
const tempTextFile = path.join(config.tts.cachePath, `${hash}.txt`);
fs.writeFileSync(tempTextFile, cleanText, 'utf-8');
try {
const command = `cat "${tempTextFile}" | "${config.tts.piperPath}" --model "${voiceModel}" --output_file "${filePath}" 2>/dev/null`;
await execAsync(command, { timeout: 120000 });
// Limpiar archivo temporal
if (fs.existsSync(tempTextFile)) {
fs.unlinkSync(tempTextFile);
}
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath);
console.log(` ✅ Audio generado: ${fileName} (${Math.round(stats.size/1024)}KB)`);
return `${config.tts.baseUrl}/api/v1/tts/audio/${fileName}`;
}
return null;
} catch (error) {
console.log(` ❌ Error TTS: ${error.message}`);
if (fs.existsSync(tempTextFile)) {
fs.unlinkSync(tempTextFile);
}
return null;
}
}
async function main() {
console.log('🎙️ Generador de Audios TTS para Monumentos');
console.log('==========================================\n');
// Verificar Piper
if (!fs.existsSync(config.tts.piperPath)) {
console.error(`❌ Piper no encontrado en: ${config.tts.piperPath}`);
process.exit(1);
}
console.log('✅ Piper TTS encontrado\n');
// Conectar a la DB
const client = new Client(config.db);
await client.connect();
console.log('✅ Conectado a la base de datos\n');
// Obtener lugares con descripción
const query = `
SELECT id, name, slug,
description_es, description_en, description_fr, description_it, description_de,
audio_url_es, audio_url_en, audio_url_fr, audio_url_it, audio_url_de
FROM tourism.places_of_interest
WHERE active = true
ORDER BY id
`;
const result = await client.query(query);
const places = result.rows;
console.log(`📍 Encontrados ${places.length} lugares\n`);
let totalGenerated = 0;
let totalSkipped = 0;
let totalFailed = 0;
for (let i = 0; i < places.length; i++) {
const place = places[i];
console.log(`\n[${i + 1}/${places.length}] ${place.name}`);
for (const lang of config.languages) {
const descCol = `description_${lang}`;
const audioCol = `audio_url_${lang}`;
const description = place[descCol];
const existingAudio = place[audioCol];
if (!description) {
continue;
}
if (existingAudio) {
totalSkipped++;
continue;
}
console.log(` 🔊 Generando audio (${lang})...`);
const audioUrl = await generateAudio(description, lang);
if (audioUrl) {
// Actualizar en DB
await client.query(
`UPDATE tourism.places_of_interest SET ${audioCol} = $1 WHERE id = $2`,
[audioUrl, place.id]
);
totalGenerated++;
} else {
totalFailed++;
}
// Pequeña pausa
await new Promise(r => setTimeout(r, 100));
}
}
await client.end();
console.log('\n==========================================');
console.log('📊 RESUMEN');
console.log(` ✅ Generados: ${totalGenerated}`);
console.log(` ⏭️ Omitidos (ya existían): ${totalSkipped}`);
console.log(` ❌ Fallidos: ${totalFailed}`);
console.log('==========================================\n');
}
main().catch(err => {
console.error('Error fatal:', err);
process.exit(1);
});