NathMen12 commited on
Commit
d5d016f
·
verified ·
1 Parent(s): 9d45f3a

Upload 18 files

Browse files
Dockerfile.txt ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for Translator API
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy package files
7
+ COPY package*.json ./
8
+ COPY central/package*.json ./central/
9
+ COPY worker/package*.json ./worker/
10
+
11
+ # Install all dependencies
12
+ RUN npm ci && \
13
+ cd central && npm ci && \
14
+ cd ../worker && npm ci
15
+
16
+ # Production stage
17
+ FROM node:20-alpine AS production
18
+
19
+ WORKDIR /app
20
+
21
+ # Install dumb-init for proper signal handling
22
+ RUN apk add --no-cache dumb-init
23
+
24
+ # Create non-root user
25
+ RUN addgroup -g 1001 -S nodejs && \
26
+ adduser -S nodejs -u 1001
27
+
28
+ # Copy built dependencies
29
+ COPY --from=builder /app/node_modules ./node_modules
30
+ COPY --from=builder /app/central/node_modules ./central/node_modules
31
+ COPY --from=builder /app/worker/node_modules ./worker/node_modules
32
+
33
+ # Copy source code
34
+ COPY --chown=nodejs:nodejs package.json ./
35
+ COPY --chown=nodejs:nodejs central ./central
36
+ COPY --chown=nodejs:nodejs worker ./worker
37
+
38
+ # Create logs directory
39
+ RUN mkdir -p central/logs worker/logs && chown -R nodejs:nodejs central/logs worker/logs
40
+
41
+ # Switch to non-root user
42
+ USER nodejs
43
+
44
+ # Expose port (HuggingFace Spaces uses 7860)
45
+ EXPOSE 7860
46
+
47
+ # Environment variables
48
+ ENV PORT=7860
49
+ ENV NODE_ENV=production
50
+
51
+ # Health check
52
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
53
+ CMD wget --no-verbose --tries=1 --spider http://localhost:7860/api/health || exit 1
54
+
55
+ # Start central server
56
+ ENTRYPOINT ["dumb-init", "--"]
57
+ CMD ["node", "central/src/index.js"]
central/secrets/.env.example CHANGED
@@ -8,8 +8,8 @@ ADMIN_ACCESS_CODE=your-secure-admin-code-here
8
  # Tailscale API key for worker discovery (optional)
9
  TAILSCALE_API_KEY=tskey-xxxxxxxxx
10
 
11
- # Optional: Custom port (default 7820)
12
- # PORT=7820
13
 
14
  # Optional: Node environment
15
  # NODE_ENV=production
 
8
  # Tailscale API key for worker discovery (optional)
9
  TAILSCALE_API_KEY=tskey-xxxxxxxxx
10
 
11
+ # Optional: Custom port (default 7860)
12
+ # PORT=7860
13
 
14
  # Optional: Node environment
15
  # NODE_ENV=production
docker-compose.yml CHANGED
@@ -5,10 +5,10 @@ services:
5
  build: .
6
  container_name: translator-central
7
  ports:
8
- - "7820:7820"
9
  environment:
10
  - NODE_ENV=production
11
- - PORT=7820
12
  - ADMIN_ACCESS_CODE=${ADMIN_ACCESS_CODE}
13
  - TAILSCALE_API_KEY=${TAILSCALE_API_KEY}
14
  volumes:
@@ -23,7 +23,7 @@ services:
23
  cpus: '1'
24
  memory: 8G
25
  healthcheck:
26
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7820/api/health"]
27
  interval: 30s
28
  timeout: 10s
29
  retries: 3
@@ -36,7 +36,7 @@ services:
36
  environment:
37
  - NODE_ENV=production
38
  - CENTRAL_HOST=central
39
- - CENTRAL_PORT=7820
40
  depends_on:
41
  central:
42
  condition: service_healthy
 
5
  build: .
6
  container_name: translator-central
7
  ports:
8
+ - "7860:7860"
9
  environment:
10
  - NODE_ENV=production
11
+ - PORT=7860
12
  - ADMIN_ACCESS_CODE=${ADMIN_ACCESS_CODE}
13
  - TAILSCALE_API_KEY=${TAILSCALE_API_KEY}
14
  volumes:
 
23
  cpus: '1'
24
  memory: 8G
25
  healthcheck:
26
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7860/api/health"]
27
  interval: 30s
28
  timeout: 10s
29
  retries: 3
 
36
  environment:
37
  - NODE_ENV=production
38
  - CENTRAL_HOST=central
39
+ - CENTRAL_PORT=7860
40
  depends_on:
41
  central:
42
  condition: service_healthy
env.example CHANGED
@@ -9,7 +9,7 @@ ADMIN_ACCESS_CODE=your_strong_admin_code_here
9
  # Get from: https://login.tailscale.com/admin/settings/keys
10
  TAILSCALE_API_KEY=tskey-xxxxxxxxxxxxxxxxx
11
 
12
- # Port (default: 7820)
13
  PORT=7860
14
 
15
  # Node environment
 
9
  # Get from: https://login.tailscale.com/admin/settings/keys
10
  TAILSCALE_API_KEY=tskey-xxxxxxxxxxxxxxxxx
11
 
12
+ # Port (default: 7860)
13
  PORT=7860
14
 
15
  # Node environment
public/wiki.html CHANGED
@@ -87,10 +87,10 @@
87
 
88
  <h3>URL de base</h3>
89
  <p>Remplacez <code>VOTRE_HOTE</code> par l'adresse de votre instance :</p>
90
- <pre><code>https://VOTRE_HOTE:7820</code></pre>
91
 
92
  <h3>Test rapide</h3>
93
- <pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7820/translate \
94
  -H "Content-Type: application/json" \
95
  -d '{"text": "Bonjour le monde", "source": "fr", "target": "en"}'</code></pre>
96
 
@@ -172,7 +172,7 @@
172
  </div>
173
 
174
  <pre class="code-block active" id="js-fetch"><code class="language-javascript">async function translate(text, source = 'auto', target = 'fr') {
175
- const response = await fetch('https://VOTRE_HOTE:7820/translate', {
176
  method: 'POST',
177
  headers: { 'Content-Type': 'application/json' },
178
  body: JSON.stringify({ text, source, target })
@@ -195,7 +195,7 @@ translate('Bonjour le monde', 'fr', 'en')
195
 
196
  async function translate(text, source = 'auto', target = 'fr') {
197
  try {
198
- const { data } = await axios.post('https://VOTRE_HOTE:7820/translate', {
199
  text, source, target
200
  });
201
  return data;
@@ -216,7 +216,7 @@ function translate(text, source = 'auto', target = 'fr') {
216
 
217
  const options = {
218
  hostname: 'VOTRE_HOTE',
219
- port: 7820,
220
  path: '/translate',
221
  method: 'POST',
222
  headers: {
@@ -247,7 +247,7 @@ function translate(text, source = 'auto', target = 'fr') {
247
 
248
  <h3>Classe réutilisable (ES6)</h3>
249
  <pre><code class="language-javascript">class TranslatorAPI {
250
- constructor(baseUrl = 'https://VOTRE_HOTE:7820') {
251
  this.baseUrl = baseUrl;
252
  }
253
 
@@ -298,7 +298,7 @@ console.log(result.translatedText); // "Hola"</code></pre>
298
 
299
  <pre class="code-block active" id="py-requests"><code class="language-python">import requests
300
 
301
- def translate(text, source='auto', target='fr', base_url='https://VOTRE_HOTE:7820'):
302
  \"\"\"Traduit un texte via l'API Translator.\"\"\"
303
  response = requests.post(
304
  f'{base_url}/translate',
@@ -326,7 +326,7 @@ except requests.HTTPError as e:
326
  import asyncio
327
 
328
  class TranslatorClient:
329
- def __init__(self, base_url='https://VOTRE_HOTE:7820'):
330
  self.base_url = base_url
331
  self.client = httpx.AsyncClient(timeout=30.0)
332
 
@@ -372,7 +372,7 @@ asyncio.run(main())</code></pre>
372
  <pre class="code-block" id="py-stdlib"><code class="language-python">import urllib.request
373
  import json
374
 
375
- def translate(text, source='auto', target='fr', base_url='https://VOTRE_HOTE:7820'):
376
  data = json.dumps({'text': text, 'source': source, 'target': target}).encode('utf-8')
377
 
378
  req = urllib.request.Request(
@@ -459,7 +459,7 @@ public class TranslatorClient {
459
 
460
  // Usage
461
  public static void main(String[] args) throws Exception {
462
- TranslatorClient translator = new TranslatorClient("https://VOTRE_HOTE:7820");
463
  TranslationResult result = translator.translate("Bonjour", "fr", "en");
464
  System.out.println(result.translatedText); // "Hello"
465
  }
@@ -588,12 +588,12 @@ public class TranslatorClient {
588
  <h2>🌐 cURL / Ligne de commande</h2>
589
 
590
  <h3>Traduction simple</h3>
591
- <pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7820/translate \
592
  -H "Content-Type: application/json" \
593
  -d '{"text": "Bonjour le monde", "source": "fr", "target": "en"}'</code></pre>
594
 
595
  <h3>Avec détection automatique</h3>
596
- <pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7820/translate \
597
  -H "Content-Type: application/json" \
598
  -d '{"text": "Hello world", "target": "fr"}'</code></pre>
599
 
@@ -601,7 +601,7 @@ public class TranslatorClient {
601
  <pre><code class="language-bash">#!/bin/bash
602
  # translate.sh - Script de traduction en ligne de commande
603
 
604
- BASE_URL="https://VOTRE_HOTE:7820"
605
  SOURCE="auto"
606
  TARGET="fr"
607
 
@@ -639,7 +639,7 @@ echo "$translated"</code></pre>
639
  <h3>Test de charge simple</h3>
640
  <pre><code class="language-bash"># Tester le rate limit (30 req/min)
641
  for i in {1..35}; do
642
- curl -s -X POST https://VOTRE_HOTE:7820/translate \
643
  -H "Content-Type: application/json" \
644
  -d "{\"text\": \"Test $i\", \"target\": \"en\"}" \
645
  -w " HTTP %{http_code}\\n" \
 
87
 
88
  <h3>URL de base</h3>
89
  <p>Remplacez <code>VOTRE_HOTE</code> par l'adresse de votre instance :</p>
90
+ <pre><code>https://VOTRE_HOTE:7860</code></pre>
91
 
92
  <h3>Test rapide</h3>
93
+ <pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7860/translate \
94
  -H "Content-Type: application/json" \
95
  -d '{"text": "Bonjour le monde", "source": "fr", "target": "en"}'</code></pre>
96
 
 
172
  </div>
173
 
174
  <pre class="code-block active" id="js-fetch"><code class="language-javascript">async function translate(text, source = 'auto', target = 'fr') {
175
+ const response = await fetch('https://VOTRE_HOTE:7860/translate', {
176
  method: 'POST',
177
  headers: { 'Content-Type': 'application/json' },
178
  body: JSON.stringify({ text, source, target })
 
195
 
196
  async function translate(text, source = 'auto', target = 'fr') {
197
  try {
198
+ const { data } = await axios.post('https://VOTRE_HOTE:7860/translate', {
199
  text, source, target
200
  });
201
  return data;
 
216
 
217
  const options = {
218
  hostname: 'VOTRE_HOTE',
219
+ port: 7860,
220
  path: '/translate',
221
  method: 'POST',
222
  headers: {
 
247
 
248
  <h3>Classe réutilisable (ES6)</h3>
249
  <pre><code class="language-javascript">class TranslatorAPI {
250
+ constructor(baseUrl = 'https://VOTRE_HOTE:7860') {
251
  this.baseUrl = baseUrl;
252
  }
253
 
 
298
 
299
  <pre class="code-block active" id="py-requests"><code class="language-python">import requests
300
 
301
+ def translate(text, source='auto', target='fr', base_url='https://VOTRE_HOTE:7860'):
302
  \"\"\"Traduit un texte via l'API Translator.\"\"\"
303
  response = requests.post(
304
  f'{base_url}/translate',
 
326
  import asyncio
327
 
328
  class TranslatorClient:
329
+ def __init__(self, base_url='https://VOTRE_HOTE:7860'):
330
  self.base_url = base_url
331
  self.client = httpx.AsyncClient(timeout=30.0)
332
 
 
372
  <pre class="code-block" id="py-stdlib"><code class="language-python">import urllib.request
373
  import json
374
 
375
+ def translate(text, source='auto', target='fr', base_url='https://VOTRE_HOTE:7860'):
376
  data = json.dumps({'text': text, 'source': source, 'target': target}).encode('utf-8')
377
 
378
  req = urllib.request.Request(
 
459
 
460
  // Usage
461
  public static void main(String[] args) throws Exception {
462
+ TranslatorClient translator = new TranslatorClient("https://VOTRE_HOTE:7860");
463
  TranslationResult result = translator.translate("Bonjour", "fr", "en");
464
  System.out.println(result.translatedText); // "Hello"
465
  }
 
588
  <h2>🌐 cURL / Ligne de commande</h2>
589
 
590
  <h3>Traduction simple</h3>
591
+ <pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7860/translate \
592
  -H "Content-Type: application/json" \
593
  -d '{"text": "Bonjour le monde", "source": "fr", "target": "en"}'</code></pre>
594
 
595
  <h3>Avec détection automatique</h3>
596
+ <pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7860/translate \
597
  -H "Content-Type: application/json" \
598
  -d '{"text": "Hello world", "target": "fr"}'</code></pre>
599
 
 
601
  <pre><code class="language-bash">#!/bin/bash
602
  # translate.sh - Script de traduction en ligne de commande
603
 
604
+ BASE_URL="https://VOTRE_HOTE:7860"
605
  SOURCE="auto"
606
  TARGET="fr"
607
 
 
639
  <h3>Test de charge simple</h3>
640
  <pre><code class="language-bash"># Tester le rate limit (30 req/min)
641
  for i in {1..35}; do
642
+ curl -s -X POST https://VOTRE_HOTE:7860/translate \
643
  -H "Content-Type: application/json" \
644
  -d "{\"text\": \"Test $i\", \"target\": \"en\"}" \
645
  -w " HTTP %{http_code}\\n" \
worker/index.js ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ require('dotenv').config();
2
+ const WebSocket = require('ws');
3
+ const translate = require('@vitalets/google-translate-api');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const winston = require('winston');
8
+
9
+ const settings = JSON.parse(fs.readFileSync(path.join(__dirname, '../settings.json'), 'utf8'));
10
+
11
+ // Logger
12
+ const logger = winston.createLogger({
13
+ level: 'info',
14
+ format: winston.format.combine(
15
+ winston.format.timestamp(),
16
+ winston.format.json()
17
+ ),
18
+ transports: [
19
+ new winston.transports.Console(),
20
+ new winston.transports.File({ filename: path.join(__dirname, '../../logs/worker.log') })
21
+ ]
22
+ });
23
+
24
+ // Worker state
25
+ let workerId = null;
26
+ let ws = null;
27
+ let maxConcurrentJobs = settings.maxConcurrentJobs;
28
+ let activeJobs = 0;
29
+ let reconnectAttempts = 0;
30
+ const MAX_RECONNECT_ATTEMPTS = 10;
31
+ const RECONNECT_DELAY = 5000;
32
+
33
+ // Connect to central server
34
+ function connect() {
35
+ const url = `ws://${settings.centralHost}:${settings.centralPort}`;
36
+ logger.info(`Connecting to central server at ${url}`);
37
+
38
+ ws = new WebSocket(url);
39
+
40
+ ws.on('open', () => {
41
+ logger.info('Connected to central server');
42
+ reconnectAttempts = 0;
43
+ });
44
+
45
+ ws.on('message', (data) => {
46
+ try {
47
+ const msg = JSON.parse(data);
48
+ handleMessage(msg);
49
+ } catch (e) {
50
+ logger.error('Invalid message from central', e);
51
+ }
52
+ });
53
+
54
+ ws.on('close', () => {
55
+ logger.warn('Disconnected from central server');
56
+ scheduleReconnect();
57
+ });
58
+
59
+ ws.on('error', (err) => {
60
+ logger.error('WebSocket error', err);
61
+ });
62
+ }
63
+
64
+ function scheduleReconnect() {
65
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
66
+ logger.error('Max reconnect attempts reached, exiting');
67
+ process.exit(1);
68
+ }
69
+
70
+ reconnectAttempts++;
71
+ const delay = RECONNECT_DELAY * reconnectAttempts;
72
+ logger.info(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
73
+
74
+ setTimeout(connect, delay);
75
+ }
76
+
77
+ function handleMessage(msg) {
78
+ switch (msg.type) {
79
+ case 'welcome':
80
+ workerId = msg.workerId;
81
+ maxConcurrentJobs = msg.maxConcurrentJobs || maxConcurrentJobs;
82
+ logger.info(`Worker registered with ID: ${workerId}, max jobs: ${maxConcurrentJobs}`);
83
+ startHeartbeat();
84
+ break;
85
+
86
+ case 'translate':
87
+ handleTranslateJob(msg);
88
+ break;
89
+
90
+ case 'ping':
91
+ // Keep alive
92
+ ws.send(JSON.stringify({ type: 'pong' }));
93
+ break;
94
+ }
95
+ }
96
+
97
+ async function handleTranslateJob(msg) {
98
+ const { jobId, text, source, target } = msg;
99
+
100
+ if (activeJobs >= maxConcurrentJobs) {
101
+ logger.warn(`Job ${jobId} rejected: at capacity (${activeJobs}/${maxConcurrentJobs})`);
102
+ sendResult(jobId, { error: 'Worker at capacity' });
103
+ return;
104
+ }
105
+
106
+ activeJobs++;
107
+ logger.info(`Starting job ${jobId} (${activeJobs}/${maxConcurrentJobs})`);
108
+
109
+ try {
110
+ const result = await translate(text, { from: source, to: target });
111
+ sendResult(jobId, { translatedText: result.text });
112
+ } catch (error) {
113
+ logger.error(`Job ${jobId} failed`, error);
114
+ sendResult(jobId, { error: error.message });
115
+ } finally {
116
+ activeJobs--;
117
+ }
118
+ }
119
+
120
+ function sendResult(jobId, result) {
121
+ if (ws && ws.readyState === WebSocket.OPEN) {
122
+ ws.send(JSON.stringify({
123
+ type: 'result',
124
+ jobId,
125
+ result
126
+ }));
127
+ }
128
+ }
129
+
130
+ // Heartbeat with metrics
131
+ function startHeartbeat() {
132
+ setInterval(() => {
133
+ if (ws && ws.readyState === WebSocket.OPEN) {
134
+ const cpuUsage = process.cpuUsage();
135
+ const memUsage = process.memoryUsage();
136
+
137
+ ws.send(JSON.stringify({
138
+ type: 'heartbeat',
139
+ cpu: (cpuUsage.user + cpuUsage.system) / 10000, // Convert to percentage-ish
140
+ ram: memUsage.heapUsed,
141
+ jobsActive: activeJobs
142
+ }));
143
+ }
144
+ }, 5000);
145
+ }
146
+
147
+ // Handle graceful shutdown
148
+ process.on('SIGTERM', () => {
149
+ logger.info('SIGTERM received, shutting down gracefully');
150
+ if (ws) ws.close();
151
+ process.exit(0);
152
+ });
153
+
154
+ process.on('SIGINT', () => {
155
+ logger.info('SIGINT received, shutting down gracefully');
156
+ if (ws) ws.close();
157
+ process.exit(0);
158
+ });
159
+
160
+ // Start
161
+ logger.info('Starting Translator Worker');
162
+ connect();
worker/settings.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
  "maxConcurrentJobs": 2,
3
  "centralHost": "localhost",
4
- "centralPort": 7820
5
- }
 
1
  {
2
  "maxConcurrentJobs": 2,
3
  "centralHost": "localhost",
4
+ "centralPort": 7860
5
+ }