danicor commited on
Commit
760f6be
·
verified ·
1 Parent(s): 8468688

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +645 -0
app.py ADDED
@@ -0,0 +1,645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import asyncio
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Dict, List, Optional
7
+ from collections import deque
8
+ from fastapi import FastAPI, HTTPException, Request
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from pydantic import BaseModel
11
+ import requests
12
+ import threading
13
+
14
+ # Configuration
15
+ ORCHESTRATOR_VERSION = "1.0.0"
16
+ WORKER_HEALTH_CHECK_INTERVAL = 30 # seconds
17
+ WORKER_TIMEOUT = 120 # seconds for translation
18
+
19
+ # Models
20
+ class TranslationRequest(BaseModel):
21
+ request_id: str
22
+ text: str
23
+ source_lang: str
24
+ target_lang: str
25
+ auto_charge: bool = False
26
+ notification_url: Optional[str] = None
27
+ wordpress_user_id: Optional[int] = None
28
+
29
+ class WorkerConfig(BaseModel):
30
+ url: str
31
+ name: str
32
+ priority: int = 1
33
+ max_concurrent: int = 3
34
+
35
+ class WorkerStatus:
36
+ def __init__(self, config: WorkerConfig):
37
+ self.config = config
38
+ self.available = False
39
+ self.active_jobs = 0
40
+ self.last_health_check = 0
41
+ self.total_completed = 0
42
+ self.total_failed = 0
43
+ self.avg_response_time = 0.0
44
+
45
+ # Orchestrator Core Class
46
+ class TranslationOrchestrator:
47
+ def __init__(self):
48
+ self.workers: Dict[str, WorkerStatus] = {}
49
+ self.job_queue = deque()
50
+ self.active_jobs: Dict[str, Dict] = {}
51
+ self.completed_jobs: Dict[str, Dict] = {}
52
+ self.worker_lock = threading.Lock()
53
+ self.job_lock = threading.Lock()
54
+
55
+ # Load worker configurations from environment
56
+ self.load_worker_configs()
57
+
58
+ # Start background tasks
59
+ self.start_health_checker()
60
+ self.start_job_processor()
61
+
62
+ def load_worker_configs(self):
63
+ """Load worker configurations from environment variables"""
64
+ # Format: WORKER_1_URL=https://..., WORKER_1_NAME=Worker1, etc.
65
+ worker_index = 1
66
+ while True:
67
+ url_key = f"WORKER_{worker_index}_URL"
68
+ name_key = f"WORKER_{worker_index}_NAME"
69
+
70
+ url = os.getenv(url_key)
71
+ if not url:
72
+ break
73
+
74
+ name = os.getenv(name_key, f"Worker {worker_index}")
75
+ priority = int(os.getenv(f"WORKER_{worker_index}_PRIORITY", "1"))
76
+ max_concurrent = int(os.getenv(f"WORKER_{worker_index}_MAX_CONCURRENT", "3"))
77
+
78
+ worker_id = f"worker_{worker_index}"
79
+ config = WorkerConfig(
80
+ url=url,
81
+ name=name,
82
+ priority=priority,
83
+ max_concurrent=max_concurrent
84
+ )
85
+
86
+ self.workers[worker_id] = WorkerStatus(config)
87
+ print(f"✓ Loaded worker: {name} at {url}")
88
+
89
+ worker_index += 1
90
+
91
+ if not self.workers:
92
+ print("⚠ No workers configured. Add WORKER_N_URL environment variables.")
93
+
94
+
95
+ def start_health_checker(self):
96
+ """Start background thread for health checking"""
97
+ def health_check_loop():
98
+ while True:
99
+ self.check_all_workers_health()
100
+ time.sleep(WORKER_HEALTH_CHECK_INTERVAL)
101
+
102
+ thread = threading.Thread(target=health_check_loop, daemon=True)
103
+ thread.start()
104
+ print("✓ Health checker started")
105
+
106
+ def check_all_workers_health(self):
107
+ """Check health of all workers"""
108
+ with self.worker_lock:
109
+ for worker_id, worker in self.workers.items():
110
+ try:
111
+ response = requests.get(
112
+ f"{worker.config.url}/api/health",
113
+ timeout=10
114
+ )
115
+
116
+ if response.status_code == 200:
117
+ data = response.json()
118
+ worker.available = data.get('status') == 'healthy'
119
+ worker.last_health_check = time.time()
120
+
121
+ if worker.available:
122
+ print(f"✓ {worker.config.name}: Healthy")
123
+ else:
124
+ print(f"✗ {worker.config.name}: Unhealthy")
125
+ else:
126
+ worker.available = False
127
+ print(f"✗ {worker.config.name}: HTTP {response.status_code}")
128
+
129
+ except Exception as e:
130
+ worker.available = False
131
+ print(f"✗ {worker.config.name}: {str(e)}")
132
+
133
+ def get_available_worker(self) -> Optional[str]:
134
+ """Get an available worker based on priority and load"""
135
+ with self.worker_lock:
136
+ available_workers = [
137
+ (worker_id, worker)
138
+ for worker_id, worker in self.workers.items()
139
+ if worker.available and worker.active_jobs < worker.config.max_concurrent
140
+ ]
141
+
142
+ if not available_workers:
143
+ return None
144
+
145
+ # Sort by priority (higher first) then by active jobs (fewer first)
146
+ available_workers.sort(
147
+ key=lambda x: (-x[1].config.priority, x[1].active_jobs)
148
+ )
149
+
150
+ return available_workers[0][0]
151
+
152
+ def start_job_processor(self):
153
+ """Start background thread for processing job queue"""
154
+ def process_queue_loop():
155
+ while True:
156
+ self.process_job_queue()
157
+ time.sleep(2) # Check queue every 2 seconds
158
+
159
+ thread = threading.Thread(target=process_queue_loop, daemon=True)
160
+ thread.start()
161
+ print("✓ Job processor started")
162
+
163
+ def add_job_to_queue(self, job_data: Dict):
164
+ """Add a job to the queue"""
165
+ with self.job_lock:
166
+ self.job_queue.append(job_data)
167
+ print(f"📝 Job {job_data['request_id']} added to queue. Queue size: {len(self.job_queue)}")
168
+
169
+ def process_job_queue(self):
170
+ """Process pending jobs in queue"""
171
+ if not self.job_queue:
172
+ return
173
+
174
+ with self.job_lock:
175
+ if not self.job_queue:
176
+ return
177
+
178
+ worker_id = self.get_available_worker()
179
+ if not worker_id:
180
+ return # No available workers
181
+
182
+ job_data = self.job_queue.popleft()
183
+
184
+ # Process outside lock to avoid blocking
185
+ self.assign_job_to_worker(worker_id, job_data)
186
+
187
+ def assign_job_to_worker(self, worker_id: str, job_data: Dict):
188
+ """Assign a job to a specific worker"""
189
+ worker = self.workers[worker_id]
190
+ request_id = job_data['request_id']
191
+
192
+ print(f"🔄 Assigning job {request_id} to {worker.config.name}")
193
+
194
+ with self.worker_lock:
195
+ worker.active_jobs += 1
196
+
197
+ # Store job info
198
+ self.active_jobs[request_id] = {
199
+ 'worker_id': worker_id,
200
+ 'job_data': job_data,
201
+ 'start_time': time.time(),
202
+ 'status': 'processing'
203
+ }
204
+
205
+ # Send to worker in background
206
+ thread = threading.Thread(
207
+ target=self.send_to_worker,
208
+ args=(worker_id, job_data),
209
+ daemon=True
210
+ )
211
+ thread.start()
212
+
213
+ def send_to_worker(self, worker_id: str, job_data: Dict):
214
+ """Send translation job to worker"""
215
+ worker = self.workers[worker_id]
216
+ request_id = job_data['request_id']
217
+
218
+ try:
219
+ # Prepare request for worker
220
+ worker_request = {
221
+ 'request_id': request_id,
222
+ 'text': job_data['text'],
223
+ 'source_lang': job_data['source_lang'],
224
+ 'target_lang': job_data['target_lang'],
225
+ 'auto_charge': job_data.get('auto_charge', False),
226
+ 'notification_url': None # Don't let worker notify WordPress directly
227
+ }
228
+
229
+ response = requests.post(
230
+ f"{worker.config.url}/api/translate/heavy",
231
+ json=worker_request,
232
+ timeout=WORKER_TIMEOUT
233
+ )
234
+
235
+ if response.status_code == 200:
236
+ print(f"✓ Job {request_id} sent to {worker.config.name}")
237
+ self.monitor_worker_job(worker_id, request_id)
238
+ else:
239
+ self.handle_worker_failure(worker_id, request_id, f"HTTP {response.status_code}")
240
+
241
+ except Exception as e:
242
+ self.handle_worker_failure(worker_id, request_id, str(e))
243
+
244
+ def monitor_worker_job(self, worker_id: str, request_id: str):
245
+ """Monitor job progress on worker"""
246
+ worker = self.workers[worker_id]
247
+ max_checks = 60 # Maximum 5 minutes (60 * 5 seconds)
248
+ check_count = 0
249
+
250
+ while check_count < max_checks:
251
+ time.sleep(5)
252
+ check_count += 1
253
+
254
+ try:
255
+ # Check status on worker
256
+ response = requests.post(
257
+ f"{worker.config.url}/api/check-translation-status",
258
+ json={'request_id': request_id},
259
+ timeout=15
260
+ )
261
+
262
+ if response.status_code == 200:
263
+ data = response.json()
264
+
265
+ if data.get('success') and data.get('status') == 'completed':
266
+ # Job completed successfully
267
+ self.handle_job_completion(worker_id, request_id, data)
268
+ return
269
+ elif data.get('status') == 'failed':
270
+ self.handle_worker_failure(worker_id, request_id, "Worker reported failure")
271
+ return
272
+
273
+ except Exception as e:
274
+ print(f"⚠ Error checking job {request_id}: {str(e)}")
275
+
276
+ # Timeout reached
277
+ self.handle_worker_failure(worker_id, request_id, "Timeout waiting for completion")
278
+
279
+
280
+
281
+
282
+ def handle_job_completion(self, worker_id: str, request_id: str, worker_response: Dict):
283
+ """Handle successful job completion"""
284
+ worker = self.workers[worker_id]
285
+
286
+ print(f"✅ Job {request_id} completed by {worker.config.name}")
287
+
288
+ # Update worker stats
289
+ with self.worker_lock:
290
+ worker.active_jobs -= 1
291
+ worker.total_completed += 1
292
+
293
+ # Get job data
294
+ job_info = self.active_jobs.get(request_id, {})
295
+ job_data = job_info.get('job_data', {})
296
+
297
+ # Calculate processing time
298
+ processing_time = time.time() - job_info.get('start_time', time.time())
299
+
300
+ # Prepare completion data
301
+ completion_data = {
302
+ 'request_id': request_id,
303
+ 'status': 'completed',
304
+ 'translated_text': worker_response.get('translated_text'),
305
+ 'processing_time': processing_time,
306
+ 'character_count': worker_response.get('character_count', len(job_data.get('text', ''))),
307
+ 'translation_length': worker_response.get('translation_length', 0),
308
+ 'from_cache': worker_response.get('from_cache', False),
309
+ 'worker_name': worker.config.name,
310
+ 'completed_at': datetime.now().isoformat()
311
+ }
312
+
313
+ # Store in completed jobs
314
+ self.completed_jobs[request_id] = completion_data
315
+
316
+ # Remove from active jobs
317
+ if request_id in self.active_jobs:
318
+ del self.active_jobs[request_id]
319
+
320
+ # Notify WordPress if notification URL provided
321
+ notification_url = job_data.get('notification_url')
322
+ if notification_url:
323
+ self.notify_wordpress(notification_url, completion_data)
324
+
325
+ def handle_worker_failure(self, worker_id: str, request_id: str, error_message: str):
326
+ """Handle job failure"""
327
+ worker = self.workers[worker_id]
328
+
329
+ print(f"❌ Job {request_id} failed on {worker.config.name}: {error_message}")
330
+
331
+ # Update worker stats
332
+ with self.worker_lock:
333
+ worker.active_jobs -= 1
334
+ worker.total_failed += 1
335
+
336
+ # Get job data
337
+ job_info = self.active_jobs.get(request_id, {})
338
+ job_data = job_info.get('job_data', {})
339
+
340
+ # Try to reassign to another worker
341
+ if job_info.get('retry_count', 0) < 2: # Maximum 2 retries
342
+ print(f"🔄 Retrying job {request_id} (attempt {job_info.get('retry_count', 0) + 1})")
343
+
344
+ job_data['retry_count'] = job_info.get('retry_count', 0) + 1
345
+ self.add_job_to_queue(job_data)
346
+
347
+ # Remove from active jobs
348
+ if request_id in self.active_jobs:
349
+ del self.active_jobs[request_id]
350
+ else:
351
+ # Max retries reached, mark as failed
352
+ failure_data = {
353
+ 'request_id': request_id,
354
+ 'status': 'failed',
355
+ 'error_message': error_message,
356
+ 'failed_at': datetime.now().isoformat()
357
+ }
358
+
359
+ self.completed_jobs[request_id] = failure_data
360
+
361
+ # Remove from active jobs
362
+ if request_id in self.active_jobs:
363
+ del self.active_jobs[request_id]
364
+
365
+ # Notify WordPress of failure
366
+ notification_url = job_data.get('notification_url')
367
+ if notification_url:
368
+ self.notify_wordpress(notification_url, failure_data)
369
+
370
+ def notify_wordpress(self, notification_url: str, data: Dict):
371
+ """Send notification to WordPress"""
372
+ try:
373
+ response = requests.post(
374
+ notification_url,
375
+ json=data,
376
+ timeout=30,
377
+ verify=False
378
+ )
379
+
380
+ if response.status_code == 200:
381
+ print(f"📤 WordPress notified successfully for {data['request_id']}")
382
+ else:
383
+ print(f"⚠ WordPress notification failed: HTTP {response.status_code}")
384
+
385
+ except Exception as e:
386
+ print(f"⚠ WordPress notification error: {str(e)}")
387
+
388
+ # Initialize FastAPI app
389
+ app = FastAPI(
390
+ title="Translation Orchestrator",
391
+ description="Main orchestrator for distributed translation system",
392
+ version=ORCHESTRATOR_VERSION
393
+ )
394
+
395
+ # CORS configuration
396
+ app.add_middleware(
397
+ CORSMiddleware,
398
+ allow_origins=["*"],
399
+ allow_credentials=True,
400
+ allow_methods=["*"],
401
+ allow_headers=["*"],
402
+ )
403
+
404
+ # Initialize orchestrator
405
+ orchestrator = TranslationOrchestrator()
406
+
407
+ # Root endpoint
408
+ @app.get("/")
409
+ async def root():
410
+ """API information"""
411
+ return {
412
+ "service": "Translation Orchestrator",
413
+ "version": ORCHESTRATOR_VERSION,
414
+ "status": "running",
415
+ "workers": len(orchestrator.workers),
416
+ "active_jobs": len(orchestrator.active_jobs),
417
+ "queued_jobs": len(orchestrator.job_queue),
418
+ "endpoints": {
419
+ "/api/translate": "Submit translation request",
420
+ "/api/status/{request_id}": "Check translation status",
421
+ "/api/workers": "List all workers",
422
+ "/api/health": "Health check"
423
+ }
424
+ }
425
+
426
+ # Health check endpoint
427
+ @app.get("/api/health")
428
+ async def health_check():
429
+ """Health check"""
430
+ available_workers = sum(1 for w in orchestrator.workers.values() if w.available)
431
+
432
+ return {
433
+ "status": "healthy" if available_workers > 0 else "degraded",
434
+ "timestamp": datetime.now().isoformat(),
435
+ "workers": {
436
+ "total": len(orchestrator.workers),
437
+ "available": available_workers,
438
+ "unavailable": len(orchestrator.workers) - available_workers
439
+ },
440
+ "queue": {
441
+ "active_jobs": len(orchestrator.active_jobs),
442
+ "queued_jobs": len(orchestrator.job_queue),
443
+ "completed_jobs": len(orchestrator.completed_jobs)
444
+ }
445
+ }
446
+
447
+ # Submit translation request
448
+ @app.post("/api/translate")
449
+ async def submit_translation(request: TranslationRequest):
450
+ """Submit a translation request"""
451
+
452
+ # Check if any workers available
453
+ if not any(w.available for w in orchestrator.workers.values()):
454
+ raise HTTPException(
455
+ status_code=503,
456
+ detail="No workers available at the moment. Please try again later."
457
+ )
458
+
459
+ # Prepare job data
460
+ job_data = {
461
+ 'request_id': request.request_id,
462
+ 'text': request.text,
463
+ 'source_lang': request.source_lang,
464
+ 'target_lang': request.target_lang,
465
+ 'auto_charge': request.auto_charge,
466
+ 'notification_url': request.notification_url,
467
+ 'wordpress_user_id': request.wordpress_user_id,
468
+ 'retry_count': 0
469
+ }
470
+
471
+ # Add to queue
472
+ orchestrator.add_job_to_queue(job_data)
473
+
474
+ return {
475
+ "success": True,
476
+ "request_id": request.request_id,
477
+ "status": "queued",
478
+ "message": "Translation request queued successfully",
479
+ "queue_position": len(orchestrator.job_queue),
480
+ "estimated_wait_time": len(orchestrator.job_queue) * 10 # Rough estimate
481
+ }
482
+
483
+ # Check translation status
484
+ @app.get("/api/status/{request_id}")
485
+ async def check_status(request_id: str):
486
+ """Check status of a translation request"""
487
+
488
+ # Check completed jobs
489
+ if request_id in orchestrator.completed_jobs:
490
+ return {
491
+ "success": True,
492
+ **orchestrator.completed_jobs[request_id]
493
+ }
494
+
495
+ # Check active jobs
496
+ if request_id in orchestrator.active_jobs:
497
+ job_info = orchestrator.active_jobs[request_id]
498
+ elapsed_time = time.time() - job_info['start_time']
499
+
500
+ return {
501
+ "success": True,
502
+ "request_id": request_id,
503
+ "status": "processing",
504
+ "worker_id": job_info['worker_id'],
505
+ "elapsed_time": elapsed_time,
506
+ "message": "Translation in progress"
507
+ }
508
+
509
+ # Check queue
510
+ for job in orchestrator.job_queue:
511
+ if job['request_id'] == request_id:
512
+ return {
513
+ "success": True,
514
+ "request_id": request_id,
515
+ "status": "queued",
516
+ "message": "Translation request is queued"
517
+ }
518
+
519
+ # Not found
520
+ return {
521
+ "success": False,
522
+ "request_id": request_id,
523
+ "status": "not_found",
524
+ "message": "Translation request not found"
525
+ }
526
+
527
+ # List workers
528
+ @app.get("/api/workers")
529
+ async def list_workers():
530
+ """List all workers and their status"""
531
+ workers_info = []
532
+
533
+ for worker_id, worker in orchestrator.workers.items():
534
+ workers_info.append({
535
+ "id": worker_id,
536
+ "name": worker.config.name,
537
+ "url": worker.config.url,
538
+ "available": worker.available,
539
+ "active_jobs": worker.active_jobs,
540
+ "max_concurrent": worker.config.max_concurrent,
541
+ "priority": worker.config.priority,
542
+ "total_completed": worker.total_completed,
543
+ "total_failed": worker.total_failed,
544
+ "last_health_check": worker.last_health_check
545
+ })
546
+
547
+ return {
548
+ "success": True,
549
+ "workers": workers_info
550
+ }
551
+
552
+ # Add worker dynamically (admin endpoint)
553
+ @app.post("/api/admin/add-worker")
554
+ async def add_worker(config: WorkerConfig):
555
+ """Add a new worker (admin only - add authentication in production)"""
556
+
557
+ worker_id = f"worker_{len(orchestrator.workers) + 1}"
558
+
559
+ with orchestrator.worker_lock:
560
+ orchestrator.workers[worker_id] = WorkerStatus(config)
561
+
562
+ # Immediate health check
563
+ orchestrator.check_all_workers_health()
564
+
565
+ return {
566
+ "success": True,
567
+ "worker_id": worker_id,
568
+ "message": f"Worker {config.name} added successfully"
569
+ }
570
+
571
+ # Remove worker
572
+ @app.delete("/api/admin/remove-worker/{worker_id}")
573
+ async def remove_worker(worker_id: str):
574
+ """Remove a worker (admin only - add authentication in production)"""
575
+
576
+ if worker_id not in orchestrator.workers:
577
+ raise HTTPException(status_code=404, detail="Worker not found")
578
+
579
+ worker = orchestrator.workers[worker_id]
580
+
581
+ # Check if worker has active jobs
582
+ if worker.active_jobs > 0:
583
+ raise HTTPException(
584
+ status_code=400,
585
+ detail=f"Cannot remove worker with {worker.active_jobs} active jobs"
586
+ )
587
+
588
+ with orchestrator.worker_lock:
589
+ del orchestrator.workers[worker_id]
590
+
591
+ return {
592
+ "success": True,
593
+ "message": f"Worker {worker_id} removed successfully"
594
+ }
595
+
596
+ # Force health check
597
+ @app.post("/api/admin/health-check")
598
+ async def force_health_check():
599
+ """Force immediate health check of all workers"""
600
+ orchestrator.check_all_workers_health()
601
+
602
+ return {
603
+ "success": True,
604
+ "message": "Health check completed",
605
+ "timestamp": datetime.now().isoformat()
606
+ }
607
+
608
+ # Get statistics
609
+ @app.get("/api/stats")
610
+ async def get_statistics():
611
+ """Get orchestrator statistics"""
612
+
613
+ total_completed = sum(w.total_completed for w in orchestrator.workers.values())
614
+ total_failed = sum(w.total_failed for w in orchestrator.workers.values())
615
+
616
+ return {
617
+ "success": True,
618
+ "statistics": {
619
+ "total_workers": len(orchestrator.workers),
620
+ "available_workers": sum(1 for w in orchestrator.workers.values() if w.available),
621
+ "active_jobs": len(orchestrator.active_jobs),
622
+ "queued_jobs": len(orchestrator.job_queue),
623
+ "completed_jobs": len(orchestrator.completed_jobs),
624
+ "total_completed": total_completed,
625
+ "total_failed": total_failed,
626
+ "success_rate": (total_completed / (total_completed + total_failed) * 100) if (total_completed + total_failed) > 0 else 0
627
+ }
628
+ }
629
+
630
+ # Main entry point
631
+ if __name__ == "__main__":
632
+ import uvicorn
633
+
634
+ port = int(os.getenv("PORT", 7860))
635
+
636
+ print(f"🚀 Translation Orchestrator v{ORCHESTRATOR_VERSION}")
637
+ print(f"📡 Starting on port {port}")
638
+ print(f"👷 Workers configured: {len(orchestrator.workers)}")
639
+
640
+ uvicorn.run(
641
+ app,
642
+ host="0.0.0.0",
643
+ port=port,
644
+ log_level="info"
645
+ )