danicor commited on
Commit
ca4e376
·
verified ·
1 Parent(s): d7a6b74

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +96 -138
app.py CHANGED
@@ -119,11 +119,11 @@ class TranslationQueue:
119
  thread.start()
120
 
121
  class TextChunker:
122
- """کلاس برای تقسیم متن طولانی به بخش‌های کوچکتر"""
123
 
124
  @staticmethod
125
  def split_text_smart(text: str, max_chunk_size: int = 400) -> List[str]:
126
- """تقسیم هوشمند متن بر اساس جملات و پاراگراف‌ها"""
127
  logger.info(f"[CHUNKER] Starting smart text splitting | Text length: {len(text)} chars | Max chunk size: {max_chunk_size}")
128
 
129
  if len(text) <= max_chunk_size:
@@ -132,27 +132,27 @@ class TextChunker:
132
 
133
  chunks = []
134
 
135
- # تقسیم بر اساس پاراگراف‌ها
136
  paragraphs = text.split('\n\n')
137
  current_chunk = ""
138
 
139
  for i, paragraph in enumerate(paragraphs):
140
  logger.debug(f"[CHUNKER] Processing paragraph {i+1}/{len(paragraphs)} | Length: {len(paragraph)}")
141
 
142
- # اگر پاراگراف خودش بزرگ است، آن را تقسیم کن
143
  if len(paragraph) > max_chunk_size:
144
- # ذخیره قسمت فعلی اگر وجود دارد
145
  if current_chunk.strip():
146
  chunks.append(current_chunk.strip())
147
  logger.debug(f"[CHUNKER] Added chunk from accumulated paragraphs | Length: {len(current_chunk.strip())}")
148
  current_chunk = ""
149
 
150
- # تقسیم پاراگراف بزرگ
151
  sub_chunks = TextChunker._split_paragraph(paragraph, max_chunk_size)
152
  chunks.extend(sub_chunks)
153
  logger.debug(f"[CHUNKER] Split large paragraph into {len(sub_chunks)} sub-chunks")
154
  else:
155
- # بررسی اینکه آیا اضافه کردن این پاراگراف از حد تجاوز می‌کند
156
  if len(current_chunk) + len(paragraph) + 2 > max_chunk_size:
157
  if current_chunk.strip():
158
  chunks.append(current_chunk.strip())
@@ -164,7 +164,7 @@ class TextChunker:
164
  else:
165
  current_chunk = paragraph
166
 
167
- # اضافه کردن آخرین قسمت
168
  if current_chunk.strip():
169
  chunks.append(current_chunk.strip())
170
  logger.debug(f"[CHUNKER] Added final chunk | Length: {len(current_chunk.strip())}")
@@ -174,10 +174,10 @@ class TextChunker:
174
 
175
  @staticmethod
176
  def _split_paragraph(paragraph: str, max_chunk_size: int) -> List[str]:
177
- """تقسیم پاراگراف بزرگ به جملات"""
178
  logger.debug(f"[CHUNKER] Splitting large paragraph | Length: {len(paragraph)}")
179
 
180
- # تقسیم بر اساس جملات
181
  sentences = re.split(r'[.!?]+\s+', paragraph)
182
  chunks = []
183
  current_chunk = ""
@@ -186,12 +186,12 @@ class TextChunker:
186
  if not sentence.strip():
187
  continue
188
 
189
- # اضافه کردن علامت نقطه اگر حذف شده
190
  if not sentence.endswith(('.', '!', '?')):
191
  sentence += '.'
192
 
193
  if len(sentence) > max_chunk_size:
194
- # جمله خودش خیلی بلند است - تقسیم بر اساس کاما
195
  if current_chunk.strip():
196
  chunks.append(current_chunk.strip())
197
  current_chunk = ""
@@ -217,7 +217,7 @@ class TextChunker:
217
 
218
  @staticmethod
219
  def _split_by_comma(sentence: str, max_chunk_size: int) -> List[str]:
220
- """تقسیم جمله طولانی بر اساس کاما"""
221
  logger.debug(f"[CHUNKER] Splitting long sentence by comma | Length: {len(sentence)}")
222
 
223
  parts = sentence.split(', ')
@@ -226,12 +226,12 @@ class TextChunker:
226
 
227
  for part in parts:
228
  if len(part) > max_chunk_size:
229
- # قسمت خودش خیلی بلند است - تقسیم اجباری
230
  if current_chunk.strip():
231
  chunks.append(current_chunk.strip())
232
  current_chunk = ""
233
 
234
- # تقسیم اجباری بر اساس طول
235
  while len(part) > max_chunk_size:
236
  chunks.append(part[:max_chunk_size].strip())
237
  part = part[max_chunk_size:].strip()
@@ -282,9 +282,9 @@ class MultilingualTranslator:
282
  logger.error(f"[INIT] Error loading model: {e}")
283
  raise
284
 
285
- # تنظیمات بهینه برای ترجمه متن‌های بلند
286
- self.max_chunk_size = 350 # حداکثر طول هر قسمت
287
- self.min_chunk_overlap = 20 # همپوشانی بین قسمت‌ها
288
 
289
  # Track translation progress
290
  self.current_translation = {}
@@ -293,109 +293,79 @@ class MultilingualTranslator:
293
  logger.info(f"[INIT] Translator initialized | Max chunk size: {self.max_chunk_size} chars")
294
 
295
  def translate_chunk(self, text: str, source_lang: str, target_lang: str, chunk_index: int = 0, total_chunks: int = 1) -> str:
296
- """ترجمه یک قسمت کوچک از متن"""
297
  try:
298
- logger.info(f"[TRANSLATE] Starting chunk translation [{chunk_index+1}/{total_chunks}] | {source_lang} → {target_lang} | Length: {len(text)} chars")
299
-
300
- # بررسی متن ورودی
301
- if not text or not text.strip():
302
- logger.warning(f"[TRANSLATE] Empty text provided for chunk {chunk_index+1}")
303
- return ""
304
 
305
  # Set source language for tokenizer
306
  self.tokenizer.src_lang = source_lang
307
 
308
  # Encode input
309
- try:
310
- encoded = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=512).to(self.device)
311
- logger.debug(f"[TRANSLATE] Text encoded | Input tokens: {encoded.input_ids.shape[1]}")
312
- except Exception as e:
313
- logger.error(f"[TRANSLATE] Tokenization error for chunk {chunk_index+1}: {e}")
314
- return f"[Tokenization Error: {str(e)}]"
315
 
316
  # Generate translation with optimized parameters
317
  start_time = time.time()
318
- try:
319
- generated_tokens = self.model.generate(
320
- **encoded,
321
- forced_bos_token_id=self.tokenizer.get_lang_id(target_lang),
322
- max_length=1024,
323
- min_length=5,
324
- num_beams=5,
325
- early_stopping=True,
326
- no_repeat_ngram_size=3,
327
- length_penalty=1.0,
328
- repetition_penalty=1.2,
329
- do_sample=False,
330
- temperature=0.7,
331
- pad_token_id=self.tokenizer.pad_token_id,
332
- eos_token_id=self.tokenizer.eos_token_id
333
- )
334
- generation_time = time.time() - start_time
335
- except Exception as e:
336
- logger.error(f"[TRANSLATE] Generation error for chunk {chunk_index+1}: {e}")
337
- return f"[Generation Error: {str(e)}]"
338
 
339
  # Decode result
340
- try:
341
- translation = self.tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
342
-
343
- # پاک‌سازی ترجمه از کاراکترهای اضافی
344
- translation = translation.strip()
345
-
346
- # بررسی محتوای ترجمه
347
- if not translation:
348
- logger.warning(f"[TRANSLATE] Empty translation result for chunk {chunk_index+1}")
349
- return f"[Empty Translation Result for chunk {chunk_index+1}]"
350
-
351
- logger.info(f"[TRANSLATE] Chunk translation completed [{chunk_index+1}/{total_chunks}] | Generation time: {generation_time:.2f}s | Output length: {len(translation)} chars")
352
-
353
- return translation
354
-
355
- except Exception as e:
356
- logger.error(f"[TRANSLATE] Decoding error for chunk {chunk_index+1}: {e}")
357
- return f"[Decoding Error: {str(e)}]"
358
-
359
  except Exception as e:
360
  logger.error(f"[TRANSLATE] Chunk translation error [{chunk_index+1}/{total_chunks}]: {e}")
361
  return f"[Translation Error: {str(e)}]"
362
-
363
-
364
 
365
  def translate_text(self, text: str, source_lang: str, target_lang: str, session_id: str = None) -> Tuple[str, float, int]:
366
- """ترجمه متن با پشتیبانی از متن‌های طولانی و لاگ‌های مفصل"""
367
  start_time = time.time()
368
 
369
  if not session_id:
370
  session_id = hashlib.md5(f"{text[:100]}{time.time()}".encode()).hexdigest()[:8]
371
 
372
- logger.info(f"[SESSION:{session_id}] Starting translation | {source_lang} → {target_lang} | Text length: {len(text)} chars")
373
 
374
- # بررسی کش برای کل متن
375
  cached_result = self.cache.get(text, source_lang, target_lang)
376
- if cached_result and cached_result.strip(): # اطمینان از عدم خالی بودن کش
377
  logger.info(f"[SESSION:{session_id}] Translation completed from cache | Time: {time.time() - start_time:.2f}s")
378
  return cached_result, time.time() - start_time, 1
379
 
380
  try:
381
- # اگر متن کوتاه است، مستقیماً ترجمه کن
382
  if len(text) <= self.max_chunk_size:
383
  logger.info(f"[SESSION:{session_id}] Processing as short text")
384
  translation = self.translate_chunk(text, source_lang, target_lang, 0, 1)
385
 
386
- # بررسی محتوای ترجمه
387
- if not translation or not translation.strip():
388
- logger.error(f"[SESSION:{session_id}] Empty translation received for short text")
389
- return "Translation failed: Empty result received", time.time() - start_time, 0
390
-
391
- # ذخیره در کش
392
  self.cache.set(text, source_lang, target_lang, translation)
393
  processing_time = time.time() - start_time
394
  logger.info(f"[SESSION:{session_id}] Short text translation completed | Total time: {processing_time:.2f}s")
395
 
396
  return translation, processing_time, 1
397
 
398
- # تقسیم متن طولانی به قسمت‌های کوچکتر
399
  logger.info(f"[SESSION:{session_id}] Processing as long text - starting chunking")
400
  chunks = TextChunker.split_text_smart(text, self.max_chunk_size)
401
  logger.info(f"[SESSION:{session_id}] Text split into {len(chunks)} chunks")
@@ -410,16 +380,16 @@ class MultilingualTranslator:
410
  'target_lang': target_lang
411
  }
412
 
413
- # ترجمه هر قسمت
414
  translated_chunks = []
415
  for i, chunk in enumerate(chunks):
416
  chunk_start_time = time.time()
417
  logger.info(f"[SESSION:{session_id}] Starting chunk {i+1}/{len(chunks)} | Chunk length: {len(chunk)} chars")
418
 
419
- # بررسی کش برای هر قسمت
420
  chunk_translation = self.cache.get(chunk, source_lang, target_lang)
421
 
422
- if not chunk_translation or not chunk_translation.strip():
423
  # Estimate remaining time
424
  if i > 0:
425
  elapsed_time = time.time() - start_time
@@ -428,14 +398,8 @@ class MultilingualTranslator:
428
  logger.info(f"[SESSION:{session_id}] Progress: {i}/{len(chunks)} | Avg time per chunk: {avg_time_per_chunk:.1f}s | Estimated remaining: {estimated_remaining:.1f}s")
429
 
430
  chunk_translation = self.translate_chunk(chunk, source_lang, target_lang, i, len(chunks))
431
-
432
- # بررسی محتوای ترجمه قسمت
433
- if not chunk_translation or not chunk_translation.strip():
434
- logger.error(f"[SESSION:{session_id}] Empty translation for chunk {i+1}")
435
- chunk_translation = f"[Translation failed for chunk {i+1}]"
436
- else:
437
- # ذخیره قسمت در کش
438
- self.cache.set(chunk, source_lang, target_lang, chunk_translation)
439
 
440
  chunk_time = time.time() - chunk_start_time
441
  logger.info(f"[SESSION:{session_id}] Chunk {i+1}/{len(chunks)} translated in {chunk_time:.2f}s")
@@ -449,21 +413,15 @@ class MultilingualTranslator:
449
  if session_id in self.current_translation:
450
  self.current_translation[session_id]['completed_chunks'] = i + 1
451
 
452
- # کمی استراحت بین ترجمه‌ها برای جلوگیری از بارگذاری زیاد
453
  if i < len(chunks) - 1:
454
  time.sleep(0.1)
455
 
456
- # ترکیب قسمت‌های ترجمه شده
457
  logger.info(f"[SESSION:{session_id}] Combining translated chunks")
458
  final_translation = self._combine_translations(translated_chunks, text)
459
 
460
- # بررسی نهایی محتوای ترجمه
461
- if not final_translation or not final_translation.strip():
462
- logger.error(f"[SESSION:{session_id}] Final translation is empty after combining chunks")
463
- processing_time = time.time() - start_time
464
- return "Translation failed: Empty result after processing", processing_time, len(chunks)
465
-
466
- # ذخیره نتیجه نهایی در کش
467
  self.cache.set(text, source_lang, target_lang, final_translation)
468
 
469
  processing_time = time.time() - start_time
@@ -481,8 +439,6 @@ class MultilingualTranslator:
481
  with self.translation_lock:
482
  self.current_translation.pop(session_id, None)
483
  return f"Translation error: {str(e)}", time.time() - start_time, 0
484
-
485
-
486
 
487
  def get_translation_progress(self, session_id: str) -> Dict:
488
  """Get current translation progress"""
@@ -509,9 +465,8 @@ class MultilingualTranslator:
509
  }
510
 
511
  def _combine_translations(self, translated_chunks: List[str], original_text: str) -> str:
512
- """ترکیب قسمت‌های ترجمه شده به یک متن یکپارچه"""
513
  if not translated_chunks:
514
- logger.warning("[COMBINER] No chunks to combine")
515
  return ""
516
 
517
  if len(translated_chunks) == 1:
@@ -519,31 +474,23 @@ class MultilingualTranslator:
519
 
520
  logger.debug(f"[COMBINER] Combining {len(translated_chunks)} translated chunks")
521
 
522
- # فیلتر کردن قسمت‌های خالی یا خرابه
523
- valid_chunks = []
524
- for i, chunk in enumerate(translated_chunks):
525
- chunk = chunk.strip() if chunk else ""
526
-
527
- # حذف قسمت‌هایی که پیام خطا هستند
528
- if chunk and not chunk.startswith('[') and not chunk.startswith('Translation Error'):
529
- valid_chunks.append(chunk)
530
- else:
531
- logger.warning(f"[COMBINER] Skipping invalid chunk {i}: {chunk[:50] if chunk else 'Empty'}")
532
-
533
- if not valid_chunks:
534
- logger.error("[COMBINER] No valid chunks found")
535
- return "Translation failed: No valid content produced"
536
-
537
- # ترکیب قسمت‌ها با در نظر گیری ساختار اصلی متن
538
  combined = []
539
 
540
- for i, chunk in enumerate(valid_chunks):
 
 
 
 
 
 
 
541
  if i > 0 and combined:
542
- # اگر قسمت قبلی با نقطه تمام نمی‌شود، نقطه اضافه کن
543
- if not combined[-1].rstrip().endswith(('.', '!', '?', ':', '؛', '.')):
544
  combined[-1] += '.'
545
 
546
- # بررسی اینکه آیا نیاز به پاراگراف جدید داریم
547
  if '\n\n' in original_text:
548
  combined.append('\n\n' + chunk)
549
  else:
@@ -553,9 +500,9 @@ class MultilingualTranslator:
553
 
554
  result = ''.join(combined)
555
 
556
- # پاک‌سازی نهایی
557
- result = re.sub(r'\s+', ' ', result) # حذف فاصله‌های اضافی
558
- result = re.sub(r'\.+', '.', result) # حذف نقطه‌های تکراری
559
  result = result.strip()
560
 
561
  logger.debug(f"[COMBINER] Combined translation length: {len(result)} chars")
@@ -570,7 +517,6 @@ class MultilingualTranslator:
570
  text, source_lang, target_lang, session_id
571
  )
572
 
573
-
574
  # Language mappings for M2M100 model
575
  LANGUAGE_MAP = {
576
  "English": "en",
@@ -707,7 +653,7 @@ async def api_translate(request: TranslationRequest):
707
  # Alternative endpoint for form data (compatibility with WordPress)
708
  @app.post("/api/translate/form")
709
  async def api_translate_form(request: Request):
710
- """Non-blocking translation endpoint"""
711
  try:
712
  form_data = await request.form()
713
  text = form_data.get("text", "")
@@ -727,13 +673,15 @@ async def api_translate_form(request: Request):
727
  logger.info(f"[FORM API] Translation request | {source_lang} → {target_lang} | Length: {len(text)} chars")
728
 
729
  if not text.strip():
730
- raise HTTPException(status_code=400, detail="No text provided")
 
731
 
732
  source_code = LANGUAGE_MAP.get(source_lang)
733
  target_code = LANGUAGE_MAP.get(target_lang)
734
 
735
  if not source_code or not target_code:
736
- raise HTTPException(status_code=400, detail="Invalid language codes")
 
737
 
738
  # Generate session ID for tracking
739
  session_id = hashlib.md5(f"{text[:100]}{time.time()}".encode()).hexdigest()[:8]
@@ -762,6 +710,16 @@ async def api_translate_form(request: Request):
762
  text, source_code, target_code, session_id
763
  )
764
 
 
 
 
 
 
 
 
 
 
 
765
  return {
766
  "translation": translation,
767
  "source_language": source_lang,
@@ -774,7 +732,7 @@ async def api_translate_form(request: Request):
774
  }
775
  except Exception as e:
776
  logger.error(f"[FORM API] Translation error: {str(e)}")
777
- raise HTTPException(status_code=500, detail=f"Translation error: {str(e)}")
778
 
779
  @app.get("/api/progress/{session_id}")
780
  async def get_translation_progress(session_id: str):
@@ -900,7 +858,7 @@ async def get_server_status():
900
  if active_sessions or background_tasks_count > 0:
901
  if active_sessions:
902
  latest_session = active_sessions[-1]
903
- message = f"Processing chunk {latest_session['completed_chunks']}/{latest_session['total_chunks']} | {latest_session['source_lang']} → {latest_session['target_lang']}"
904
  else:
905
  message = f"{background_tasks_count} translation(s) in background queue"
906
 
@@ -927,7 +885,7 @@ async def get_server_status():
927
  return {
928
  "has_active_translation": True,
929
  "status": "processing",
930
- "message": f"Processing chunk {latest_session['completed_chunks']}/{latest_session['total_chunks']} | {latest_session['source_lang']} → {latest_session['target_lang']}",
931
  "session_data": latest_session
932
  }
933
  else:
 
119
  thread.start()
120
 
121
  class TextChunker:
122
+ """کلاس برای تقسیم متن طولانی به بخش‌های کوچکتر"""
123
 
124
  @staticmethod
125
  def split_text_smart(text: str, max_chunk_size: int = 400) -> List[str]:
126
+ """تقسیم هوشمند متن بر اساس جملات و پاراگراف‌ها"""
127
  logger.info(f"[CHUNKER] Starting smart text splitting | Text length: {len(text)} chars | Max chunk size: {max_chunk_size}")
128
 
129
  if len(text) <= max_chunk_size:
 
132
 
133
  chunks = []
134
 
135
+ # تقسیم بر اساس پاراگراف‌ها
136
  paragraphs = text.split('\n\n')
137
  current_chunk = ""
138
 
139
  for i, paragraph in enumerate(paragraphs):
140
  logger.debug(f"[CHUNKER] Processing paragraph {i+1}/{len(paragraphs)} | Length: {len(paragraph)}")
141
 
142
+ # اگر پاراگراف خودش بزرگ است، آن را تقسیم کن
143
  if len(paragraph) > max_chunk_size:
144
+ # ذخیره قسمت فعلی اگر وجود دارد
145
  if current_chunk.strip():
146
  chunks.append(current_chunk.strip())
147
  logger.debug(f"[CHUNKER] Added chunk from accumulated paragraphs | Length: {len(current_chunk.strip())}")
148
  current_chunk = ""
149
 
150
+ # تقسیم پاراگراف بزرگ
151
  sub_chunks = TextChunker._split_paragraph(paragraph, max_chunk_size)
152
  chunks.extend(sub_chunks)
153
  logger.debug(f"[CHUNKER] Split large paragraph into {len(sub_chunks)} sub-chunks")
154
  else:
155
+ # بررسی اینکه آیا اضافه کردن این پاراگراف از حد تجاوز می‌کند
156
  if len(current_chunk) + len(paragraph) + 2 > max_chunk_size:
157
  if current_chunk.strip():
158
  chunks.append(current_chunk.strip())
 
164
  else:
165
  current_chunk = paragraph
166
 
167
+ # اضافه کردن آخرین قسمت
168
  if current_chunk.strip():
169
  chunks.append(current_chunk.strip())
170
  logger.debug(f"[CHUNKER] Added final chunk | Length: {len(current_chunk.strip())}")
 
174
 
175
  @staticmethod
176
  def _split_paragraph(paragraph: str, max_chunk_size: int) -> List[str]:
177
+ """تقسیم پاراگراف بزرگ به جملات"""
178
  logger.debug(f"[CHUNKER] Splitting large paragraph | Length: {len(paragraph)}")
179
 
180
+ # تقسیم بر اساس جملات
181
  sentences = re.split(r'[.!?]+\s+', paragraph)
182
  chunks = []
183
  current_chunk = ""
 
186
  if not sentence.strip():
187
  continue
188
 
189
+ # اضافه کردن علامت نقطه اگر حذف شده
190
  if not sentence.endswith(('.', '!', '?')):
191
  sentence += '.'
192
 
193
  if len(sentence) > max_chunk_size:
194
+ # جمله خودش خیلی بلند است - تقسیم بر اساس کاما
195
  if current_chunk.strip():
196
  chunks.append(current_chunk.strip())
197
  current_chunk = ""
 
217
 
218
  @staticmethod
219
  def _split_by_comma(sentence: str, max_chunk_size: int) -> List[str]:
220
+ """تقسیم جمله طولانی بر اساس کاما"""
221
  logger.debug(f"[CHUNKER] Splitting long sentence by comma | Length: {len(sentence)}")
222
 
223
  parts = sentence.split(', ')
 
226
 
227
  for part in parts:
228
  if len(part) > max_chunk_size:
229
+ # قسمت خودش خیلی بلند است - تقسیم اجباری
230
  if current_chunk.strip():
231
  chunks.append(current_chunk.strip())
232
  current_chunk = ""
233
 
234
+ # تقسیم اجباری بر اساس طول
235
  while len(part) > max_chunk_size:
236
  chunks.append(part[:max_chunk_size].strip())
237
  part = part[max_chunk_size:].strip()
 
282
  logger.error(f"[INIT] Error loading model: {e}")
283
  raise
284
 
285
+ # تنظیمات بهینه برای ترجمه متن‌های بلند
286
+ self.max_chunk_size = 350 # حداکثر طول هر قسمت
287
+ self.min_chunk_overlap = 20 # همپوشانی بین قسمت‌ها
288
 
289
  # Track translation progress
290
  self.current_translation = {}
 
293
  logger.info(f"[INIT] Translator initialized | Max chunk size: {self.max_chunk_size} chars")
294
 
295
  def translate_chunk(self, text: str, source_lang: str, target_lang: str, chunk_index: int = 0, total_chunks: int = 1) -> str:
296
+ """ترجمه یک قسمت کوچک از متن"""
297
  try:
298
+ logger.info(f"[TRANSLATE] Starting chunk translation [{chunk_index+1}/{total_chunks}] | {source_lang} → {target_lang} | Length: {len(text)} chars")
 
 
 
 
 
299
 
300
  # Set source language for tokenizer
301
  self.tokenizer.src_lang = source_lang
302
 
303
  # Encode input
304
+ encoded = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=512).to(self.device)
305
+ logger.debug(f"[TRANSLATE] Text encoded | Input tokens: {encoded.input_ids.shape[1]}")
 
 
 
 
306
 
307
  # Generate translation with optimized parameters
308
  start_time = time.time()
309
+ generated_tokens = self.model.generate(
310
+ **encoded,
311
+ forced_bos_token_id=self.tokenizer.get_lang_id(target_lang),
312
+ max_length=1024, # افزایش طول خروجی
313
+ min_length=10, # حداقل طول خروجی
314
+ num_beams=5, # افزایش تعداد beam ها برای کیفیت بهتر
315
+ early_stopping=True,
316
+ no_repeat_ngram_size=3, # جلوگیری از تکرار
317
+ length_penalty=1.0, # تنظیم جریمه طول
318
+ repetition_penalty=1.2, # جلوگیری از تکرار کلمات
319
+ do_sample=False, # استفاده از روش قطعی
320
+ temperature=0.7, # کنترل تنوع
321
+ pad_token_id=self.tokenizer.pad_token_id,
322
+ eos_token_id=self.tokenizer.eos_token_id
323
+ )
324
+ generation_time = time.time() - start_time
 
 
 
 
325
 
326
  # Decode result
327
+ translation = self.tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
328
+
329
+ # پاک‌سازی ترجمه از کاراکترهای اضافی
330
+ translation = translation.strip()
331
+
332
+ logger.info(f"[TRANSLATE] Chunk translation completed [{chunk_index+1}/{total_chunks}] | Generation time: {generation_time:.2f}s | Output length: {len(translation)} chars")
333
+
334
+ return translation
335
+
 
 
 
 
 
 
 
 
 
 
336
  except Exception as e:
337
  logger.error(f"[TRANSLATE] Chunk translation error [{chunk_index+1}/{total_chunks}]: {e}")
338
  return f"[Translation Error: {str(e)}]"
 
 
339
 
340
  def translate_text(self, text: str, source_lang: str, target_lang: str, session_id: str = None) -> Tuple[str, float, int]:
341
+ """ترجمه متن با پشتیبانی از متن‌های طولانی و لاگ‌های مفصل"""
342
  start_time = time.time()
343
 
344
  if not session_id:
345
  session_id = hashlib.md5(f"{text[:100]}{time.time()}".encode()).hexdigest()[:8]
346
 
347
+ logger.info(f"[SESSION:{session_id}] Starting translation | {source_lang} → {target_lang} | Text length: {len(text)} chars")
348
 
349
+ # بررسی کش برای کل متن
350
  cached_result = self.cache.get(text, source_lang, target_lang)
351
+ if cached_result:
352
  logger.info(f"[SESSION:{session_id}] Translation completed from cache | Time: {time.time() - start_time:.2f}s")
353
  return cached_result, time.time() - start_time, 1
354
 
355
  try:
356
+ # اگر متن کوتاه است، مستقیماً ترجمه کن
357
  if len(text) <= self.max_chunk_size:
358
  logger.info(f"[SESSION:{session_id}] Processing as short text")
359
  translation = self.translate_chunk(text, source_lang, target_lang, 0, 1)
360
 
361
+ # ذخیره در کش
 
 
 
 
 
362
  self.cache.set(text, source_lang, target_lang, translation)
363
  processing_time = time.time() - start_time
364
  logger.info(f"[SESSION:{session_id}] Short text translation completed | Total time: {processing_time:.2f}s")
365
 
366
  return translation, processing_time, 1
367
 
368
+ # تقسیم متن طولانی به قسمت‌های کوچکتر
369
  logger.info(f"[SESSION:{session_id}] Processing as long text - starting chunking")
370
  chunks = TextChunker.split_text_smart(text, self.max_chunk_size)
371
  logger.info(f"[SESSION:{session_id}] Text split into {len(chunks)} chunks")
 
380
  'target_lang': target_lang
381
  }
382
 
383
+ # ترجمه هر قسمت
384
  translated_chunks = []
385
  for i, chunk in enumerate(chunks):
386
  chunk_start_time = time.time()
387
  logger.info(f"[SESSION:{session_id}] Starting chunk {i+1}/{len(chunks)} | Chunk length: {len(chunk)} chars")
388
 
389
+ # بررسی کش برای هر قسمت
390
  chunk_translation = self.cache.get(chunk, source_lang, target_lang)
391
 
392
+ if not chunk_translation:
393
  # Estimate remaining time
394
  if i > 0:
395
  elapsed_time = time.time() - start_time
 
398
  logger.info(f"[SESSION:{session_id}] Progress: {i}/{len(chunks)} | Avg time per chunk: {avg_time_per_chunk:.1f}s | Estimated remaining: {estimated_remaining:.1f}s")
399
 
400
  chunk_translation = self.translate_chunk(chunk, source_lang, target_lang, i, len(chunks))
401
+ # ذخیره قسمت در کش
402
+ self.cache.set(chunk, source_lang, target_lang, chunk_translation)
 
 
 
 
 
 
403
 
404
  chunk_time = time.time() - chunk_start_time
405
  logger.info(f"[SESSION:{session_id}] Chunk {i+1}/{len(chunks)} translated in {chunk_time:.2f}s")
 
413
  if session_id in self.current_translation:
414
  self.current_translation[session_id]['completed_chunks'] = i + 1
415
 
416
+ # کمی استراحت بین ترجمه‌ها برای جلوگیری از بارگذاری زیاد
417
  if i < len(chunks) - 1:
418
  time.sleep(0.1)
419
 
420
+ # ترکیب قسمت‌های ترجمه شده
421
  logger.info(f"[SESSION:{session_id}] Combining translated chunks")
422
  final_translation = self._combine_translations(translated_chunks, text)
423
 
424
+ # ��°Ø®ÛŒØ±Ù‡ نتیجه نهایی در Ú©Ø´
 
 
 
 
 
 
425
  self.cache.set(text, source_lang, target_lang, final_translation)
426
 
427
  processing_time = time.time() - start_time
 
439
  with self.translation_lock:
440
  self.current_translation.pop(session_id, None)
441
  return f"Translation error: {str(e)}", time.time() - start_time, 0
 
 
442
 
443
  def get_translation_progress(self, session_id: str) -> Dict:
444
  """Get current translation progress"""
 
465
  }
466
 
467
  def _combine_translations(self, translated_chunks: List[str], original_text: str) -> str:
468
+ """ترکیب قسمت‌های ترجمه شده به یک متن یکپارچه"""
469
  if not translated_chunks:
 
470
  return ""
471
 
472
  if len(translated_chunks) == 1:
 
474
 
475
  logger.debug(f"[COMBINER] Combining {len(translated_chunks)} translated chunks")
476
 
477
+ # ترکیب قسمت‌ها با در نظر گیری ساختار اصلی متن
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  combined = []
479
 
480
+ for i, chunk in enumerate(translated_chunks):
481
+ # پاک‌سازی قسمت
482
+ chunk = chunk.strip()
483
+
484
+ if not chunk:
485
+ continue
486
+
487
+ # اضافه کردن فاصله مناسب بین قسمت‌ها
488
  if i > 0 and combined:
489
+ # اگر قسمت قبلی با نقطه تمام نمی‌شود، نقطه اضافه کن
490
+ if not combined[-1].rstrip().endswith(('.', '!', '?', ':', 'Ø›', '.')):
491
  combined[-1] += '.'
492
 
493
+ # بررسی اینکه آیا نیاز به پاراگراف جدید داریم
494
  if '\n\n' in original_text:
495
  combined.append('\n\n' + chunk)
496
  else:
 
500
 
501
  result = ''.join(combined)
502
 
503
+ # پاک‌سازی نهایی
504
+ result = re.sub(r'\s+', ' ', result) # حذف فاصله‌های اضافی
505
+ result = re.sub(r'\.+', '.', result) # حذف نقطه‌های تکراری
506
  result = result.strip()
507
 
508
  logger.debug(f"[COMBINER] Combined translation length: {len(result)} chars")
 
517
  text, source_lang, target_lang, session_id
518
  )
519
 
 
520
  # Language mappings for M2M100 model
521
  LANGUAGE_MAP = {
522
  "English": "en",
 
653
  # Alternative endpoint for form data (compatibility with WordPress)
654
  @app.post("/api/translate/form")
655
  async def api_translate_form(request: Request):
656
+ """Non-blocking translation endpoint with enhanced error handling"""
657
  try:
658
  form_data = await request.form()
659
  text = form_data.get("text", "")
 
673
  logger.info(f"[FORM API] Translation request | {source_lang} → {target_lang} | Length: {len(text)} chars")
674
 
675
  if not text.strip():
676
+ logger.error("[FORM API] No text provided")
677
+ return {"status": "error", "message": "No text provided"}
678
 
679
  source_code = LANGUAGE_MAP.get(source_lang)
680
  target_code = LANGUAGE_MAP.get(target_lang)
681
 
682
  if not source_code or not target_code:
683
+ logger.error(f"[FORM API] Invalid language codes: {source_lang} -> {target_lang}")
684
+ return {"status": "error", "message": "Invalid language codes"}
685
 
686
  # Generate session ID for tracking
687
  session_id = hashlib.md5(f"{text[:100]}{time.time()}".encode()).hexdigest()[:8]
 
710
  text, source_code, target_code, session_id
711
  )
712
 
713
+ # بررسی محتوای ترجمه
714
+ if not translation or not translation.strip() or translation.startswith("Translation error"):
715
+ logger.error(f"[FORM API] Invalid translation result: {translation[:100] if translation else 'None'}")
716
+ return {
717
+ "status": "error",
718
+ "message": "Translation failed - empty or invalid result",
719
+ "session_id": session_id
720
+ }
721
+
722
+ logger.info(f"[FORM API] Translation successful | Length: {len(translation)} chars")
723
  return {
724
  "translation": translation,
725
  "source_language": source_lang,
 
732
  }
733
  except Exception as e:
734
  logger.error(f"[FORM API] Translation error: {str(e)}")
735
+ return {"status": "error", "message": f"Translation error: {str(e)}"}
736
 
737
  @app.get("/api/progress/{session_id}")
738
  async def get_translation_progress(session_id: str):
 
858
  if active_sessions or background_tasks_count > 0:
859
  if active_sessions:
860
  latest_session = active_sessions[-1]
861
+ message = f"Processing chunk {latest_session['completed_chunks']}/{latest_session['total_chunks']} | {latest_session['source_lang']} → {latest_session['target_lang']}"
862
  else:
863
  message = f"{background_tasks_count} translation(s) in background queue"
864
 
 
885
  return {
886
  "has_active_translation": True,
887
  "status": "processing",
888
+ "message": f"Processing chunk {latest_session['completed_chunks']}/{latest_session['total_chunks']} | {latest_session['source_lang']} → {latest_session['target_lang']}",
889
  "session_data": latest_session
890
  }
891
  else: