Spaces:
Build error
Build error
| 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')); | |