server.js-Optimización para Dictado por Voz
Optimización para Dictado por Voz en Tiempo Real
Voy a proponer varias mejoras para optimizar el sistema y hacer que la transcripción sea más rápida, especialmente enfocándonos en el procesamiento asíncrono y la gestión de colas.
Cambios propuestos para el servidor (server.js)
const WebSocket = require('ws');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const { Worker, isMainThread, workerData } = require('worker_threads');
const wss = new WebSocket.Server({ port: 9000 });
const tempDir = path.join(__dirname, 'temp');
// Crear directorio temporal si no existe
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Mapeo de idiomas a códigos de Whisper
const languageCodes = {
spanish: 'es',
english: 'en',
french: 'fr',
german: 'de',
italian: 'it',
portuguese: 'pt',
japanese: 'ja'
};
// Cola de procesamiento
const processingQueue = [];
let isProcessing = false;
wss.on('connection', (ws) => {
console.log('Nuevo cliente conectado');
ws.on('message', async (message) => {
try {
const data = JSON.parse(message);
const segmentId = data.segmentId;
const audioData = Buffer.from(data.audioData, 'base64');
const language = languageCodes[data.language?.toLowerCase()] || 'es';
if (!audioData.length) {
throw new Error('Datos de audio vacíos');
}
// Nombre de archivo temporal
const timestamp = Date.now();
const inputFile = path.join(tempDir, `input_${segmentId}_${timestamp}.webm`);
// Guardar archivo recibido
fs.writeFileSync(inputFile, audioData);
// Agregar a la cola de procesamiento
processingQueue.push({
inputFile,
segmentId,
language,
ws
});
// Iniciar procesamiento si no está en curso
if (!isProcessing) {
processQueue();
}
} catch (error) {
console.error('Error procesando segmento:', error);
ws.send(JSON.stringify({
segmentId: data?.segmentId || 0,
error: error.message,
status: 'error'
}));
}
});
});
// Procesar cola de manera asíncrona
async function processQueue() {
if (processingQueue.length === 0 || isProcessing) return;
isProcessing = true;
const { inputFile, segmentId, language, ws } = processingQueue.shift();
try {
// Usar worker thread para procesamiento en paralelo
const worker = new Worker(__filename, {
workerData: { inputFile, segmentId, language }
});
worker.on('message', (result) => {
ws.send(JSON.stringify({
segmentId: result.segmentId,
text: result.text,
status: 'success'
}));
// Procesar siguiente elemento en la cola
isProcessing = false;
if (processingQueue.length > 0) {
setImmediate(processQueue);
}
});
worker.on('error', (error) => {
console.error('Error en worker:', error);
ws.send(JSON.stringify({
segmentId,
error: error.message,
status: 'error'
}));
isProcessing = false;
if (processingQueue.length > 0) {
setImmediate(processQueue);
}
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker se detuvo con código ${code}`);
}
});
} catch (error) {
console.error('Error en procesamiento:', error);
ws.send(JSON.stringify({
segmentId,
error: error.message,
status: 'error'
}));
isProcessing = false;
if (processingQueue.length > 0) {
setImmediate(processQueue);
}
}
}
// Código del worker thread
if (!isMainThread) {
const { inputFile, segmentId, language } = workerData;
(async () => {
try {
const wavFile = inputFile.replace('.webm', '.wav');
// Convertir a WAV
await new Promise((resolve, reject) => {
const command = `ffmpeg -i "${inputFile}" -ar 16000 -ac 1 -c:a pcm_s16le "${wavFile}" -y`;
exec(command, (error, stdout, stderr) => {
if (error) {
console.error("FFmpeg Error:", stderr);
reject(new Error('Error en conversión de audio'));
return;
}
resolve();
});
});
// Transcribir con Whisper
const text = await new Promise((resolve, reject) => {
const command = `whisper "${wavFile}" --language ${language} ` +
`--model base --task transcribe --output_dir "${tempDir}" ` +
`--output_format txt --fp16 False`;
exec(command, { timeout: 15000 }, (error, stdout, stderr) => {
if (error) {
console.error("Whisper Error:", stderr);
reject(new Error('Error en transcripción'));
return;
}
const outputFile = wavFile.replace('.wav', '.txt');
if (!fs.existsSync(outputFile)) {
reject(new Error('Archivo de transcripción no generado'));
return;
}
const data = fs.readFileSync(outputFile, 'utf8');
const cleanText = data.split('\n')
.filter(line => !line.startsWith('[') && !/^[0-9:.]+ --> [0-9:.]+$/.test(line))
.join(' ')
.replace(/\s+/g, ' ')
.trim();
resolve(cleanText);
});
});
// Enviar resultado al thread principal
parentPort.postMessage({
segmentId,
text
});
} catch (error) {
parentPort.postMessage({
segmentId,
error: error.message
});
}
})();
}
console.log('Servidor de transcripción optimizado iniciado en ws://localhost:9000');Cambios propuestos para el cliente (index.html)
// Reemplazar la función processAudioSegment con esta versión optimizada
async function processAudioSegment(blob) {
const segmentId = ++currentSegmentId;
const now = new Date();
try {
// Usar directamente el Blob sin conversión adicional
const reader = new FileReader();
reader.onload = () => {
const base64Data = reader.result.split(',')[1];
if (transcriptionSocket && transcriptionSocket.readyState === WebSocket.OPEN) {
transcriptionSocket.send(JSON.stringify({
segmentId: segmentId,
timestamp: now.toISOString(),
audioData: base64Data,
language: languageSelect.value,
sampleRate: 16000,
sampleSize: 16,
channels: 1
}));
addStatusMessage(`Enviando segmento ${segmentId} (${Math.round(blob.size/1024)} KB)`);
lastSegmentTime = Date.now();
updateQueueStatus();
} else {
if (audioQueue.length < MAX_QUEUE_SIZE) {
// Almacenar el Blob directamente para evitar procesamiento adicional
audioQueue.push({
blob,
segmentId,
timestamp: now.toISOString(),
language: languageSelect.value
});
updateQueueStatus();
} else {
addStatusMessage("Cola llena - descartando segmento");
}
}
};
reader.readAsDataURL(blob);
} catch (error) {
console.error("Error procesando segmento:", error);
addStatusMessage(`Error al procesar segmento ${segmentId}`);
}
}
// Modificar la función processQueue para manejar los objetos completos
function processQueue() {
if (audioQueue.length > 0 && transcriptionSocket && transcriptionSocket.readyState === WebSocket.OPEN) {
const queuedItem = audioQueue.shift();
const reader = new FileReader();
reader.onload = () => {
const base64Data = reader.result.split(',')[1];
transcriptionSocket.send(JSON.stringify({
segmentId: queuedItem.segmentId,
timestamp: queuedItem.timestamp,
audioData: base64Data,
language: queuedItem.language,
sampleRate: 16000,
sampleSize: 16,
channels: 1
}));
updateQueueStatus();
};
reader.readAsDataURL(queuedItem.blob);
}
}Mejoras implementadas:
Procesamiento en paralelo con Worker Threads:
Uso de worker threads para procesar múltiples segmentos simultáneamente
Cada segmento se procesa en un hilo separado sin bloquear el evento loop principal
Gestión optimizada de colas:
Sistema de cola mejorado para manejar segmentos pendientes
Procesamiento automático cuando hay capacidad disponible
Eliminación de limpieza de archivos temporales:
Los archivos temporales se mantienen para reducir sobrecarga de I/O
Puedes implementar una limpieza periódica si el espacio es un problema
Optimización del flujo de datos:
Envío directo de blobs de audio sin conversiones adicionales
Procesamiento más eficiente de base64 en el cliente
Tiempos de espera reducidos:
Timeout de Whisper reducido a 15 segundos
Priorización del procesamiento sobre la limpieza
Manejo mejorado de errores:
Sistema de reintentos integrado
Notificaciones claras de estado
Recomendaciones adicionales:
Configuración de Whisper:
Usa el modelo
tinyosmallpara mayor velocidad (aunque con menor precisión)Añade
--beam_size 1y--best_of 1para acelerar la inferencia
Escalabilidad:
Considera usar un sistema de colas externo como Redis para manejar muchos clientes
Implementa balanceo de carga si necesitas escalar horizontalmente
Monitoreo:
Añade métricas de rendimiento para identificar cuellos de botella
Monitoriza el tamaño de la cola y los tiempos de procesamiento
Estos cambios deberían proporcionar una mejora significativa en la velocidad de respuesta del sistema de dictado por voz, especialmente en entornos con múltiples segmentos de audio siendo procesados simultáneamente.
Comentarios
Publicar un comentario