danicor commited on
Commit
fc0bb73
·
verified ·
1 Parent(s): b563fff

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1083 -0
app.py ADDED
@@ -0,0 +1,1083 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import hashlib
4
+ import threading
5
+ import asyncio
6
+ import re
7
+ from datetime import datetime, timedelta
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from typing import Dict, List, Optional, Any
10
+ import json
11
+
12
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.responses import JSONResponse
15
+ from pydantic import BaseModel, Field
16
+ import torch
17
+ from transformers import M2M100ForConditionalGeneration, M2M100Tokenizer
18
+ import requests
19
+
20
+ # تنظیمات اولیه
21
+ MODEL_NAME = "facebook/m2m100_418M"
22
+ CACHE_EXPIRY = 60 * 60 # 60 دقیقه
23
+ MAX_CHUNK_SIZE = 350
24
+ MAX_WORKERS = 3
25
+ CLEANUP_INTERVAL = 300 # 5 دقیقه
26
+
27
+ # نگاشت زبان‌ها
28
+ LANGUAGE_MAP = {
29
+ "English": "en",
30
+ "Persian": "fa",
31
+ "Farsi": "fa",
32
+ "Arabic": "ar",
33
+ "Spanish": "es",
34
+ "French": "fr",
35
+ "German": "de",
36
+ "Italian": "it",
37
+ "Portuguese": "pt",
38
+ "Russian": "ru",
39
+ "Chinese": "zh",
40
+ "Japanese": "ja",
41
+ "Korean": "ko",
42
+ "Hindi": "hi",
43
+ "Turkish": "tr",
44
+ "Dutch": "nl",
45
+ "Swedish": "sv",
46
+ "Norwegian": "no",
47
+ "Danish": "da",
48
+ "Finnish": "fi",
49
+ "Polish": "pl",
50
+ "Czech": "cs",
51
+ "Hungarian": "hu",
52
+ "Romanian": "ro",
53
+ "Greek": "el",
54
+ "Hebrew": "he",
55
+ "Thai": "th",
56
+ "Vietnamese": "vi",
57
+ "Indonesian": "id",
58
+ "Malay": "ms",
59
+ "Tamil": "ta",
60
+ "Bengali": "bn",
61
+ "Urdu": "ur",
62
+ "Ukrainian": "uk",
63
+ "Bulgarian": "bg",
64
+ "Croatian": "hr",
65
+ "Serbian": "sr",
66
+ "Slovak": "sk",
67
+ "Slovenian": "sl",
68
+ "Estonian": "et",
69
+ "Latvian": "lv",
70
+ "Lithuanian": "lt",
71
+ "Maltese": "mt",
72
+ "Catalan": "ca",
73
+ "Galician": "gl",
74
+ "Basque": "eu",
75
+ "Welsh": "cy",
76
+ "Irish": "ga",
77
+ "Scottish": "gd",
78
+ "Icelandic": "is",
79
+ "Albanian": "sq",
80
+ "Macedonian": "mk",
81
+ "Bosnian": "bs",
82
+ "Montenegrin": "cnr",
83
+ "Swahili": "sw",
84
+ "Amharic": "am",
85
+ "Yoruba": "yo",
86
+ "Igbo": "ig",
87
+ "Hausa": "ha",
88
+ "Somali": "so",
89
+ "Oromo": "om",
90
+ "Tigrinya": "ti",
91
+ "Afrikaans": "af",
92
+ "Zulu": "zu",
93
+ "Xhosa": "xh",
94
+ "Sotho": "st",
95
+ "Tswana": "tn",
96
+ "Tsonga": "ts",
97
+ "Venda": "ve",
98
+ "Ndebele": "nr",
99
+ "Gujarati": "gu",
100
+ "Punjabi": "pa",
101
+ "Telugu": "te",
102
+ "Kannada": "kn",
103
+ "Malayalam": "ml",
104
+ "Marathi": "mr",
105
+ "Nepali": "ne",
106
+ "Sinhala": "si",
107
+ "Burmese": "my",
108
+ "Khmer": "km",
109
+ "Lao": "lo",
110
+ "Mongolian": "mn",
111
+ "Kazakh": "kk",
112
+ "Uzbek": "uz",
113
+ "Tajik": "tg",
114
+ "Kyrgyz": "ky",
115
+ "Turkmen": "tk",
116
+ "Azerbaijani": "az",
117
+ "Georgian": "ka",
118
+ "Armenian": "hy"
119
+ }
120
+
121
+ # مدل‌های Pydantic
122
+ class TranslationRequest(BaseModel):
123
+ text: str = Field(..., description="متن برای ترجمه")
124
+ source_lang: str = Field(..., description="زبان مبدا")
125
+ target_lang: str = Field(..., description="زبان مقصد")
126
+ auto_charge: bool = Field(default=False, description="کسر خودکار اعتبار")
127
+
128
+ class TranslationFormRequest(BaseModel):
129
+ text: str
130
+ source_lang: str
131
+ target_lang: str
132
+
133
+ class CompletionCheckRequest(BaseModel):
134
+ request_id: str
135
+
136
+ class StatusCheckRequest(BaseModel):
137
+ request_id: str
138
+
139
+ class AutoChargeStatusRequest(BaseModel):
140
+ request_id: str
141
+
142
+ # کلاس کش ترجمه
143
+ class TranslationCache:
144
+ def __init__(self, expiry_minutes: int = 60):
145
+ self.cache: Dict[str, Dict] = {}
146
+ self.expiry = expiry_minutes * 60
147
+ self.lock = threading.Lock()
148
+
149
+ def _generate_key(self, text: str, source_lang: str, target_lang: str) -> str:
150
+ content = f"{text}:{source_lang}:{target_lang}"
151
+ return hashlib.md5(content.encode()).hexdigest()
152
+
153
+ def get(self, text: str, source_lang: str, target_lang: str) -> Optional[str]:
154
+ key = self._generate_key(text, source_lang, target_lang)
155
+ with self.lock:
156
+ if key in self.cache:
157
+ entry = self.cache[key]
158
+ if time.time() - entry['timestamp'] < self.expiry:
159
+ return entry['translation']
160
+ else:
161
+ del self.cache[key]
162
+ return None
163
+
164
+ def set(self, text: str, source_lang: str, target_lang: str, translation: str):
165
+ key = self._generate_key(text, source_lang, target_lang)
166
+ with self.lock:
167
+ self.cache[key] = {
168
+ 'translation': translation,
169
+ 'timestamp': time.time()
170
+ }
171
+
172
+ def clear_expired(self):
173
+ current_time = time.time()
174
+ with self.lock:
175
+ expired_keys = [
176
+ key for key, entry in self.cache.items()
177
+ if current_time - entry['timestamp'] >= self.expiry
178
+ ]
179
+ for key in expired_keys:
180
+ del self.cache[key]
181
+
182
+ def get_stats(self) -> Dict:
183
+ with self.lock:
184
+ return {
185
+ 'cache_size': len(self.cache),
186
+ 'total_entries': len(self.cache)
187
+ }
188
+
189
+ # کلاس تقسیم متن
190
+ class TextChunker:
191
+ def __init__(self, max_chunk_size: int = MAX_CHUNK_SIZE):
192
+ self.max_chunk_size = max_chunk_size
193
+
194
+ def chunk_text(self, text: str) -> List[str]:
195
+ if len(text) <= self.max_chunk_size:
196
+ return [text]
197
+
198
+ # تقسیم بر اساس پاراگراف
199
+ paragraphs = text.split('\n\n')
200
+ chunks = []
201
+ current_chunk = ""
202
+
203
+ for paragraph in paragraphs:
204
+ if len(current_chunk) + len(paragraph) <= self.max_chunk_size:
205
+ if current_chunk:
206
+ current_chunk += '\n\n' + paragraph
207
+ else:
208
+ current_chunk = paragraph
209
+ else:
210
+ if current_chunk:
211
+ chunks.append(current_chunk)
212
+
213
+ if len(paragraph) <= self.max_chunk_size:
214
+ current_chunk = paragraph
215
+ else:
216
+ # تقسیم پاراگراف طولانی
217
+ sub_chunks = self._split_long_paragraph(paragraph)
218
+ chunks.extend(sub_chunks[:-1])
219
+ current_chunk = sub_chunks[-1] if sub_chunks else ""
220
+
221
+ if current_chunk:
222
+ chunks.append(current_chunk)
223
+
224
+ return chunks
225
+
226
+ def _split_long_paragraph(self, text: str) -> List[str]:
227
+ # تقسیم بر اساس جملات
228
+ sentences = re.split(r'[.!?]+\s+', text)
229
+ chunks = []
230
+ current_chunk = ""
231
+
232
+ for sentence in sentences:
233
+ if len(current_chunk) + len(sentence) <= self.max_chunk_size:
234
+ if current_chunk:
235
+ current_chunk += '. ' + sentence
236
+ else:
237
+ current_chunk = sentence
238
+ else:
239
+ if current_chunk:
240
+ chunks.append(current_chunk)
241
+
242
+ if len(sentence) <= self.max_chunk_size:
243
+ current_chunk = sentence
244
+ else:
245
+ # تقسیم بر اساس کاما
246
+ comma_parts = sentence.split(', ')
247
+ for part in comma_parts:
248
+ if len(current_chunk) + len(part) <= self.max_chunk_size:
249
+ if current_chunk:
250
+ current_chunk += ', ' + part
251
+ else:
252
+ current_chunk = part
253
+ else:
254
+ if current_chunk:
255
+ chunks.append(current_chunk)
256
+ current_chunk = part
257
+
258
+ if current_chunk:
259
+ chunks.append(current_chunk)
260
+
261
+ return chunks
262
+
263
+ # صف ترجمه
264
+ class TranslationQueue:
265
+ def __init__(self, max_workers: int = MAX_WORKERS):
266
+ self.executor = ThreadPoolExecutor(max_workers=max_workers)
267
+ self.tasks: Dict[str, Dict] = {}
268
+ self.lock = threading.Lock()
269
+
270
+ def add_task(self, session_id: str, task_func, *args, **kwargs):
271
+ with self.lock:
272
+ future = self.executor.submit(task_func, *args, **kwargs)
273
+ self.tasks[session_id] = {
274
+ 'future': future,
275
+ 'start_time': time.time(),
276
+ 'status': 'processing'
277
+ }
278
+
279
+ def get_task_status(self, session_id: str) -> Optional[Dict]:
280
+ with self.lock:
281
+ return self.tasks.get(session_id)
282
+
283
+ def remove_task(self, session_id: str):
284
+ with self.lock:
285
+ if session_id in self.tasks:
286
+ del self.tasks[session_id]
287
+
288
+ # کلاس اصلی مترجم
289
+ class MultilingualTranslator:
290
+ def __init__(self, cache_expiry_minutes: int = 60):
291
+ print("در حال بارگذاری مدل M2M100...")
292
+
293
+ # بارگذاری مدل و توکنایزر
294
+ self.tokenizer = M2M100Tokenizer.from_pretrained(MODEL_NAME)
295
+ self.model = M2M100ForConditionalGeneration.from_pretrained(MODEL_NAME)
296
+
297
+ # تشخیص GPU
298
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
299
+ self.model.to(self.device)
300
+ print(f"مدل روی {self.device} بارگذاری شد")
301
+
302
+ # اجزای کمکی
303
+ self.cache = TranslationCache(cache_expiry_minutes)
304
+ self.chunker = TextChunker()
305
+ self.queue = TranslationQueue()
306
+
307
+ # ذخیره وضعیت ترجمه‌ها
308
+ self.translation_sessions: Dict[str, Dict] = {}
309
+ self.completed_translations: Dict[str, Dict] = {}
310
+ self.translation_requests: Dict[str, Dict] = {}
311
+
312
+ # آمار
313
+ self.total_requests = 0
314
+ self.lock = threading.Lock()
315
+
316
+ def _normalize_language(self, lang: str) -> str:
317
+ """تبدیل نام زبان به کد دوحرفی"""
318
+ if lang in LANGUAGE_MAP:
319
+ return LANGUAGE_MAP[lang]
320
+ elif lang.lower() in [v.lower() for v in LANGUAGE_MAP.values()]:
321
+ return lang.lower()
322
+ else:
323
+ raise ValueError(f"زبان پشتیبانی نمی‌شود: {lang}")
324
+
325
+ def translate_chunk(self, text: str, source_lang: str, target_lang: str) -> str:
326
+ """ترجمه یک بخش از متن"""
327
+ try:
328
+ # تنظیم زبان مبدا
329
+ self.tokenizer.src_lang = source_lang
330
+
331
+ # کدگذاری متن
332
+ encoded = self.tokenizer(text, return_tensors="pt").to(self.device)
333
+
334
+ # تولید ترجمه
335
+ generated_tokens = self.model.generate(
336
+ **encoded,
337
+ forced_bos_token_id=self.tokenizer.get_lang_id(target_lang),
338
+ max_length=512,
339
+ num_beams=5,
340
+ early_stopping=True
341
+ )
342
+
343
+ # رمزگشایی نتیجه
344
+ translation = self.tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
345
+
346
+ return translation.strip()
347
+
348
+ except Exception as e:
349
+ print(f"خطا در ترجمه بخش: {str(e)}")
350
+ return text
351
+
352
+ def translate_text(self, text: str, source_lang: str, target_lang: str,
353
+ session_id: Optional[str] = None) -> Dict[str, Any]:
354
+ """ترجمه متن کامل"""
355
+ start_time = time.time()
356
+
357
+ # تبدیل کدهای زبان
358
+ source_lang = self._normalize_language(source_lang)
359
+ target_lang = self._normalize_language(target_lang)
360
+
361
+ # بررسی کش
362
+ cached_result = self.cache.get(text, source_lang, target_lang)
363
+ if cached_result:
364
+ return {
365
+ 'translated_text': cached_result,
366
+ 'processing_time': 0,
367
+ 'chunks_count': 1,
368
+ 'from_cache': True
369
+ }
370
+
371
+ # تقسیم متن
372
+ chunks = self.chunker.chunk_text(text)
373
+ translated_chunks = []
374
+
375
+ # ذخیره وضعیت session
376
+ if session_id:
377
+ with self.lock:
378
+ self.translation_sessions[session_id] = {
379
+ 'total_chunks': len(chunks),
380
+ 'completed_chunks': 0,
381
+ 'start_time': start_time,
382
+ 'status': 'processing'
383
+ }
384
+
385
+ # ترجمه هر بخش
386
+ for i, chunk in enumerate(chunks):
387
+ translated_chunk = self.translate_chunk(chunk, source_lang, target_lang)
388
+ translated_chunks.append(translated_chunk)
389
+
390
+ # بروزرسانی پیشرفت
391
+ if session_id:
392
+ with self.lock:
393
+ if session_id in self.translation_sessions:
394
+ self.translation_sessions[session_id]['completed_chunks'] = i + 1
395
+
396
+ # ترکیب نتایج
397
+ final_translation = self.combine_translations(translated_chunks)
398
+ processing_time = time.time() - start_time
399
+
400
+ # ذخیره در کش
401
+ self.cache.set(text, source_lang, target_lang, final_translation)
402
+
403
+ # بروزرسانی وضعیت نهایی
404
+ if session_id:
405
+ with self.lock:
406
+ if session_id in self.translation_sessions:
407
+ self.translation_sessions[session_id].update({
408
+ 'status': 'completed',
409
+ 'end_time': time.time(),
410
+ 'result': final_translation
411
+ })
412
+
413
+ # افزایش آمار
414
+ with self.lock:
415
+ self.total_requests += 1
416
+
417
+ result = {
418
+ 'translated_text': final_translation,
419
+ 'processing_time': processing_time,
420
+ 'chunks_count': len(chunks),
421
+ 'from_cache': False
422
+ }
423
+
424
+ return result
425
+
426
+ def combine_translations(self, chunks: List[str]) -> str:
427
+ """ترکیب بخش‌های ترجمه شده"""
428
+ return ' '.join(chunks)
429
+
430
+ def get_translation_progress(self, session_id: str) -> Optional[Dict]:
431
+ """دریافت پیشرفت ترجمه"""
432
+ with self.lock:
433
+ if session_id not in self.translation_sessions:
434
+ return None
435
+
436
+ session = self.translation_sessions[session_id]
437
+ current_time = time.time()
438
+ elapsed_time = current_time - session['start_time']
439
+
440
+ if session['status'] == 'completed':
441
+ return {
442
+ 'progress': 100,
443
+ 'completed_chunks': session['completed_chunks'],
444
+ 'total_chunks': session['total_chunks'],
445
+ 'elapsed_time': elapsed_time,
446
+ 'status': 'completed',
447
+ 'result': session.get('result')
448
+ }
449
+
450
+ progress = (session['completed_chunks'] / session['total_chunks']) * 100
451
+
452
+ # تخمین زمان باقی‌مانده
453
+ if session['completed_chunks'] > 0:
454
+ avg_time_per_chunk = elapsed_time / session['completed_chunks']
455
+ remaining_chunks = session['total_chunks'] - session['completed_chunks']
456
+ estimated_remaining = avg_time_per_chunk * remaining_chunks
457
+ else:
458
+ estimated_remaining = None
459
+
460
+ return {
461
+ 'progress': progress,
462
+ 'completed_chunks': session['completed_chunks'],
463
+ 'total_chunks': session['total_chunks'],
464
+ 'elapsed_time': elapsed_time,
465
+ 'estimated_remaining': estimated_remaining,
466
+ 'status': 'processing'
467
+ }
468
+
469
+ async def translate_text_async(self, text: str, source_lang: str, target_lang: str) -> Dict[str, Any]:
470
+ """نسخه آسنکرون ترجمه"""
471
+ loop = asyncio.get_event_loop()
472
+ with ThreadPoolExecutor() as executor:
473
+ result = await loop.run_in_executor(
474
+ executor,
475
+ self.translate_text,
476
+ text, source_lang, target_lang
477
+ )
478
+ return result
479
+
480
+ # توابع مربوط به WordPress
481
+ def notify_wordpress_completion_and_charge(request_id: str, character_count: int,
482
+ translation_length: int, source_lang: str,
483
+ target_lang: str):
484
+ """اطلاع‌رسانی تکمیل به WordPress و کسر اعتبار"""
485
+ try:
486
+ wordpress_url = os.getenv("WORDPRESS_NOTIFICATION_URL")
487
+ if not wordpress_url:
488
+ print("URL وردپرس تنظیم نشده است")
489
+ return False
490
+
491
+ payload = {
492
+ 'request_id': request_id,
493
+ 'character_count': character_count,
494
+ 'translation_length': translation_length,
495
+ 'source_lang': source_lang,
496
+ 'target_lang': target_lang,
497
+ 'completed_at': datetime.now().isoformat()
498
+ }
499
+
500
+ response = requests.post(wordpress_url, json=payload, timeout=30)
501
+
502
+ if response.status_code == 200:
503
+ # ثبت فلگ کسر خودکار
504
+ if request_id in translator.translation_requests:
505
+ translator.translation_requests[request_id]['auto_charged'] = True
506
+ print(f"اطلاع‌رسانی موفق برای درخواست {request_id}")
507
+ return True
508
+ else:
509
+ print(f"خطا در اطلاع‌رسانی: {response.status_code}")
510
+ return False
511
+
512
+ except Exception as e:
513
+ print(f"خطا در اطلاع‌رسانی به WordPress: {str(e)}")
514
+ return False
515
+
516
+ def process_heavy_translation_background(text: str, source_lang: str, target_lang: str,
517
+ request_id: str, auto_charge: bool = False):
518
+ """پردازش ترجمه سنگین در پس‌زمینه"""
519
+ try:
520
+ # ثبت درخواست
521
+ translator.translation_requests[request_id] = {
522
+ 'text': text,
523
+ 'source_lang': source_lang,
524
+ 'target_lang': target_lang,
525
+ 'start_time': time.time(),
526
+ 'status': 'processing',
527
+ 'auto_charge': auto_charge,
528
+ 'auto_charged': False
529
+ }
530
+
531
+ # انجام ترجمه
532
+ result = translator.translate_text(text, source_lang, target_lang, request_id)
533
+
534
+ # ذخیره نتیجه
535
+ translator.completed_translations[request_id] = {
536
+ 'result': result,
537
+ 'completed_at': time.time(),
538
+ 'character_count': len(text),
539
+ 'translation_length': len(result['translated_text'])
540
+ }
541
+
542
+ # اطلاع‌رسانی به WordPress در صورت نیاز
543
+ if auto_charge:
544
+ notify_wordpress_completion_and_charge(
545
+ request_id,
546
+ len(text),
547
+ len(result['translated_text']),
548
+ source_lang,
549
+ target_lang
550
+ )
551
+
552
+ print(f"ترجمه پس‌زمینه {request_id} تکمیل شد")
553
+
554
+ except Exception as e:
555
+ print(f"خطا در ترجمه پس‌زمینه {request_id}: {str(e)}")
556
+ if request_id in translator.translation_requests:
557
+ translator.translation_requests[request_id]['status'] = 'failed'
558
+ translator.translation_requests[request_id]['error'] = str(e)
559
+
560
+ def perform_translation_internal(text: str, source_lang: str, target_lang: str) -> Dict[str, Any]:
561
+ """تابع کمکی برای انجام ترجمه"""
562
+ try:
563
+ result = translator.translate_text(text, source_lang, target_lang)
564
+ return result
565
+ except Exception as e:
566
+ raise HTTPException(status_code=500, detail=f"خطا در ترجمه: {str(e)}")
567
+
568
+ # ایجاد مترجم
569
+ print("در حال راه‌اندازی مترجم...")
570
+ translator = MultilingualTranslator(60)
571
+
572
+ # تابع پاکسازی
573
+ def cleanup_old_data():
574
+ """پاکسازی داده‌های قدیمی"""
575
+ while True:
576
+ try:
577
+ current_time = time.time()
578
+
579
+ # پاکسازی کش
580
+ translator.cache.clear_expired()
581
+
582
+ # پاکسازی درخواست‌های قدیمی (بیش از 2 ساعت)
583
+ expired_requests = []
584
+ for req_id, req_data in translator.translation_requests.items():
585
+ if current_time - req_data['start_time'] > 7200: # 2 ساعت
586
+ expired_requests.append(req_id)
587
+
588
+ for req_id in expired_requests:
589
+ translator.translation_requests.pop(req_id, None)
590
+ translator.completed_translations.pop(req_id, None)
591
+
592
+ # پاکسازی session های قدیمی
593
+ expired_sessions = []
594
+ for session_id, session_data in translator.translation_sessions.items():
595
+ if current_time - session_data['start_time'] > 3600: # 1 ساعت
596
+ expired_sessions.append(session_id)
597
+
598
+ for session_id in expired_sessions:
599
+ translator.translation_sessions.pop(session_id, None)
600
+
601
+ print(f"پاکسازی انجام شد: {len(expired_requests)} درخواست و {len(expired_sessions)} session حذف شد")
602
+
603
+ except Exception as e:
604
+ print(f"خطا در پاکسازی: {str(e)}")
605
+
606
+ time.sleep(CLEANUP_INTERVAL)
607
+
608
+ # راه‌اندازی FastAPI
609
+ app = FastAPI(
610
+ title="سرویس ترجمه چندزبانه M2M100",
611
+ description="API ترجمه مبتنی بر مدل M2M100 فیسبوک",
612
+ version="1.0.0"
613
+ )
614
+
615
+ # تنظیم CORS
616
+ app.add_middleware(
617
+ CORSMiddleware,
618
+ allow_origins=["*"],
619
+ allow_credentials=True,
620
+ allow_methods=["*"],
621
+ allow_headers=["*"],
622
+ )
623
+
624
+ # شروع نخ پاکسازی
625
+ cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True)
626
+ cleanup_thread.start()
627
+
628
+ # Endpoints
629
+
630
+ @app.get("/")
631
+ async def root():
632
+ """صفحه اصلی API"""
633
+ return {
634
+ "message": "سرویس ترجمه چندزبانه M2M100",
635
+ "model": MODEL_NAME,
636
+ "device": str(translator.device),
637
+ "features": [
638
+ "multilingual_translation",
639
+ "text_chunking",
640
+ "translation_cache",
641
+ "background_processing",
642
+ "progress_tracking",
643
+ "wordpress_integration"
644
+ ],
645
+ "supported_languages": len(LANGUAGE_MAP),
646
+ "endpoints": {
647
+ "/": "صفحه اصلی",
648
+ "/api/translate": "ترجمه همزمان",
649
+ "/api/translate/form": "ترجمه از فرم",
650
+ "/api/languages": "لیست زبان‌ها",
651
+ "/api/health": "وضعیت سلامت",
652
+ "/api/progress/{session_id}": "پیگیری پیشرفت",
653
+ "/api/status/{session_id}": "وضعیت کلی",
654
+ "/api/server-status": "وضعیت سرور",
655
+ "/api/check-completion": "بررسی تکمیل",
656
+ "/api/check-translation-status": "وضعیت ترجمه",
657
+ "/api/check-auto-charge-status": "وضعیت کسر خودکار"
658
+ }
659
+ }
660
+
661
+ @app.post("/api/translate")
662
+ async def translate_text_api(request: TranslationRequest):
663
+ """ترجمه همزمان متن"""
664
+ try:
665
+ result = perform_translation_internal(
666
+ request.text,
667
+ request.source_lang,
668
+ request.target_lang
669
+ )
670
+
671
+ return {
672
+ "success": True,
673
+ "translated_text": result['translated_text'],
674
+ "processing_time": result['processing_time'],
675
+ "chunks_count": result['chunks_count'],
676
+ "from_cache": result.get('from_cache', False),
677
+ "character_count": len(request.text),
678
+ "translation_length": len(result['translated_text'])
679
+ }
680
+
681
+ except Exception as e:
682
+ raise HTTPException(status_code=500, detail=str(e))
683
+
684
+ @app.post("/api/translate/form")
685
+ async def translate_form_api(request: TranslationFormRequest):
686
+ """ترجمه از فرم (با احتمال استفاده از کش)"""
687
+ try:
688
+ result = perform_translation_internal(
689
+ request.text,
690
+ request.source_lang,
691
+ request.target_lang
692
+ )
693
+
694
+ return {
695
+ "success": True,
696
+ "translated_text": result['translated_text'],
697
+ "processing_time": result['processing_time'],
698
+ "chunks_count": result['chunks_count'],
699
+ "from_cache": result.get('from_cache', False)
700
+ }
701
+
702
+ except Exception as e:
703
+ raise HTTPException(status_code=500, detail=str(e))
704
+
705
+ @app.get("/api/languages")
706
+ async def get_supported_languages():
707
+ """دریافت لیست زبان‌های پشتیبانی شده"""
708
+ return {
709
+ "success": True,
710
+ "languages": LANGUAGE_MAP,
711
+ "total_count": len(LANGUAGE_MAP)
712
+ }
713
+
714
+ @app.get("/api/health")
715
+ async def health_check():
716
+ """بررسی سلامت سرویس"""
717
+ cache_stats = translator.cache.get_stats()
718
+
719
+ return {
720
+ "status": "healthy",
721
+ "model": MODEL_NAME,
722
+ "device": str(translator.device),
723
+ "gpu_available": torch.cuda.is_available(),
724
+ "cache_size": cache_stats['cache_size'],
725
+ "total_requests": translator.total_requests,
726
+ "active_sessions": len(translator.translation_sessions),
727
+ "completed_translations": len(translator.completed_translations),
728
+ "version": "1.0.0",
729
+ "timestamp": datetime.now().isoformat()
730
+ }
731
+
732
+ @app.get("/api/progress/{session_id}")
733
+ async def get_translation_progress(session_id: str):
734
+ """دریافت پیشرفت ترجمه"""
735
+ progress = translator.get_translation_progress(session_id)
736
+
737
+ if progress is None:
738
+ raise HTTPException(status_code=404, detail="Session پیدا نشد یا تکمیل شده است")
739
+
740
+ return {
741
+ "success": True,
742
+ "session_id": session_id,
743
+ **progress
744
+ }
745
+
746
+ @app.get("/api/status/{session_id}")
747
+ async def get_translation_status(session_id: str):
748
+ """دریافت وضعیت کلی ترجمه"""
749
+ progress = translator.get_translation_progress(session_id)
750
+
751
+ if progress is None:
752
+ # بررسی در ترجمه‌های تکمیل شده
753
+ if session_id in translator.completed_translations:
754
+ completed = translator.completed_translations[session_id]
755
+ return {
756
+ "success": True,
757
+ "status": "completed",
758
+ "result": completed['result'],
759
+ "completed_at": completed['completed_at']
760
+ }
761
+ else:
762
+ raise HTTPException(status_code=404, detail="Session پیدا نشد")
763
+
764
+ return {
765
+ "success": True,
766
+ "session_id": session_id,
767
+ **progress
768
+ }
769
+
770
+ @app.get("/api/server-status")
771
+ async def get_server_status():
772
+ """دریافت وضعیت کلی سرور"""
773
+ active_sessions = len(translator.translation_sessions)
774
+ background_tasks = len(translator.translation_requests)
775
+ completed_count = len(translator.completed_translations)
776
+
777
+ # شمارش وظایف در حال پردازش
778
+ processing_count = sum(1 for req in translator.translation_requests.values()
779
+ if req.get('status') == 'processing')
780
+
781
+ return {
782
+ "success": True,
783
+ "server_status": "running",
784
+ "active_sessions": active_sessions,
785
+ "background_tasks": background_tasks,
786
+ "processing_tasks": processing_count,
787
+ "completed_translations": completed_count,
788
+ "total_requests": translator.total_requests,
789
+ "uptime": time.time(),
790
+ "message": f"سرور فعال - {active_sessions} session فعال، {processing_count} در حال پردازش"
791
+ }
792
+
793
+ @app.post("/api/check-completion")
794
+ async def check_completion(request: CompletionCheckRequest):
795
+ """بررسی تکمیل ترجمه با request_id"""
796
+ request_id = request.request_id
797
+
798
+ # بررسی در ترجمه‌های تکمیل شده
799
+ if request_id in translator.completed_translations:
800
+ completed = translator.completed_translations[request_id]
801
+ return {
802
+ "success": True,
803
+ "completed": True,
804
+ "completed_at": completed['completed_at'],
805
+ "processing_time": completed.get('processing_time', 0)
806
+ }
807
+
808
+ # بررسی در درخواست‌های در حال پردازش
809
+ if request_id in translator.translation_requests:
810
+ req_data = translator.translation_requests[request_id]
811
+ if req_data.get('status') == 'processing':
812
+ return {
813
+ "success": True,
814
+ "completed": False,
815
+ "status": "در حال پردازش",
816
+ "elapsed_time": time.time() - req_data['start_time']
817
+ }
818
+ elif req_data.get('status') == 'failed':
819
+ return {
820
+ "success": False,
821
+ "completed": False,
822
+ "status": "ناموفق",
823
+ "error": req_data.get('error', 'خطای ناشناخته')
824
+ }
825
+
826
+ # درخواست پیدا نشد
827
+ return {
828
+ "success": False,
829
+ "completed": False,
830
+ "status": "درخواست پیدا نشد"
831
+ }
832
+
833
+ @app.post("/api/check-translation-status")
834
+ async def check_translation_status(request: StatusCheckRequest):
835
+ """بررسی وضعیت و نتیجه نهایی ترجمه"""
836
+ request_id = request.request_id
837
+
838
+ # بررسی در ترجمه‌های تکمیل شده
839
+ if request_id in translator.completed_translations:
840
+ completed = translator.completed_translations[request_id]
841
+ req_data = translator.translation_requests.get(request_id, {})
842
+
843
+ return {
844
+ "success": True,
845
+ "status": "completed",
846
+ "translated_text": completed['result']['translated_text'],
847
+ "processing_time": completed['result']['processing_time'],
848
+ "chunks_count": completed['result']['chunks_count'],
849
+ "character_count": completed['character_count'],
850
+ "translation_length": completed['translation_length'],
851
+ "completed_at": completed['completed_at'],
852
+ "source_lang": req_data.get('source_lang'),
853
+ "target_lang": req_data.get('target_lang')
854
+ }
855
+
856
+ # بررسی در درخواست‌های در حال پردازش
857
+ if request_id in translator.translation_requests:
858
+ req_data = translator.translation_requests[request_id]
859
+ elapsed_time = time.time() - req_data['start_time']
860
+
861
+ if req_data.get('status') == 'processing':
862
+ # تخمین پیشرفت بر اساس طول متن
863
+ text_length = len(req_data.get('text', ''))
864
+ chunks_estimate = max(1, text_length // MAX_CHUNK_SIZE)
865
+
866
+ # تخمین پیشرفت (این تخمینی است)
867
+ progress_estimate = min(90, (elapsed_time / 10) * 100) # حداکثر 90% تا زمان تکمیل
868
+
869
+ return {
870
+ "success": True,
871
+ "status": "processing",
872
+ "progress": progress_estimate,
873
+ "elapsed_time": elapsed_time,
874
+ "estimated_chunks": chunks_estimate,
875
+ "message": "در حال پردازش ترجمه..."
876
+ }
877
+ elif req_data.get('status') == 'failed':
878
+ return {
879
+ "success": False,
880
+ "status": "failed",
881
+ "error": req_data.get('error', 'خطای ناشناخته'),
882
+ "elapsed_time": elapsed_time
883
+ }
884
+
885
+ # درخواست پیدا نشد
886
+ return {
887
+ "success": False,
888
+ "status": "not_found",
889
+ "message": "درخواست ترجمه پیدا نشد"
890
+ }
891
+
892
+ @app.post("/api/check-auto-charge-status")
893
+ async def check_auto_charge_status(request: AutoChargeStatusRequest):
894
+ """بررسی وضعیت کسر اعتبار خودکار"""
895
+ request_id = request.request_id
896
+
897
+ if request_id not in translator.translation_requests:
898
+ return {
899
+ "success": False,
900
+ "message": "درخواست پیدا نشد"
901
+ }
902
+
903
+ req_data = translator.translation_requests[request_id]
904
+
905
+ return {
906
+ "success": True,
907
+ "request_id": request_id,
908
+ "auto_charge_enabled": req_data.get('auto_charge', False),
909
+ "auto_charged": req_data.get('auto_charged', False),
910
+ "status": req_data.get('status', 'unknown')
911
+ }
912
+
913
+ @app.post("/api/translate/heavy")
914
+ async def translate_heavy_text(request: TranslationRequest, background_tasks: BackgroundTasks):
915
+ """ترجمه متن‌های سنگین در پس‌زمینه"""
916
+ import uuid
917
+
918
+ # تولید شناسه منحصر به فرد
919
+ request_id = str(uuid.uuid4())
920
+
921
+ # اضافه کردن به وظایف پس‌زمینه
922
+ background_tasks.add_task(
923
+ process_heavy_translation_background,
924
+ request.text,
925
+ request.source_lang,
926
+ request.target_lang,
927
+ request_id,
928
+ request.auto_charge
929
+ )
930
+
931
+ return {
932
+ "success": True,
933
+ "message": "ترجمه در پس‌زمینه آغاز شد",
934
+ "request_id": request_id,
935
+ "estimated_time": len(request.text) // 100, # تخمین بر اساس طول متن
936
+ "auto_charge": request.auto_charge
937
+ }
938
+
939
+ @app.post("/api/translate/session")
940
+ async def translate_with_session(request: TranslationRequest):
941
+ """ترجمه با session برای پیگیری پیشرفت"""
942
+ import uuid
943
+
944
+ session_id = str(uuid.uuid4())
945
+
946
+ try:
947
+ # شروع ترجمه با session_id
948
+ result = translator.translate_text(
949
+ request.text,
950
+ request.source_lang,
951
+ request.target_lang,
952
+ session_id
953
+ )
954
+
955
+ return {
956
+ "success": True,
957
+ "session_id": session_id,
958
+ "translated_text": result['translated_text'],
959
+ "processing_time": result['processing_time'],
960
+ "chunks_count": result['chunks_count'],
961
+ "from_cache": result.get('from_cache', False)
962
+ }
963
+
964
+ except Exception as e:
965
+ raise HTTPException(status_code=500, detail=str(e))
966
+
967
+ @app.get("/api/cache/stats")
968
+ async def get_cache_stats():
969
+ """آمار کش ترجمه"""
970
+ stats = translator.cache.get_stats()
971
+
972
+ return {
973
+ "success": True,
974
+ "cache_stats": stats,
975
+ "expiry_minutes": 60
976
+ }
977
+
978
+ @app.post("/api/cache/clear")
979
+ async def clear_cache():
980
+ """پاک کردن کش (فقط برای ��دیران)"""
981
+ try:
982
+ with translator.cache.lock:
983
+ translator.cache.cache.clear()
984
+
985
+ return {
986
+ "success": True,
987
+ "message": "کش پاک شد"
988
+ }
989
+ except Exception as e:
990
+ raise HTTPException(status_code=500, detail=f"خطا در پاک کردن کش: {str(e)}")
991
+
992
+ @app.get("/api/stats")
993
+ async def get_api_stats():
994
+ """آمار کلی API"""
995
+ return {
996
+ "success": True,
997
+ "total_requests": translator.total_requests,
998
+ "active_sessions": len(translator.translation_sessions),
999
+ "background_tasks": len(translator.translation_requests),
1000
+ "completed_translations": len(translator.completed_translations),
1001
+ "cache_size": translator.cache.get_stats()['cache_size'],
1002
+ "supported_languages": len(LANGUAGE_MAP),
1003
+ "model_info": {
1004
+ "name": MODEL_NAME,
1005
+ "device": str(translator.device),
1006
+ "gpu_available": torch.cuda.is_available()
1007
+ }
1008
+ }
1009
+
1010
+ @app.post("/api/webhook/wordpress")
1011
+ async def wordpress_webhook(data: dict):
1012
+ """Webhook برای دریافت اطلاعات از WordPress"""
1013
+ try:
1014
+ # پردازش داده‌های دریافتی از WordPress
1015
+ request_id = data.get('request_id')
1016
+ action = data.get('action')
1017
+
1018
+ if action == 'translation_request':
1019
+ # درخواست ترجمه از WordPress
1020
+ text = data.get('text')
1021
+ source_lang = data.get('source_lang')
1022
+ target_lang = data.get('target_lang')
1023
+ auto_charge = data.get('auto_charge', False)
1024
+
1025
+ if not all([text, source_lang, target_lang, request_id]):
1026
+ raise HTTPException(status_code=400, detail="داده‌های ناقص")
1027
+
1028
+ # شروع ترجمه در پس‌زمینه
1029
+ background_tasks = BackgroundTasks()
1030
+ background_tasks.add_task(
1031
+ process_heavy_translation_background,
1032
+ text, source_lang, target_lang, request_id, auto_charge
1033
+ )
1034
+
1035
+ return {
1036
+ "success": True,
1037
+ "message": "ترجمه آغاز شد",
1038
+ "request_id": request_id
1039
+ }
1040
+
1041
+ elif action == 'status_check':
1042
+ # بررسی وضعیت
1043
+ if request_id in translator.completed_translations:
1044
+ completed = translator.completed_translations[request_id]
1045
+ return {
1046
+ "success": True,
1047
+ "status": "completed",
1048
+ "result": completed['result']
1049
+ }
1050
+ elif request_id in translator.translation_requests:
1051
+ return {
1052
+ "success": True,
1053
+ "status": "processing"
1054
+ }
1055
+ else:
1056
+ return {
1057
+ "success": False,
1058
+ "status": "not_found"
1059
+ }
1060
+
1061
+ else:
1062
+ raise HTTPException(status_code=400, detail="عمل نامعتبر")
1063
+
1064
+ except Exception as e:
1065
+ raise HTTPException(status_code=500, detail=f"خطا در webhook: {str(e)}")
1066
+
1067
+ # تابع راه‌اندازی (برای Hugging Face Spaces)
1068
+ if __name__ == "__main__":
1069
+ import uvicorn
1070
+
1071
+ port = int(os.getenv("PORT", 7860)) # پورت پیش‌فرض Hugging Face Spaces
1072
+
1073
+ print(f"راه‌اندازی سرور روی پورت {port}")
1074
+ print(f"مدل: {MODEL_NAME}")
1075
+ print(f"دستگاه: {translator.device}")
1076
+ print(f"زبان‌های پشتیبانی شده: {len(LANGUAGE_MAP)}")
1077
+
1078
+ uvicorn.run(
1079
+ app,
1080
+ host="0.0.0.0",
1081
+ port=port,
1082
+ log_level="info"
1083
+ )