import express from 'express'; import cors from 'cors'; import { pipeline, env } from '@xenova/transformers'; import path from "path"; import { fileURLToPath } from 'url'; import textToSpeechAPI from './textToSpeech/textToSpeechAPI.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); app.use(cors()); app.use(express.json()); let extractor = null; let isInitializing = false; const init = async () => { if (extractor || isInitializing) return extractor; isInitializing = true; console.log('🔄 Inicializando E5...'); try { // Configurações para modelo local // env.allowRemoteModels = false; //env.allowLocalModels = true; env.useBrowserCache = false; // Define o caminho para a pasta de modelos //env.localModelPath = path.join(__dirname, 'models'); const modelPath = 'Xenova/multilingual-e5-small'; extractor = await pipeline('feature-extraction', modelPath, { quantized: true, device: 'cpu', local_files_only: false, }); console.log('✅ E5 carregado!'); return extractor; } catch (error) { console.error('❌ Erro:', error.message); throw error; } finally { isInitializing = false; } }; console.log('🔄 Inicializando TTS...'); textToSpeechAPI(app); console.log('✅ TTS carregado!'); // Health check mínimo app.get('/', (req, res) => { const uptimeSeconds = process.uptime(); // tempo desde que o servidor iniciou const uptime = `${Math.floor(uptimeSeconds / 60)} min ${Math.floor(uptimeSeconds % 60)} sec`; const now = new Date(); res.json({ status: extractor ? 'ready' : 'loading', model: 'e5-small', api_version: 'v1', timestamp: now.toISOString(), uptime: uptime, server_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, requester_ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress }); }); app.get('/ping', (req, res) => res.json({ pong: true })); // Endpoint principal otimizado app.post('/api/embed', async (req, res) => { try { if (!extractor) await init(); const { text, type = 'query' } = req.body; if (!text) return res.status(400).json({ error: 'Texto necessário' }); // Processa com configurações de baixa memória const result = await extractor([`${type}: ${text}`], { pooling: 'mean', normalize: true, // Reduz batch size para economizar RAM batch_size: 1, // Usa menos threads num_threads: 1 }); const embedding = Array.from(result[0].data); res.json({ embedding }); // Force garbage collection se disponível if (global.gc) global.gc(); } catch (err) { console.error('Erro embed:', err.message); res.status(500).json({ error: 'Erro interno' }); } }); // Batch otimizado para múltiplos textos app.post('/api/embed/batch', async (req, res) => { try { if (!extractor) await init(); const { texts, type = 'query' } = req.body; if (!Array.isArray(texts)) return res.status(400).json({ error: 'Array necessário' }); // Processa em chunks pequenos para não estourar memória const chunkSize = 3; const embeddings = []; for (let i = 0; i < texts.length; i += chunkSize) { const chunk = texts.slice(i, i + chunkSize); const prefixed = chunk.map(text => `${type}: ${text}`); const results = await extractor(prefixed, { pooling: 'mean', normalize: true, batch_size: 1, num_threads: 1 }); embeddings.push(...results.map(r => Array.from(r.data))); // Pequena pausa entre chunks await new Promise(resolve => setTimeout(resolve, 10)); } res.json({ embeddings, count: embeddings.length }); if (global.gc) global.gc(); } catch (err) { console.error('Erro batch:', err.message); res.status(500).json({ error: 'Erro interno' }); } }); const PORT = process.env.PORT || 7860; // Timeout agressivo const timeout = setTimeout(() => { if (isInitializing) { console.log('⚠️ Timeout - reiniciando'); process.exit(1); } }, 90000); // 90s // Inicializa o modelo automaticamente init().catch(console.error); app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 E5 na porta ${PORT}`); }); // Cleanup process.on('SIGTERM', () => { console.log('📴 Finalizando...'); process.exit(0); }); process.on('uncaughtException', (err) => { console.error('💥 Erro crítico:', err.message); process.exit(1); });