danicor commited on
Commit
b250f6c
·
verified ·
1 Parent(s): 49a587c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +257 -29
app.py CHANGED
@@ -1,9 +1,9 @@
1
- # app.py
2
  import torch
3
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
4
  import time
5
  import json
6
  import hashlib
 
7
  from datetime import datetime, timedelta
8
  import threading
9
  from queue import Queue
@@ -32,6 +32,7 @@ class TranslationResponse(BaseModel):
32
  processing_time: float
33
  character_count: int
34
  status: str
 
35
 
36
  class TranslationCache:
37
  def __init__(self, cache_duration_minutes: int = 60):
@@ -99,6 +100,127 @@ class TranslationQueue:
99
  thread = threading.Thread(target=worker)
100
  thread.start()
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  class MultilingualTranslator:
103
  def __init__(self, cache_duration_minutes: int = 60):
104
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -120,46 +242,148 @@ class MultilingualTranslator:
120
  except Exception as e:
121
  logger.error(f"Error loading model: {e}")
122
  raise
123
-
124
- def translate_text(self, text: str, source_lang: str, target_lang: str) -> Tuple[str, float]:
125
- """Translate text from source to target language"""
126
- start_time = time.time()
127
-
128
- # Check cache first
129
- cached_result = self.cache.get(text, source_lang, target_lang)
130
- if cached_result:
131
- return cached_result, time.time() - start_time
132
 
 
 
 
 
 
 
133
  try:
134
  # Set source language for tokenizer
135
  self.tokenizer.src_lang = source_lang
136
 
137
  # Encode input
138
- encoded = self.tokenizer(text, return_tensors="pt").to(self.device)
139
 
140
- # Generate translation
141
  generated_tokens = self.model.generate(
142
  **encoded,
143
  forced_bos_token_id=self.tokenizer.get_lang_id(target_lang),
144
- max_length=512,
145
- num_beams=4,
146
- early_stopping=True
 
 
 
 
 
 
 
 
147
  )
148
 
149
  # Decode result
150
  translation = self.tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
151
 
152
- # Cache the result
153
- self.cache.set(text, source_lang, target_lang, translation)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
  processing_time = time.time() - start_time
156
- logger.info(f"Translation completed in {processing_time:.2f} seconds")
157
 
158
- return translation, processing_time
159
 
160
  except Exception as e:
161
  logger.error(f"Translation error: {e}")
162
- return f"Translation error: {str(e)}", time.time() - start_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  # Language mappings for M2M100 model
165
  LANGUAGE_MAP = {
@@ -236,7 +460,7 @@ LANGUAGE_MAP = {
236
  translator = MultilingualTranslator(60)
237
 
238
  # Create FastAPI app
239
- app = FastAPI(title="Multilingual Translation API", version="1.0.0")
240
 
241
  # Add CORS middleware
242
  app.add_middleware(
@@ -249,11 +473,11 @@ app.add_middleware(
249
 
250
  @app.get("/")
251
  async def root():
252
- return {"message": "Multilingual Translation API", "status": "active"}
253
 
254
  @app.post("/api/translate")
255
  async def api_translate(request: TranslationRequest):
256
- """API endpoint for translation"""
257
  if not request.text.strip():
258
  raise HTTPException(status_code=400, detail="No text provided")
259
 
@@ -264,7 +488,7 @@ async def api_translate(request: TranslationRequest):
264
  raise HTTPException(status_code=400, detail="Invalid language codes")
265
 
266
  try:
267
- translation, processing_time = translator.translate_text(request.text, source_code, target_code)
268
 
269
  return TranslationResponse(
270
  translation=translation,
@@ -272,7 +496,8 @@ async def api_translate(request: TranslationRequest):
272
  target_language=request.target_lang,
273
  processing_time=processing_time,
274
  character_count=len(request.text),
275
- status="success"
 
276
  )
277
  except Exception as e:
278
  raise HTTPException(status_code=500, detail=f"Translation error: {str(e)}")
@@ -280,7 +505,7 @@ async def api_translate(request: TranslationRequest):
280
  # Alternative endpoint for form data (compatibility with WordPress)
281
  @app.post("/api/translate/form")
282
  async def api_translate_form(request: Request):
283
- """Alternative endpoint that accepts form data"""
284
  try:
285
  form_data = await request.form()
286
  text = form_data.get("text", "")
@@ -308,7 +533,7 @@ async def api_translate_form(request: Request):
308
  raise HTTPException(status_code=400, detail="Invalid language codes")
309
 
310
  try:
311
- translation, processing_time = translator.translate_text(text, source_code, target_code)
312
 
313
  return {
314
  "translation": translation,
@@ -316,7 +541,8 @@ async def api_translate_form(request: Request):
316
  "target_language": target_lang,
317
  "processing_time": processing_time,
318
  "character_count": len(text),
319
- "status": "success"
 
320
  }
321
  except Exception as e:
322
  raise HTTPException(status_code=500, detail=f"Translation error: {str(e)}")
@@ -337,7 +563,9 @@ async def health_check():
337
  "status": "healthy",
338
  "device": str(translator.device),
339
  "model": translator.model_name,
340
- "cache_size": len(translator.cache.cache)
 
 
341
  }
342
 
343
  if __name__ == "__main__":
 
 
1
  import torch
2
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
3
  import time
4
  import json
5
  import hashlib
6
+ import re
7
  from datetime import datetime, timedelta
8
  import threading
9
  from queue import Queue
 
32
  processing_time: float
33
  character_count: int
34
  status: str
35
+ chunks_processed: Optional[int] = None
36
 
37
  class TranslationCache:
38
  def __init__(self, cache_duration_minutes: int = 60):
 
100
  thread = threading.Thread(target=worker)
101
  thread.start()
102
 
103
+ class TextChunker:
104
+ """کلاس برای تقسیم متن طولانی به بخش‌های کوچکتر"""
105
+
106
+ @staticmethod
107
+ def split_text_smart(text: str, max_chunk_size: int = 400) -> List[str]:
108
+ """تقسیم هوشمند متن بر اساس جملات و پاراگراف‌ها"""
109
+ if len(text) <= max_chunk_size:
110
+ return [text]
111
+
112
+ chunks = []
113
+
114
+ # تقسیم بر اساس پاراگراف‌ها
115
+ paragraphs = text.split('\n\n')
116
+ current_chunk = ""
117
+
118
+ for paragraph in paragraphs:
119
+ # اگر پاراگراف خودش بزرگ است، آن را تقسیم کن
120
+ if len(paragraph) > max_chunk_size:
121
+ # ذخیره قسمت فعلی اگر وجود دارد
122
+ if current_chunk.strip():
123
+ chunks.append(current_chunk.strip())
124
+ current_chunk = ""
125
+
126
+ # تقسیم پاراگراف بزرگ
127
+ sub_chunks = TextChunker._split_paragraph(paragraph, max_chunk_size)
128
+ chunks.extend(sub_chunks)
129
+ else:
130
+ # بررسی اینکه آیا اضافه کردن این پاراگراف از حد تجاوز می‌کند
131
+ if len(current_chunk) + len(paragraph) + 2 > max_chunk_size:
132
+ if current_chunk.strip():
133
+ chunks.append(current_chunk.strip())
134
+ current_chunk = paragraph
135
+ else:
136
+ if current_chunk:
137
+ current_chunk += "\n\n" + paragraph
138
+ else:
139
+ current_chunk = paragraph
140
+
141
+ # اضافه کردن آخرین قسمت
142
+ if current_chunk.strip():
143
+ chunks.append(current_chunk.strip())
144
+
145
+ return chunks
146
+
147
+ @staticmethod
148
+ def _split_paragraph(paragraph: str, max_chunk_size: int) -> List[str]:
149
+ """تقسیم پاراگراف بزرگ به جملات"""
150
+ # تقسیم بر اساس جملات
151
+ sentences = re.split(r'[.!?]+\s+', paragraph)
152
+ chunks = []
153
+ current_chunk = ""
154
+
155
+ for sentence in sentences:
156
+ if not sentence.strip():
157
+ continue
158
+
159
+ # اضافه کردن علامت نقطه اگر حذف شده
160
+ if not sentence.endswith(('.', '!', '?')):
161
+ sentence += '.'
162
+
163
+ if len(sentence) > max_chunk_size:
164
+ # جمله خودش خیلی بلند است - تقسیم بر اساس کاما
165
+ if current_chunk.strip():
166
+ chunks.append(current_chunk.strip())
167
+ current_chunk = ""
168
+
169
+ sub_chunks = TextChunker._split_by_comma(sentence, max_chunk_size)
170
+ chunks.extend(sub_chunks)
171
+ else:
172
+ if len(current_chunk) + len(sentence) + 1 > max_chunk_size:
173
+ if current_chunk.strip():
174
+ chunks.append(current_chunk.strip())
175
+ current_chunk = sentence
176
+ else:
177
+ if current_chunk:
178
+ current_chunk += " " + sentence
179
+ else:
180
+ current_chunk = sentence
181
+
182
+ if current_chunk.strip():
183
+ chunks.append(current_chunk.strip())
184
+
185
+ return chunks
186
+
187
+ @staticmethod
188
+ def _split_by_comma(sentence: str, max_chunk_size: int) -> List[str]:
189
+ """تقسیم جمله طولانی بر اساس کاما"""
190
+ parts = sentence.split(', ')
191
+ chunks = []
192
+ current_chunk = ""
193
+
194
+ for part in parts:
195
+ if len(part) > max_chunk_size:
196
+ # قسمت خودش خیلی بلند است - تقسیم اجباری
197
+ if current_chunk.strip():
198
+ chunks.append(current_chunk.strip())
199
+ current_chunk = ""
200
+
201
+ # تقسیم اجباری بر اساس طول
202
+ while len(part) > max_chunk_size:
203
+ chunks.append(part[:max_chunk_size].strip())
204
+ part = part[max_chunk_size:].strip()
205
+
206
+ if part:
207
+ current_chunk = part
208
+ else:
209
+ if len(current_chunk) + len(part) + 2 > max_chunk_size:
210
+ if current_chunk.strip():
211
+ chunks.append(current_chunk.strip())
212
+ current_chunk = part
213
+ else:
214
+ if current_chunk:
215
+ current_chunk += ", " + part
216
+ else:
217
+ current_chunk = part
218
+
219
+ if current_chunk.strip():
220
+ chunks.append(current_chunk.strip())
221
+
222
+ return chunks
223
+
224
  class MultilingualTranslator:
225
  def __init__(self, cache_duration_minutes: int = 60):
226
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
242
  except Exception as e:
243
  logger.error(f"Error loading model: {e}")
244
  raise
 
 
 
 
 
 
 
 
 
245
 
246
+ # تنظیمات بهینه برای ترجمه متن‌های بلند
247
+ self.max_chunk_size = 350 # حداکثر طول هر قسمت
248
+ self.min_chunk_overlap = 20 # همپوشانی بین قسمت‌ها
249
+
250
+ def translate_chunk(self, text: str, source_lang: str, target_lang: str) -> str:
251
+ """ترجمه یک قسمت کوچک از متن"""
252
  try:
253
  # Set source language for tokenizer
254
  self.tokenizer.src_lang = source_lang
255
 
256
  # Encode input
257
+ encoded = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=512).to(self.device)
258
 
259
+ # Generate translation with optimized parameters
260
  generated_tokens = self.model.generate(
261
  **encoded,
262
  forced_bos_token_id=self.tokenizer.get_lang_id(target_lang),
263
+ max_length=1024, # افزایش طول خروجی
264
+ min_length=10, # حداقل طول خروجی
265
+ num_beams=5, # افزایش تعداد beam ها برای کیفیت بهتر
266
+ early_stopping=True,
267
+ no_repeat_ngram_size=3, # جلوگیری از تکرار
268
+ length_penalty=1.0, # تنظیم جریمه طول
269
+ repetition_penalty=1.2, # جلوگیری از تکرار کلمات
270
+ do_sample=False, # استفاده از روش قطعی
271
+ temperature=0.7, # کنترل تنوع
272
+ pad_token_id=self.tokenizer.pad_token_id,
273
+ eos_token_id=self.tokenizer.eos_token_id
274
  )
275
 
276
  # Decode result
277
  translation = self.tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
278
 
279
+ # پاک‌سازی ترجمه از کاراکترهای اضافی
280
+ translation = translation.strip()
281
+
282
+ return translation
283
+
284
+ except Exception as e:
285
+ logger.error(f"Chunk translation error: {e}")
286
+ return f"[Translation Error: {str(e)}]"
287
+
288
+ def translate_text(self, text: str, source_lang: str, target_lang: str) -> Tuple[str, float, int]:
289
+ """ترجمه متن با پشتیبانی از متن‌های طولانی"""
290
+ start_time = time.time()
291
+
292
+ # بررسی کش برای کل متن
293
+ cached_result = self.cache.get(text, source_lang, target_lang)
294
+ if cached_result:
295
+ return cached_result, time.time() - start_time, 1
296
+
297
+ try:
298
+ # اگر متن کوتاه است، مستقیماً ترجمه کن
299
+ if len(text) <= self.max_chunk_size:
300
+ translation = self.translate_chunk(text, source_lang, target_lang)
301
+
302
+ # ذخیره در کش
303
+ self.cache.set(text, source_lang, target_lang, translation)
304
+ processing_time = time.time() - start_time
305
+ logger.info(f"Short text translation completed in {processing_time:.2f} seconds")
306
+
307
+ return translation, processing_time, 1
308
+
309
+ # تقسیم متن طولانی به قسمت‌های کوچکتر
310
+ chunks = TextChunker.split_text_smart(text, self.max_chunk_size)
311
+ logger.info(f"Split long text into {len(chunks)} chunks")
312
+
313
+ # ترجمه هر قسمت
314
+ translated_chunks = []
315
+ for i, chunk in enumerate(chunks):
316
+ logger.info(f"Translating chunk {i+1}/{len(chunks)} (length: {len(chunk)})")
317
+
318
+ # بررسی کش برای هر قسمت
319
+ chunk_translation = self.cache.get(chunk, source_lang, target_lang)
320
+
321
+ if not chunk_translation:
322
+ chunk_translation = self.translate_chunk(chunk, source_lang, target_lang)
323
+ # ذخیره قسمت در کش
324
+ self.cache.set(chunk, source_lang, target_lang, chunk_translation)
325
+
326
+ translated_chunks.append(chunk_translation)
327
+
328
+ # کمی استراحت بین ترجمه‌ها برای جلوگیری از بارگذاری زیاد
329
+ if i < len(chunks) - 1:
330
+ time.sleep(0.1)
331
+
332
+ # ترکیب قسمت‌های ترجمه شده
333
+ final_translation = self._combine_translations(translated_chunks, text)
334
+
335
+ # ذخیره نتیجه نهایی در کش
336
+ self.cache.set(text, source_lang, target_lang, final_translation)
337
 
338
  processing_time = time.time() - start_time
339
+ logger.info(f"Long text translation completed in {processing_time:.2f} seconds ({len(chunks)} chunks)")
340
 
341
+ return final_translation, processing_time, len(chunks)
342
 
343
  except Exception as e:
344
  logger.error(f"Translation error: {e}")
345
+ return f"Translation error: {str(e)}", time.time() - start_time, 0
346
+
347
+ def _combine_translations(self, translated_chunks: List[str], original_text: str) -> str:
348
+ """ترکیب قسمت‌های ترجمه شده به یک متن یکپارچه"""
349
+ if not translated_chunks:
350
+ return ""
351
+
352
+ if len(translated_chunks) == 1:
353
+ return translated_chunks[0]
354
+
355
+ # ترکیب قسمت‌ها با در نظر گیری ساختار اصلی متن
356
+ combined = []
357
+
358
+ for i, chunk in enumerate(translated_chunks):
359
+ # پاک‌سازی قسمت
360
+ chunk = chunk.strip()
361
+
362
+ if not chunk:
363
+ continue
364
+
365
+ # اضافه کردن فاصله مناسب بین قسمت‌ها
366
+ if i > 0 and combined:
367
+ # اگر قسمت قبلی با نقطه تمام نمی‌شود، نقطه اضافه کن
368
+ if not combined[-1].rstrip().endswith(('.', '!', '?', ':', '؛', '.')):
369
+ combined[-1] += '.'
370
+
371
+ # بررسی اینکه آیا نیاز به پاراگراف جدید داریم
372
+ if '\n\n' in original_text:
373
+ combined.append('\n\n' + chunk)
374
+ else:
375
+ combined.append(' ' + chunk)
376
+ else:
377
+ combined.append(chunk)
378
+
379
+ result = ''.join(combined)
380
+
381
+ # پاک‌سازی نهایی
382
+ result = re.sub(r'\s+', ' ', result) # حذف فاصله‌های اضافی
383
+ result = re.sub(r'\.+', '.', result) # حذف نقطه‌های تکراری
384
+ result = result.strip()
385
+
386
+ return result
387
 
388
  # Language mappings for M2M100 model
389
  LANGUAGE_MAP = {
 
460
  translator = MultilingualTranslator(60)
461
 
462
  # Create FastAPI app
463
+ app = FastAPI(title="Multilingual Translation API", version="2.0.0")
464
 
465
  # Add CORS middleware
466
  app.add_middleware(
 
473
 
474
  @app.get("/")
475
  async def root():
476
+ return {"message": "Multilingual Translation API v2.0", "status": "active", "features": ["long_text_support", "smart_chunking", "cache_optimization"]}
477
 
478
  @app.post("/api/translate")
479
  async def api_translate(request: TranslationRequest):
480
+ """API endpoint for translation with long text support"""
481
  if not request.text.strip():
482
  raise HTTPException(status_code=400, detail="No text provided")
483
 
 
488
  raise HTTPException(status_code=400, detail="Invalid language codes")
489
 
490
  try:
491
+ translation, processing_time, chunks_count = translator.translate_text(request.text, source_code, target_code)
492
 
493
  return TranslationResponse(
494
  translation=translation,
 
496
  target_language=request.target_lang,
497
  processing_time=processing_time,
498
  character_count=len(request.text),
499
+ status="success",
500
+ chunks_processed=chunks_count
501
  )
502
  except Exception as e:
503
  raise HTTPException(status_code=500, detail=f"Translation error: {str(e)}")
 
505
  # Alternative endpoint for form data (compatibility with WordPress)
506
  @app.post("/api/translate/form")
507
  async def api_translate_form(request: Request):
508
+ """Alternative endpoint that accepts form data with long text support"""
509
  try:
510
  form_data = await request.form()
511
  text = form_data.get("text", "")
 
533
  raise HTTPException(status_code=400, detail="Invalid language codes")
534
 
535
  try:
536
+ translation, processing_time, chunks_count = translator.translate_text(text, source_code, target_code)
537
 
538
  return {
539
  "translation": translation,
 
541
  "target_language": target_lang,
542
  "processing_time": processing_time,
543
  "character_count": len(text),
544
+ "status": "success",
545
+ "chunks_processed": chunks_count
546
  }
547
  except Exception as e:
548
  raise HTTPException(status_code=500, detail=f"Translation error: {str(e)}")
 
563
  "status": "healthy",
564
  "device": str(translator.device),
565
  "model": translator.model_name,
566
+ "cache_size": len(translator.cache.cache),
567
+ "max_chunk_size": translator.max_chunk_size,
568
+ "version": "2.0.0"
569
  }
570
 
571
  if __name__ == "__main__":