File size: 6,511 Bytes
bd2463e
 
 
 
40f299d
84ea3a0
bd2463e
 
 
84ea3a0
53561cf
 
bd2463e
40f299d
 
9389ef1
 
bd2463e
84ea3a0
bd2463e
40f299d
 
254b9d0
bd2463e
 
40f299d
bd2463e
84ea3a0
9389ef1
40f299d
84ea3a0
40f299d
 
 
 
254b9d0
40f299d
 
254b9d0
40f299d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9389ef1
c18ee50
40f299d
 
254b9d0
40f299d
 
 
 
 
 
254b9d0
 
40f299d
 
 
254b9d0
40f299d
 
 
9389ef1
40f299d
 
 
 
9389ef1
40f299d
 
 
 
 
 
 
 
 
 
 
 
 
9389ef1
40f299d
bd2463e
40f299d
 
9389ef1
 
 
 
 
 
40f299d
9389ef1
bd2463e
40f299d
9389ef1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40f299d
 
 
9389ef1
 
40f299d
9389ef1
 
40f299d
9389ef1
 
 
 
 
 
 
 
 
 
 
 
 
 
40f299d
9389ef1
 
 
bd2463e
 
 
84ea3a0
40f299d
 
 
 
9389ef1
 
40f299d
9389ef1
40f299d
84ea3a0
c18ee50
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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'));