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>
This commit is contained in:
213
scripts/generate-audios.js
Executable file
213
scripts/generate-audios.js
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user