|
|
import os |
|
|
import time |
|
|
import hashlib |
|
|
import threading |
|
|
import asyncio |
|
|
import re |
|
|
from datetime import datetime, timedelta |
|
|
from concurrent.futures import ThreadPoolExecutor |
|
|
from typing import Dict, List, Optional, Any |
|
|
import json |
|
|
import uuid |
|
|
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from fastapi.responses import JSONResponse |
|
|
from pydantic import BaseModel, Field |
|
|
import torch |
|
|
from transformers import M2M100ForConditionalGeneration, M2M100Tokenizer |
|
|
import requests |
|
|
|
|
|
|
|
|
MODEL_NAME = "facebook/m2m100_418M" |
|
|
CACHE_EXPIRY = 60 * 60 |
|
|
MAX_CHUNK_SIZE = 350 |
|
|
MAX_WORKERS = 1 |
|
|
CLEANUP_INTERVAL = 300 |
|
|
|
|
|
translations = {} |
|
|
|
|
|
os.environ["HF_HOME"] = "/tmp/huggingface" |
|
|
os.environ["TRANSFORMERS_CACHE"] = "/tmp/huggingface" |
|
|
cache_dir = "/tmp/huggingface" |
|
|
os.makedirs(cache_dir, mode=0o777, exist_ok=True) |
|
|
|
|
|
|
|
|
WORDPRESS_BASE_URL = os.getenv("WORDPRESS_NOTIFICATION_URL", "https://echovizio.us.to") |
|
|
|
|
|
|
|
|
LANGUAGE_MAP = { |
|
|
"English": "en", |
|
|
"Persian": "fa", |
|
|
"Farsi": "fa", |
|
|
"Arabic": "ar", |
|
|
"Spanish": "es", |
|
|
"French": "fr", |
|
|
"German": "de", |
|
|
"Italian": "it", |
|
|
"Portuguese": "pt", |
|
|
"Russian": "ru", |
|
|
"Chinese": "zh", |
|
|
"Japanese": "ja", |
|
|
"Korean": "ko", |
|
|
"Hindi": "hi", |
|
|
"Turkish": "tr", |
|
|
"Dutch": "nl", |
|
|
"Swedish": "sv", |
|
|
"Norwegian": "no", |
|
|
"Danish": "da", |
|
|
"Finnish": "fi", |
|
|
"Polish": "pl", |
|
|
"Czech": "cs", |
|
|
"Hungarian": "hu", |
|
|
"Romanian": "ro", |
|
|
"Greek": "el", |
|
|
"Hebrew": "he", |
|
|
"Thai": "th", |
|
|
"Vietnamese": "vi", |
|
|
"Indonesian": "id", |
|
|
"Malay": "ms", |
|
|
"Tamil": "ta", |
|
|
"Bengali": "bn", |
|
|
"Urdu": "ur", |
|
|
"Ukrainian": "uk", |
|
|
"Bulgarian": "bg", |
|
|
"Croatian": "hr", |
|
|
"Serbian": "sr", |
|
|
"Slovak": "sk", |
|
|
"Slovenian": "sl", |
|
|
"Estonian": "et", |
|
|
"Latvian": "lv", |
|
|
"Lithuanian": "lt", |
|
|
"Maltese": "mt", |
|
|
"Catalan": "ca", |
|
|
"Galician": "gl", |
|
|
"Basque": "eu", |
|
|
"Welsh": "cy", |
|
|
"Irish": "ga", |
|
|
"Scottish": "gd", |
|
|
"Icelandic": "is", |
|
|
"Albanian": "sq", |
|
|
"Macedonian": "mk", |
|
|
"Bosnian": "bs", |
|
|
"Montenegrin": "cnr", |
|
|
"Swahili": "sw", |
|
|
"Amharic": "am", |
|
|
"Yoruba": "yo", |
|
|
"Igbo": "ig", |
|
|
"Hausa": "ha", |
|
|
"Somali": "so", |
|
|
"Oromo": "om", |
|
|
"Tigrinya": "ti", |
|
|
"Afrikaans": "af", |
|
|
"Zulu": "zu", |
|
|
"Xhosa": "xh", |
|
|
"Sotho": "st", |
|
|
"Tswana": "tn", |
|
|
"Tsonga": "ts", |
|
|
"Venda": "ve", |
|
|
"Ndebele": "nr", |
|
|
"Gujarati": "gu", |
|
|
"Punjabi": "pa", |
|
|
"Telugu": "te", |
|
|
"Kannada": "kn", |
|
|
"Malayalam": "ml", |
|
|
"Marathi": "mr", |
|
|
"Nepali": "ne", |
|
|
"Sinhala": "si", |
|
|
"Burmese": "my", |
|
|
"Khmer": "km", |
|
|
"Lao": "lo", |
|
|
"Mongolian": "mn", |
|
|
"Kazakh": "kk", |
|
|
"Uzbek": "uz", |
|
|
"Tajik": "tg", |
|
|
"Kyrgyz": "ky", |
|
|
"Turkmen": "tk", |
|
|
"Azerbaijani": "az", |
|
|
"Georgian": "ka", |
|
|
"Armenian": "hy" |
|
|
} |
|
|
|
|
|
|
|
|
class TranslationRequest(BaseModel): |
|
|
text: str = Field(..., description="متن برای ترجمه") |
|
|
source_lang: str = Field(..., description="زبان مبدا") |
|
|
target_lang: str = Field(..., description="زبان مقصد") |
|
|
auto_charge: bool = Field(default=False, description="کسر خودکار اعتبار") |
|
|
|
|
|
class TranslationFormRequest(BaseModel): |
|
|
text: str |
|
|
source_lang: str |
|
|
target_lang: str |
|
|
|
|
|
class CompletionCheckRequest(BaseModel): |
|
|
request_id: str |
|
|
|
|
|
class StatusCheckRequest(BaseModel): |
|
|
request_id: str |
|
|
|
|
|
class AutoChargeStatusRequest(BaseModel): |
|
|
request_id: str |
|
|
|
|
|
|
|
|
class TranslationCache: |
|
|
def __init__(self, expiry_minutes: int = 60): |
|
|
self.cache: Dict[str, Dict] = {} |
|
|
self.expiry = expiry_minutes * 60 |
|
|
self.lock = threading.Lock() |
|
|
|
|
|
def _generate_key(self, text: str, source_lang: str, target_lang: str) -> str: |
|
|
content = f"{text}:{source_lang}:{target_lang}" |
|
|
return hashlib.md5(content.encode()).hexdigest() |
|
|
|
|
|
def get(self, text: str, source_lang: str, target_lang: str) -> Optional[str]: |
|
|
key = self._generate_key(text, source_lang, target_lang) |
|
|
with self.lock: |
|
|
if key in self.cache: |
|
|
entry = self.cache[key] |
|
|
if time.time() - entry['timestamp'] < self.expiry: |
|
|
return entry['translation'] |
|
|
else: |
|
|
del self.cache[key] |
|
|
return None |
|
|
|
|
|
def set(self, text: str, source_lang: str, target_lang: str, translation: str): |
|
|
key = self._generate_key(text, source_lang, target_lang) |
|
|
with self.lock: |
|
|
self.cache[key] = { |
|
|
'translation': translation, |
|
|
'timestamp': time.time() |
|
|
} |
|
|
|
|
|
def clear_expired(self): |
|
|
current_time = time.time() |
|
|
with self.lock: |
|
|
expired_keys = [ |
|
|
key for key, entry in self.cache.items() |
|
|
if current_time - entry['timestamp'] >= self.expiry |
|
|
] |
|
|
for key in expired_keys: |
|
|
del self.cache[key] |
|
|
|
|
|
def get_stats(self) -> Dict: |
|
|
with self.lock: |
|
|
return { |
|
|
'cache_size': len(self.cache), |
|
|
'total_entries': len(self.cache) |
|
|
} |
|
|
|
|
|
|
|
|
class TextChunker: |
|
|
def __init__(self, max_chunk_size: int = MAX_CHUNK_SIZE): |
|
|
self.max_chunk_size = max_chunk_size |
|
|
|
|
|
def chunk_text(self, text: str) -> List[str]: |
|
|
if len(text) <= self.max_chunk_size: |
|
|
return [text] |
|
|
|
|
|
|
|
|
paragraphs = text.split('\n\n') |
|
|
chunks = [] |
|
|
current_chunk = "" |
|
|
|
|
|
for paragraph in paragraphs: |
|
|
if len(current_chunk) + len(paragraph) <= self.max_chunk_size: |
|
|
if current_chunk: |
|
|
current_chunk += '\n\n' + paragraph |
|
|
else: |
|
|
current_chunk = paragraph |
|
|
else: |
|
|
if current_chunk: |
|
|
chunks.append(current_chunk) |
|
|
|
|
|
if len(paragraph) <= self.max_chunk_size: |
|
|
current_chunk = paragraph |
|
|
else: |
|
|
|
|
|
sub_chunks = self._split_long_paragraph(paragraph) |
|
|
chunks.extend(sub_chunks[:-1]) |
|
|
current_chunk = sub_chunks[-1] if sub_chunks else "" |
|
|
|
|
|
if current_chunk: |
|
|
chunks.append(current_chunk) |
|
|
|
|
|
return chunks |
|
|
|
|
|
def _split_long_paragraph(self, text: str) -> List[str]: |
|
|
|
|
|
sentences = re.split(r'[.!?]+\s+', text) |
|
|
chunks = [] |
|
|
current_chunk = "" |
|
|
|
|
|
for sentence in sentences: |
|
|
if len(current_chunk) + len(sentence) <= self.max_chunk_size: |
|
|
if current_chunk: |
|
|
current_chunk += '. ' + sentence |
|
|
else: |
|
|
current_chunk = sentence |
|
|
else: |
|
|
if current_chunk: |
|
|
chunks.append(current_chunk) |
|
|
|
|
|
if len(sentence) <= self.max_chunk_size: |
|
|
current_chunk = sentence |
|
|
else: |
|
|
|
|
|
comma_parts = sentence.split(', ') |
|
|
for part in comma_parts: |
|
|
if len(current_chunk) + len(part) <= self.max_chunk_size: |
|
|
if current_chunk: |
|
|
current_chunk += ', ' + part |
|
|
else: |
|
|
current_chunk = part |
|
|
else: |
|
|
if current_chunk: |
|
|
chunks.append(current_chunk) |
|
|
current_chunk = part |
|
|
|
|
|
if current_chunk: |
|
|
chunks.append(current_chunk) |
|
|
|
|
|
return chunks |
|
|
|
|
|
|
|
|
class TranslationQueue: |
|
|
def __init__(self, max_workers: int = MAX_WORKERS): |
|
|
self.executor = ThreadPoolExecutor(max_workers=max_workers) |
|
|
self.tasks: Dict[str, Dict] = {} |
|
|
self.lock = threading.Lock() |
|
|
|
|
|
def add_task(self, session_id: str, task_func, *args, **kwargs): |
|
|
with self.lock: |
|
|
future = self.executor.submit(task_func, *args, **kwargs) |
|
|
self.tasks[session_id] = { |
|
|
'future': future, |
|
|
'start_time': time.time(), |
|
|
'status': 'processing' |
|
|
} |
|
|
|
|
|
def get_task_status(self, session_id: str) -> Optional[Dict]: |
|
|
with self.lock: |
|
|
return self.tasks.get(session_id) |
|
|
|
|
|
def remove_task(self, session_id: str): |
|
|
with self.lock: |
|
|
if session_id in self.tasks: |
|
|
del self.tasks[session_id] |
|
|
|
|
|
|
|
|
class MultilingualTranslator: |
|
|
def __init__(self, cache_expiry_minutes: int = 60): |
|
|
print("در حال بارگذاری مدل M2M100...") |
|
|
|
|
|
try: |
|
|
|
|
|
self.tokenizer = M2M100Tokenizer.from_pretrained( |
|
|
MODEL_NAME, |
|
|
cache_dir=cache_dir, |
|
|
local_files_only=False |
|
|
) |
|
|
|
|
|
self.model = M2M100ForConditionalGeneration.from_pretrained( |
|
|
MODEL_NAME, |
|
|
cache_dir=cache_dir, |
|
|
local_files_only=False |
|
|
) |
|
|
|
|
|
|
|
|
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
|
|
self.model.to(self.device) |
|
|
print(f"مدل روی {self.device} بارگذاری شد") |
|
|
|
|
|
except Exception as e: |
|
|
print(f"خطا در بارگذاری مدل: {str(e)}") |
|
|
print("تلاش مجدد با تنظیمات مختلف...") |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
import shutil |
|
|
if os.path.exists(cache_dir): |
|
|
shutil.rmtree(cache_dir, ignore_errors=True) |
|
|
os.makedirs(cache_dir, mode=0o777, exist_ok=True) |
|
|
|
|
|
self.tokenizer = M2M100Tokenizer.from_pretrained(MODEL_NAME) |
|
|
self.model = M2M100ForConditionalGeneration.from_pretrained(MODEL_NAME) |
|
|
|
|
|
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
|
|
self.model.to(self.device) |
|
|
print(f"مدل با تلاش مجدد روی {self.device} بارگذاری شد") |
|
|
|
|
|
except Exception as e2: |
|
|
print(f"خطای نهایی در بارگذاری مدل: {str(e2)}") |
|
|
raise e2 |
|
|
|
|
|
|
|
|
self.cache = TranslationCache(cache_expiry_minutes) |
|
|
self.chunker = TextChunker() |
|
|
self.queue = TranslationQueue() |
|
|
|
|
|
|
|
|
self.translation_sessions: Dict[str, Dict] = {} |
|
|
self.completed_translations: Dict[str, Dict] = {} |
|
|
self.translation_requests: Dict[str, Dict] = {} |
|
|
|
|
|
|
|
|
self.total_requests = 0 |
|
|
self.lock = threading.Lock() |
|
|
|
|
|
def _normalize_language(self, lang: str) -> str: |
|
|
"""تبدیل نام زبان به کد دوحرفی""" |
|
|
if lang in LANGUAGE_MAP: |
|
|
return LANGUAGE_MAP[lang] |
|
|
elif lang.lower() in [v.lower() for v in LANGUAGE_MAP.values()]: |
|
|
return lang.lower() |
|
|
else: |
|
|
raise ValueError(f"زبان پشتیبانی نمیشود: {lang}") |
|
|
|
|
|
def translate_chunk(self, text: str, source_lang: str, target_lang: str) -> str: |
|
|
"""ترجمه یک بخش از متن""" |
|
|
try: |
|
|
|
|
|
self.tokenizer.src_lang = source_lang |
|
|
|
|
|
|
|
|
encoded = self.tokenizer(text, return_tensors="pt").to(self.device) |
|
|
|
|
|
|
|
|
generated_tokens = self.model.generate( |
|
|
**encoded, |
|
|
forced_bos_token_id=self.tokenizer.get_lang_id(target_lang), |
|
|
max_length=512, |
|
|
num_beams=5, |
|
|
early_stopping=True |
|
|
) |
|
|
|
|
|
|
|
|
translation = self.tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0] |
|
|
|
|
|
return translation.strip() |
|
|
|
|
|
except Exception as e: |
|
|
print(f"خطا در ترجمه بخش: {str(e)}") |
|
|
return text |
|
|
|
|
|
def translate_text(self, text: str, source_lang: str, target_lang: str, |
|
|
session_id: Optional[str] = None) -> Dict[str, Any]: |
|
|
"""ترجمه متن کامل""" |
|
|
start_time = time.time() |
|
|
|
|
|
|
|
|
source_lang = self._normalize_language(source_lang) |
|
|
target_lang = self._normalize_language(target_lang) |
|
|
|
|
|
|
|
|
cached_result = self.cache.get(text, source_lang, target_lang) |
|
|
if cached_result: |
|
|
return { |
|
|
'translated_text': cached_result, |
|
|
'processing_time': 0, |
|
|
'chunks_count': 1, |
|
|
'from_cache': True |
|
|
} |
|
|
|
|
|
|
|
|
chunks = self.chunker.chunk_text(text) |
|
|
translated_chunks = [] |
|
|
|
|
|
|
|
|
if session_id: |
|
|
with self.lock: |
|
|
self.translation_sessions[session_id] = { |
|
|
'total_chunks': len(chunks), |
|
|
'completed_chunks': 0, |
|
|
'start_time': start_time, |
|
|
'status': 'processing' |
|
|
} |
|
|
|
|
|
|
|
|
for i, chunk in enumerate(chunks): |
|
|
translated_chunk = self.translate_chunk(chunk, source_lang, target_lang) |
|
|
translated_chunks.append(translated_chunk) |
|
|
|
|
|
|
|
|
if session_id: |
|
|
with self.lock: |
|
|
if session_id in self.translation_sessions: |
|
|
self.translation_sessions[session_id]['completed_chunks'] = i + 1 |
|
|
|
|
|
|
|
|
final_translation = self.combine_translations(translated_chunks) |
|
|
processing_time = time.time() - start_time |
|
|
|
|
|
|
|
|
self.cache.set(text, source_lang, target_lang, final_translation) |
|
|
|
|
|
|
|
|
if session_id: |
|
|
with self.lock: |
|
|
if session_id in self.translation_sessions: |
|
|
self.translation_sessions[session_id].update({ |
|
|
'status': 'completed', |
|
|
'end_time': time.time(), |
|
|
'result': final_translation |
|
|
}) |
|
|
|
|
|
|
|
|
with self.lock: |
|
|
self.total_requests += 1 |
|
|
|
|
|
result = { |
|
|
'translated_text': final_translation, |
|
|
'processing_time': processing_time, |
|
|
'chunks_count': len(chunks), |
|
|
'from_cache': False |
|
|
} |
|
|
|
|
|
return result |
|
|
|
|
|
def combine_translations(self, chunks: List[str]) -> str: |
|
|
"""ترکیب بخشهای ترجمه شده""" |
|
|
return ' '.join(chunks) |
|
|
|
|
|
def get_translation_progress(self, session_id: str) -> Optional[Dict]: |
|
|
"""دریافت پیشرفت ترجمه""" |
|
|
with self.lock: |
|
|
if session_id not in self.translation_sessions: |
|
|
return None |
|
|
|
|
|
session = self.translation_sessions[session_id] |
|
|
current_time = time.time() |
|
|
elapsed_time = current_time - session['start_time'] |
|
|
|
|
|
if session['status'] == 'completed': |
|
|
return { |
|
|
'progress': 100, |
|
|
'completed_chunks': session['completed_chunks'], |
|
|
'total_chunks': session['total_chunks'], |
|
|
'elapsed_time': elapsed_time, |
|
|
'status': 'completed', |
|
|
'result': session.get('result') |
|
|
} |
|
|
|
|
|
progress = (session['completed_chunks'] / session['total_chunks']) * 100 |
|
|
|
|
|
|
|
|
if session['completed_chunks'] > 0: |
|
|
avg_time_per_chunk = elapsed_time / session['completed_chunks'] |
|
|
remaining_chunks = session['total_chunks'] - session['completed_chunks'] |
|
|
estimated_remaining = avg_time_per_chunk * remaining_chunks |
|
|
else: |
|
|
estimated_remaining = None |
|
|
|
|
|
return { |
|
|
'progress': progress, |
|
|
'completed_chunks': session['completed_chunks'], |
|
|
'total_chunks': session['total_chunks'], |
|
|
'elapsed_time': elapsed_time, |
|
|
'estimated_remaining': estimated_remaining, |
|
|
'status': 'processing' |
|
|
} |
|
|
|
|
|
async def translate_text_async(self, text: str, source_lang: str, target_lang: str) -> Dict[str, Any]: |
|
|
"""نسخه آسنکرون ترجمه""" |
|
|
loop = asyncio.get_event_loop() |
|
|
with ThreadPoolExecutor() as executor: |
|
|
result = await loop.run_in_executor( |
|
|
executor, |
|
|
self.translate_text, |
|
|
text, source_lang, target_lang |
|
|
) |
|
|
return result |
|
|
|
|
|
def process_heavy_translation_background(text: str, source_lang: str, target_lang: str, |
|
|
request_id: str, auto_charge: bool = False): |
|
|
"""پردازش ترجمه سنگین در پسزمینه""" |
|
|
try: |
|
|
translator.translation_requests[request_id] = { |
|
|
'text': text, |
|
|
'source_lang': source_lang, |
|
|
'target_lang': target_lang, |
|
|
'start_time': time.time(), |
|
|
'status': 'processing', |
|
|
'auto_charge': False, |
|
|
'auto_charged': False |
|
|
} |
|
|
|
|
|
result = translator.translate_text(text, source_lang, target_lang, request_id) |
|
|
|
|
|
translator.completed_translations[request_id] = { |
|
|
'result': result, |
|
|
'completed_at': time.time(), |
|
|
'character_count': len(text), |
|
|
'translation_length': len(result['translated_text']) |
|
|
} |
|
|
|
|
|
|
|
|
print(f"ترجمه پسزمینه {request_id} تکمیل شد - منتظر دریافت از orchestrator") |
|
|
|
|
|
except Exception as e: |
|
|
print(f"خطا در ترجمه پسزمینه {request_id}: {str(e)}") |
|
|
if request_id in translator.translation_requests: |
|
|
translator.translation_requests[request_id]['status'] ='failed' |
|
|
translator.translation_requests[request_id]['error'] = str(e) |
|
|
|
|
|
def perform_translation_internal(text: str, source_lang: str, target_lang: str) -> Dict[str, Any]: |
|
|
"""تابع کمکی برای انجام ترجمه""" |
|
|
try: |
|
|
result = translator.translate_text(text, source_lang, target_lang) |
|
|
return result |
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=f"خطا در ترجمه: {str(e)}") |
|
|
|
|
|
|
|
|
print("در حال راهاندازی مترجم...") |
|
|
translator = MultilingualTranslator(60) |
|
|
|
|
|
|
|
|
def cleanup_old_data(): |
|
|
"""پاکسازی دادههای قدیمی""" |
|
|
while True: |
|
|
try: |
|
|
current_time = time.time() |
|
|
|
|
|
|
|
|
translator.cache.clear_expired() |
|
|
|
|
|
|
|
|
expired_requests = [] |
|
|
for req_id, req_data in translator.translation_requests.items(): |
|
|
if current_time - req_data['start_time'] > 7200: |
|
|
expired_requests.append(req_id) |
|
|
|
|
|
for req_id in expired_requests: |
|
|
translator.translation_requests.pop(req_id, None) |
|
|
translator.completed_translations.pop(req_id, None) |
|
|
|
|
|
|
|
|
expired_sessions = [] |
|
|
for session_id, session_data in translator.translation_sessions.items(): |
|
|
if current_time - session_data['start_time'] > 3600: |
|
|
expired_sessions.append(session_id) |
|
|
|
|
|
for session_id in expired_sessions: |
|
|
translator.translation_sessions.pop(session_id, None) |
|
|
|
|
|
if expired_requests or expired_sessions: |
|
|
print(f"پاکسازی انجام شد: {len(expired_requests)} درخواست و {len(expired_sessions)} session حذف شد") |
|
|
|
|
|
except Exception as e: |
|
|
print(f"خطا در پاکسازی: {str(e)}") |
|
|
|
|
|
time.sleep(CLEANUP_INTERVAL) |
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="سرویس ترجمه چندزبانه M2M100", |
|
|
description="API ترجمه مبتنی بر مدل M2M100 فیسبوک", |
|
|
version="1.0.0" |
|
|
) |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
|
|
|
cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True) |
|
|
cleanup_thread.start() |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/") |
|
|
async def root(): |
|
|
"""صفحه اصلی API""" |
|
|
return { |
|
|
"message": "سرویس ترجمه چندزبانه M2M100", |
|
|
"model": MODEL_NAME, |
|
|
"device": str(translator.device), |
|
|
"features": [ |
|
|
"multilingual_translation", |
|
|
"text_chunking", |
|
|
"translation_cache", |
|
|
"background_processing", |
|
|
"progress_tracking", |
|
|
"wordpress_integration" |
|
|
], |
|
|
"supported_languages": len(LANGUAGE_MAP), |
|
|
"endpoints": { |
|
|
"/": "صفحه اصلی", |
|
|
"/api/translate": "ترجمه همزمان", |
|
|
"/api/translate/form": "ترجمه از فرم", |
|
|
"/api/languages": "لیست زبانها", |
|
|
"/api/health": "وضعیت سلامت", |
|
|
"/api/progress/{session_id}": "پیگیری پیشرفت", |
|
|
"/api/status/{session_id}": "وضعیت کلی", |
|
|
"/api/server-status": "وضعیت سرور", |
|
|
"/api/check-completion": "بررسی تکمیل", |
|
|
"/api/check-translation-status": "وضعیت ترجمه", |
|
|
"/api/check-auto-charge-status": "وضعیت کسر خودکار" |
|
|
} |
|
|
} |
|
|
|
|
|
@app.post("/api/translate") |
|
|
async def translate_text_api(request: TranslationRequest): |
|
|
"""ترجمه همزمان متن""" |
|
|
try: |
|
|
result = perform_translation_internal( |
|
|
request.text, |
|
|
request.source_lang, |
|
|
request.target_lang |
|
|
) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"translated_text": result['translated_text'], |
|
|
"processing_time": result['processing_time'], |
|
|
"chunks_count": result['chunks_count'], |
|
|
"from_cache": result.get('from_cache', False), |
|
|
"character_count": len(request.text), |
|
|
"translation_length": len(result['translated_text']) |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
@app.post("/api/translate/form") |
|
|
async def translate_form_api(request: TranslationFormRequest): |
|
|
"""ترجمه از فرم (با احتمال استفاده از کش)""" |
|
|
try: |
|
|
result = perform_translation_internal( |
|
|
request.text, |
|
|
request.source_lang, |
|
|
request.target_lang |
|
|
) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"translated_text": result['translated_text'], |
|
|
"processing_time": result['processing_time'], |
|
|
"chunks_count": result['chunks_count'], |
|
|
"from_cache": result.get('from_cache', False) |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
@app.get("/api/languages") |
|
|
async def get_supported_languages(): |
|
|
"""دریافت لیست زبانهای پشتیبانی شده""" |
|
|
return { |
|
|
"success": True, |
|
|
"languages": LANGUAGE_MAP, |
|
|
"total_count": len(LANGUAGE_MAP) |
|
|
} |
|
|
|
|
|
@app.get("/api/health") |
|
|
async def health_check(): |
|
|
"""بررسی سلامت سرویس""" |
|
|
cache_stats = translator.cache.get_stats() |
|
|
|
|
|
return { |
|
|
"status": "healthy", |
|
|
"model": MODEL_NAME, |
|
|
"device": str(translator.device), |
|
|
"gpu_available": torch.cuda.is_available(), |
|
|
"cache_size": cache_stats['cache_size'], |
|
|
"total_requests": translator.total_requests, |
|
|
"active_sessions": len(translator.translation_sessions), |
|
|
"completed_translations": len(translator.completed_translations), |
|
|
"version": "1.0.0", |
|
|
"timestamp": datetime.now().isoformat() |
|
|
} |
|
|
|
|
|
@app.get("/api/progress/{session_id}") |
|
|
async def get_translation_progress(session_id: str): |
|
|
"""دریافت پیشرفت ترجمه""" |
|
|
progress = translator.get_translation_progress(session_id) |
|
|
|
|
|
if progress is None: |
|
|
raise HTTPException(status_code=404, detail="Session پیدا نشد یا تکمیل شده است") |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"session_id": session_id, |
|
|
**progress |
|
|
} |
|
|
|
|
|
@app.get("/api/status/{session_id}") |
|
|
async def get_translation_status(session_id: str): |
|
|
"""دریافت وضعیت کلی ترجمه""" |
|
|
progress = translator.get_translation_progress(session_id) |
|
|
|
|
|
if progress is None: |
|
|
|
|
|
if session_id in translator.completed_translations: |
|
|
completed = translator.completed_translations[session_id] |
|
|
return { |
|
|
"success": True, |
|
|
"status": "completed", |
|
|
"result": completed['result'], |
|
|
"completed_at": completed['completed_at'] |
|
|
} |
|
|
else: |
|
|
raise HTTPException(status_code=404, detail="Session پیدا نشد") |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"session_id": session_id, |
|
|
**progress |
|
|
} |
|
|
|
|
|
@app.get("/api/server-status") |
|
|
async def get_server_status(): |
|
|
"""دریافت وضعیت کلی سرور""" |
|
|
active_sessions = len(translator.translation_sessions) |
|
|
background_tasks = len(translator.translation_requests) |
|
|
completed_count = len(translator.completed_translations) |
|
|
|
|
|
|
|
|
processing_count = sum(1 for req in translator.translation_requests.values() |
|
|
if req.get('status') == 'processing') |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"server_status": "running", |
|
|
"active_sessions": active_sessions, |
|
|
"background_tasks": background_tasks, |
|
|
"processing_tasks": processing_count, |
|
|
"completed_translations": completed_count, |
|
|
"total_requests": translator.total_requests, |
|
|
"uptime": time.time(), |
|
|
"message": f"سرور فعال - {active_sessions} session فعال، {processing_count} در حال پردازش" |
|
|
} |
|
|
|
|
|
@app.post("/api/check-completion") |
|
|
async def check_completion(request: CompletionCheckRequest): |
|
|
"""بررسی تکمیل ترجمه با request_id""" |
|
|
request_id = request.request_id |
|
|
|
|
|
|
|
|
if request_id in translator.completed_translations: |
|
|
completed = translator.completed_translations[request_id] |
|
|
return { |
|
|
"success": True, |
|
|
"completed": True, |
|
|
"completed_at": completed['completed_at'], |
|
|
"processing_time": completed.get('processing_time', 0) |
|
|
} |
|
|
|
|
|
|
|
|
if request_id in translator.translation_requests: |
|
|
req_data = translator.translation_requests[request_id] |
|
|
if req_data.get('status') == 'processing': |
|
|
return { |
|
|
"success": True, |
|
|
"completed": False, |
|
|
"status": "در حال پردازش", |
|
|
"elapsed_time": time.time() - req_data['start_time'] |
|
|
} |
|
|
elif req_data.get('status') == 'failed': |
|
|
return { |
|
|
"success": False, |
|
|
"completed": False, |
|
|
"status": "ناموفق", |
|
|
"error": req_data.get('error', 'خطای ناشناخته') |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
"success": False, |
|
|
"completed": False, |
|
|
"status": "درخواست پیدا نشد" |
|
|
} |
|
|
|
|
|
@app.post("/api/check-translation-status") |
|
|
async def check_translation_status(request: StatusCheckRequest): |
|
|
"""بررسی وضعیت و نتیجه نهایی ترجمه""" |
|
|
request_id = request.request_id |
|
|
|
|
|
|
|
|
if request_id in translator.completed_translations: |
|
|
completed = translator.completed_translations[request_id] |
|
|
req_data = translator.translation_requests.get(request_id, {}) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"status": "completed", |
|
|
"translated_text": completed['result']['translated_text'], |
|
|
"processing_time": completed['result']['processing_time'], |
|
|
"chunks_count": completed['result']['chunks_count'], |
|
|
"character_count": completed['character_count'], |
|
|
"translation_length": completed['translation_length'], |
|
|
"completed_at": completed['completed_at'], |
|
|
"source_lang": req_data.get('source_lang'), |
|
|
"target_lang": req_data.get('target_lang') |
|
|
} |
|
|
|
|
|
|
|
|
if request_id in translator.translation_requests: |
|
|
req_data = translator.translation_requests[request_id] |
|
|
elapsed_time = time.time() - req_data['start_time'] |
|
|
|
|
|
if req_data.get('status') == 'processing': |
|
|
|
|
|
text_length = len(req_data.get('text', '')) |
|
|
chunks_estimate = max(1, text_length // MAX_CHUNK_SIZE) |
|
|
|
|
|
|
|
|
progress_estimate = min(90, (elapsed_time / 10) * 100) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"status": "processing", |
|
|
"progress": progress_estimate, |
|
|
"elapsed_time": elapsed_time, |
|
|
"estimated_chunks": chunks_estimate, |
|
|
"message": "در حال پردازش ترجمه..." |
|
|
} |
|
|
elif req_data.get('status') == 'failed': |
|
|
return { |
|
|
"success": False, |
|
|
"status": "failed", |
|
|
"error": req_data.get('error', 'خطای ناشناخته'), |
|
|
"elapsed_time": elapsed_time |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
"success": False, |
|
|
"status": "not_found", |
|
|
"message": "درخواست ترجمه پیدا نشد" |
|
|
} |
|
|
|
|
|
@app.post("/api/check-auto-charge-status") |
|
|
async def check_auto_charge_status(request: AutoChargeStatusRequest): |
|
|
"""بررسی وضعیت کسر اعتبار خودکار""" |
|
|
request_id = request.request_id |
|
|
|
|
|
if request_id not in translator.translation_requests: |
|
|
return { |
|
|
"success": False, |
|
|
"message": "درخواست پیدا نشد" |
|
|
} |
|
|
|
|
|
req_data = translator.translation_requests[request_id] |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"request_id": request_id, |
|
|
"auto_charge_enabled": req_data.get('auto_charge', False), |
|
|
"auto_charged": req_data.get('auto_charged', False), |
|
|
"status": req_data.get('status', 'unknown') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
translations = {} |
|
|
|
|
|
@app.post("/api/translate/heavy") |
|
|
async def heavy_translate(request: Request): |
|
|
data = await request.json() |
|
|
|
|
|
|
|
|
request_id = data.get("request_id") |
|
|
if not request_id: |
|
|
return {"success": False, "error": "request_id is required"} |
|
|
|
|
|
text = data.get("text") |
|
|
source_lang = data.get("source_lang") |
|
|
target_lang = data.get("target_lang") |
|
|
auto_charge = data.get("auto_charge", False) |
|
|
|
|
|
|
|
|
translations[request_id] = { |
|
|
"status": "processing", |
|
|
"progress": 0, |
|
|
"elapsed_time": 0, |
|
|
"message": "Translation in progress..." |
|
|
} |
|
|
|
|
|
|
|
|
asyncio.create_task(run_translation_job(request_id, text, source_lang, target_lang)) |
|
|
|
|
|
return {"success": True, "request_id": request_id, "message": "Translation started"} |
|
|
|
|
|
async def run_translation_job(request_id, text, source_lang, target_lang): |
|
|
try: |
|
|
|
|
|
for i in range(1, 10): |
|
|
await asyncio.sleep(5) |
|
|
translations[request_id]["progress"] = i * 10 |
|
|
translations[request_id]["elapsed_time"] += 5 |
|
|
|
|
|
|
|
|
result = translator.translate_text(text, source_lang, target_lang) |
|
|
|
|
|
translated_text = result['translated_text'] |
|
|
translations[request_id] = { |
|
|
"status": "completed", |
|
|
"progress": 100, |
|
|
"elapsed_time": translations[request_id]["elapsed_time"], |
|
|
"message": "Translation completed successfully.", |
|
|
"result": translated_text |
|
|
} |
|
|
|
|
|
|
|
|
translator.completed_translations[request_id] = { |
|
|
'result': result, |
|
|
'completed_at': time.time(), |
|
|
'character_count': len(text), |
|
|
'translation_length': len(translated_text) |
|
|
} |
|
|
|
|
|
print(f"✅ Translation {request_id} completed successfully") |
|
|
|
|
|
except Exception as e: |
|
|
translations[request_id] = { |
|
|
"status": "failed", |
|
|
"message": f"Error: {e}" |
|
|
} |
|
|
print(f"❌ Translation {request_id} failed: {e}") |
|
|
|
|
|
@app.post("/api/translate/session") |
|
|
async def translate_with_session(request: TranslationRequest): |
|
|
"""ترجمه با session برای پیگیری پیشرفت""" |
|
|
import uuid |
|
|
|
|
|
session_id = str(uuid.uuid4()) |
|
|
|
|
|
try: |
|
|
|
|
|
result = translator.translate_text( |
|
|
request.text, |
|
|
request.source_lang, |
|
|
request.target_lang, |
|
|
session_id |
|
|
) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"session_id": session_id, |
|
|
"translated_text": result['translated_text'], |
|
|
"processing_time": result['processing_time'], |
|
|
"chunks_count": result['chunks_count'], |
|
|
"from_cache": result.get('from_cache', False) |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
@app.get("/api/cache/stats") |
|
|
async def get_cache_stats(): |
|
|
"""آمار کش ترجمه""" |
|
|
stats = translator.cache.get_stats() |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"cache_stats": stats, |
|
|
"expiry_minutes": 60 |
|
|
} |
|
|
|
|
|
@app.post("/api/cache/clear") |
|
|
async def clear_cache(): |
|
|
"""پاک کردن کش (فقط برای مدیران)""" |
|
|
try: |
|
|
with translator.cache.lock: |
|
|
translator.cache.cache.clear() |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"message": "کش پاک شد" |
|
|
} |
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=f"خطا در پاک کردن کش: {str(e)}") |
|
|
|
|
|
@app.get("/api/stats") |
|
|
async def get_api_stats(): |
|
|
"""آمار کلی API""" |
|
|
return { |
|
|
"success": True, |
|
|
"total_requests": translator.total_requests, |
|
|
"active_sessions": len(translator.translation_sessions), |
|
|
"background_tasks": len(translator.translation_requests), |
|
|
"completed_translations": len(translator.completed_translations), |
|
|
"cache_size": translator.cache.get_stats()['cache_size'], |
|
|
"supported_languages": len(LANGUAGE_MAP), |
|
|
"model_info": { |
|
|
"name": MODEL_NAME, |
|
|
"device": str(translator.device), |
|
|
"gpu_available": torch.cuda.is_available() |
|
|
} |
|
|
} |
|
|
|
|
|
@app.post("/api/webhook/wordpress") |
|
|
async def wordpress_webhook(data: dict): |
|
|
"""Webhook برای دریافت اطلاعات از WordPress""" |
|
|
try: |
|
|
|
|
|
request_id = data.get('request_id') |
|
|
action = data.get('action') |
|
|
|
|
|
if action == 'translation_request': |
|
|
|
|
|
text = data.get('text') |
|
|
source_lang = data.get('source_lang') |
|
|
target_lang = data.get('target_lang') |
|
|
auto_charge = data.get('auto_charge', False) |
|
|
|
|
|
if not all([text, source_lang, target_lang, request_id]): |
|
|
raise HTTPException(status_code=400, detail="دادههای ناقص") |
|
|
|
|
|
|
|
|
background_tasks = BackgroundTasks() |
|
|
background_tasks.add_task( |
|
|
process_heavy_translation_background, |
|
|
text, source_lang, target_lang, request_id, auto_charge |
|
|
) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"message": "ترجمه آغاز شد", |
|
|
"request_id": request_id |
|
|
} |
|
|
|
|
|
elif action == 'status_check': |
|
|
|
|
|
if request_id in translator.completed_translations: |
|
|
completed = translator.completed_translations[request_id] |
|
|
return { |
|
|
"success": True, |
|
|
"status": "completed", |
|
|
"result": completed['result'] |
|
|
} |
|
|
elif request_id in translator.translation_requests: |
|
|
return { |
|
|
"success": True, |
|
|
"status": "processing" |
|
|
} |
|
|
else: |
|
|
return { |
|
|
"success": False, |
|
|
"status": "not_found" |
|
|
} |
|
|
|
|
|
else: |
|
|
raise HTTPException(status_code=400, detail="عمل نامعتبر") |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=f"خطا در webhook: {str(e)}") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
|
|
|
port = int(os.getenv("PORT", 7860)) |
|
|
|
|
|
print(f"راهاندازی سرور روی پورت {port}") |
|
|
print(f"مدل: {MODEL_NAME}") |
|
|
print(f"دستگاه: {translator.device}") |
|
|
print(f"زبانهای پشتیبانی شده: {len(LANGUAGE_MAP)}") |
|
|
|
|
|
uvicorn.run( |
|
|
app, |
|
|
host="0.0.0.0", |
|
|
port=port, |
|
|
log_level="info" |
|
|
) |