import express from 'express'; import multer from 'multer'; import AdmZip from 'adm-zip'; import { pipeline, env } from '@xenova/transformers'; import fs from 'fs'; env.allowLocalModels = false; env.useBrowserCache = false; const app = express(); const upload = multer({ storage: multer.memoryStorage() }); const LANG_MAP = { 'fr_fr': 'fra_Latn', 'es_es': 'spa_Latn', 'de_de': 'deu_Latn', 'it_it': 'ita_Latn', 'pt_pt': 'por_Latn', 'ru_ru': 'rus_Cyrl', 'ja_jp': 'jpn_Jpan', 'zh_cn': 'zho_Hans', 'ko_kr': 'kor_Hang', 'pl_pl': 'pol_Latn', 'uk_ua': 'ukr_Cyrl', 'tr_tr': 'tur_Latn', 'nl_nl': 'nld_Latn', 'sv_se': 'swe_Latn', 'da_dk': 'dan_Latn', 'no_no': 'nob_Latn' }; let translator; // Stockage temporaire des tâches de traduction en cours const jobs = {}; pipeline('translation', 'Xenova/nllb-200-distilled-600M').then(model => { translator = model; console.log("✅ Modèle d'IA prêt !"); }); app.use(express.static('public')); app.use(express.json()); // Petite pause pour laisser respirer l'événement de Node.js (évite de figer le serveur) const breathe = () => new Promise(resolve => setImmediate(resolve)); // API : Lecture des limites réelles du conteneur Docker (cgroups Linux) app.get('/api/stats', (req, res) => { let usedRam = 0; let totalRam = 16; // Valeur par défaut sur HF Basic CPU try { // Lecture de la RAM consommée par le Docker if (fs.existsSync('/sys/fs/cgroup/memory/memory.usage_in_bytes')) { usedRam = parseInt(fs.readFileSync('/sys/fs/cgroup/memory/memory.usage_in_bytes', 'utf8'), 10); totalRam = parseInt(fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8'), 10); } else if (fs.existsSync('/sys/fs/cgroup/memory.current')) { // cgroups v2 usedRam = parseInt(fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf8'), 10); totalRam = parseInt(fs.readFileSync('/sys/fs/cgroup/memory.max', 'utf8'), 10); } usedRam = usedRam / (1024 ** 3); totalRam = totalRam / (1024 ** 3); } catch (e) { usedRam = 3.5; // Fallback visuel si hors-linux } // Si l'IA tourne, on simule l'usage CPU alloué au conteneur const activeJobs = Object.values(jobs).some(j => !j.done && !j.error); const cpuPercent = activeJobs ? Math.floor(Math.random() * 20) + 75 : 1; res.json({ cpu: cpuPercent, usedRam: usedRam.toFixed(1), totalRam: Math.round(totalRam), ramPercent: Math.round((usedRam / totalRam) * 100) }); }); // Route 1 : Soumission du fichier (Réponse immédiate) app.post('/api/translate', upload.array('mods'), (req, res) => { if (!translator) return res.status(503).send("L'IA charge encore son modèle."); let targetLangs = req.body.langs; if (!targetLangs) return res.status(400).send("Aucune langue sélectionnée."); if (!Array.isArray(targetLangs)) targetLangs = [targetLangs]; const jobId = Date.now().toString(); jobs[jobId] = { percent: 0, status: "Fichier reçu, analyse...", done: false, error: null, base64: null }; res.json({ jobId }); // Lancement asynchrone en tâche de fond processTranslation(jobId, req.files, targetLangs); }); // Route 2 : Le navigateur demande l'état de sa tâche app.get('/api/status/:id', (req, res) => { const job = jobs[req.params.id]; if (!job) return res.status(404).json({ error: "Tâche introuvable" }); res.json(job); // Nettoyage de la mémoire si c'est fini pour éviter de saturer le Docker if (job.done || job.error) { setTimeout(() => { delete jobs[req.params.id]; }, 60000); } }); async function processTranslation(jobId, files, targetLangs) { const job = jobs[jobId]; try { const outZip = new AdmZip(); outZip.addFile('pack.mcmeta', Buffer.from(JSON.stringify({ pack: { pack_format: 15, description: "Traductions générées par IA" } }, null, 2))); let totalTasks = files.length * targetLangs.length; let taskCount = 0; for (const file of files) { const modZip = new AdmZip(file.buffer); const entries = modZip.getEntries(); const enEntries = entries.filter(e => e.entryName.match(/^assets\/([^\/]+)\/lang\/en_us\.json$/)); if (enEntries.length === 0) { taskCount += targetLangs.length; continue; } for (const enEntry of enEntries) { const namespace = enEntry.entryName.split('/')[1]; const enJson = JSON.parse(modZip.readAsText(enEntry)); const keys = Object.keys(enJson); const texts = Object.values(enJson); for (const lang of targetLangs) { taskCount++; job.percent = Math.min(95, Math.floor((taskCount / totalTasks) * 90)); job.status = `Traduction [${lang}] de ${namespace}...`; await breathe(); const expectedPath = `assets/${namespace}/lang/${lang}.json`; if (entries.some(e => e.entryName === expectedPath)) continue; const translatedJson = {}; const batchSize = 4; for (let i = 0; i < texts.length; i += batchSize) { const batchTexts = texts.slice(i, i + batchSize); const batchKeys = keys.slice(i, i + batchSize); const promises = batchTexts.map(text => translator(text, { src_lang: 'eng_Latn', tgt_lang: LANG_MAP[lang] || 'fra_Latn' }) ); const batchResults = await Promise.all(promises); batchResults.forEach((res, index) => { translatedJson[batchKeys[index]] = res[0].translation_text; }); await breathe(); } outZip.addFile(expectedPath, Buffer.from(JSON.stringify(translatedJson, null, 2), 'utf-8')); } } } job.percent = 100; job.status = "Terminé !"; job.base64 = outZip.toBuffer().toString('base64'); job.done = true; } catch (err) { job.error = err.message; } } app.listen(7860, '0.0.0.0', () => console.log('🚀 Serveur prêt sur le port 7860'));