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)

javascript
Copy
Download
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)

javascript
Copy
Download
// 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:

  1. 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

  2. Gestión optimizada de colas:

    • Sistema de cola mejorado para manejar segmentos pendientes

    • Procesamiento automático cuando hay capacidad disponible

  3. 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

  4. 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

  5. Tiempos de espera reducidos:

    • Timeout de Whisper reducido a 15 segundos

    • Priorización del procesamiento sobre la limpieza

  6. Manejo mejorado de errores:

    • Sistema de reintentos integrado

    • Notificaciones claras de estado

Recomendaciones adicionales:

  1. Configuración de Whisper:

    • Usa el modelo tiny o small para mayor velocidad (aunque con menor precisión)

    • Añade --beam_size 1 y --best_of 1 para acelerar la inferencia

  2. Escalabilidad:

    • Considera usar un sistema de colas externo como Redis para manejar muchos clientes

    • Implementa balanceo de carga si necesitas escalar horizontalmente

  3. 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

Entradas más populares de este blog

1-configurar Vosk para que utilice todos los recursos de mi pc

3-Whisper vs Vosk