Chat-With-AI / server.js
NathMen12's picture
Update server.js
c18ee50 verified
Raw
History Blame Contribute Delete
6.51 kB
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'));