Adjoumani commited on
Commit
c6312a3
·
verified ·
1 Parent(s): 8992d48

Create translator_engine.py

Browse files
Files changed (1) hide show
  1. translator_engine.py +381 -0
translator_engine.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 4. translator_engine.py
2
+ """
3
+ Moteur de traduction multi-engines avec fallback intelligent
4
+ """
5
+ import time
6
+ import logging
7
+ from typing import List, Optional, Dict, Any
8
+ from abc import ABC, abstractmethod
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
+ import streamlit as st
11
+
12
+ # Import des différents moteurs
13
+ try:
14
+ import translators as ts
15
+ except ImportError:
16
+ ts = None
17
+
18
+ try:
19
+ from googletrans import Translator as GoogleTranslator
20
+ except ImportError:
21
+ GoogleTranslator = None
22
+
23
+ try:
24
+ import deepl
25
+ except ImportError:
26
+ deepl = None
27
+
28
+ try:
29
+ import openai
30
+ except ImportError:
31
+ openai = None
32
+
33
+ try:
34
+ import anthropic
35
+ except ImportError:
36
+ anthropic = None
37
+
38
+ from utils import RateLimiter, TranslationCache
39
+
40
+ class TranslationEngine(ABC):
41
+ """Classe abstraite pour les moteurs de traduction"""
42
+
43
+ def __init__(self, name: str):
44
+ self.name = name
45
+ self.logger = logging.getLogger(f"Engine.{name}")
46
+ self.rate_limiter = RateLimiter()
47
+ self.is_available = self.check_availability()
48
+
49
+ @abstractmethod
50
+ def check_availability(self) -> bool:
51
+ """Vérifie si le moteur est disponible"""
52
+ pass
53
+
54
+ @abstractmethod
55
+ def translate(self, text: str, source_lang: str, target_lang: str) -> Optional[str]:
56
+ """Traduit le texte"""
57
+ pass
58
+
59
+ def translate_with_retry(self, text: str, source_lang: str, target_lang: str, max_retries: int = 3) -> Optional[str]:
60
+ """Traduit avec retry automatique"""
61
+ for attempt in range(max_retries):
62
+ try:
63
+ self.rate_limiter.wait()
64
+ result = self.translate(text, source_lang, target_lang)
65
+ if result:
66
+ self.rate_limiter.reset_errors()
67
+ return result
68
+ except Exception as e:
69
+ self.logger.warning(f"Tentative {attempt + 1}/{max_retries} échouée: {e}")
70
+ self.rate_limiter.register_error()
71
+ if attempt < max_retries - 1:
72
+ time.sleep(2 ** attempt) # Backoff exponentiel
73
+ return None
74
+
75
+ class TranslatorsEngine(TranslationEngine):
76
+ """Moteur utilisant la bibliothèque translators"""
77
+
78
+ def __init__(self, provider: str = 'google'):
79
+ self.provider = provider
80
+ super().__init__(f"Translators-{provider}")
81
+
82
+ def check_availability(self) -> bool:
83
+ return ts is not None
84
+
85
+ def translate(self, text: str, source_lang: str, target_lang: str) -> Optional[str]:
86
+ if not self.is_available:
87
+ return None
88
+
89
+ try:
90
+ # Conversion des codes de langue si nécessaire
91
+ if source_lang == 'auto':
92
+ source_lang = 'auto'
93
+
94
+ result = ts.translate_text(
95
+ text,
96
+ translator=self.provider,
97
+ from_language=source_lang,
98
+ to_language=target_lang,
99
+ timeout=30
100
+ )
101
+ return result
102
+ except Exception as e:
103
+ self.logger.error(f"Erreur traduction {self.provider}: {e}")
104
+ return None
105
+
106
+ class GoogleTransEngine(TranslationEngine):
107
+ """Moteur Google Translate (googletrans)"""
108
+
109
+ def __init__(self):
110
+ super().__init__("GoogleTrans")
111
+ self.translator = GoogleTranslator() if GoogleTranslator else None
112
+
113
+ def check_availability(self) -> bool:
114
+ return GoogleTranslator is not None
115
+
116
+ def translate(self, text: str, source_lang: str, target_lang: str) -> Optional[str]:
117
+ if not self.is_available:
118
+ return None
119
+
120
+ try:
121
+ result = self.translator.translate(
122
+ text,
123
+ src=source_lang if source_lang != 'auto' else 'auto',
124
+ dest=target_lang
125
+ )
126
+ return result.text
127
+ except Exception as e:
128
+ self.logger.error(f"Erreur GoogleTrans: {e}")
129
+ return None
130
+
131
+ class DeepLEngine(TranslationEngine):
132
+ """Moteur DeepL (nécessite une clé API)"""
133
+
134
+ def __init__(self, api_key: str = None):
135
+ super().__init__("DeepL")
136
+ self.api_key = api_key
137
+ self.translator = None
138
+ if api_key and deepl:
139
+ try:
140
+ self.translator = deepl.Translator(api_key)
141
+ except:
142
+ pass
143
+
144
+ def check_availability(self) -> bool:
145
+ return self.translator is not None
146
+
147
+ def translate(self, text: str, source_lang: str, target_lang: str) -> Optional[str]:
148
+ if not self.is_available:
149
+ return None
150
+
151
+ try:
152
+ # Conversion des codes de langue pour DeepL
153
+ target_lang_deepl = target_lang.upper()
154
+ if target_lang_deepl == 'EN':
155
+ target_lang_deepl = 'EN-US'
156
+
157
+ result = self.translator.translate_text(
158
+ text,
159
+ source_lang=None if source_lang == 'auto' else source_lang.upper(),
160
+ target_lang=target_lang_deepl
161
+ )
162
+ return result.text
163
+ except Exception as e:
164
+ self.logger.error(f"Erreur DeepL: {e}")
165
+ return None
166
+
167
+ class OpenAIEngine(TranslationEngine):
168
+ """Moteur OpenAI GPT (nécessite une clé API)"""
169
+
170
+ def __init__(self, api_key: str = None, model: str = "gpt-3.5-turbo"):
171
+ super().__init__("OpenAI")
172
+ self.api_key = api_key
173
+ self.model = model
174
+ if api_key and openai:
175
+ openai.api_key = api_key
176
+
177
+ def check_availability(self) -> bool:
178
+ return self.api_key is not None and openai is not None
179
+
180
+ def translate(self, text: str, source_lang: str, target_lang: str) -> Optional[str]:
181
+ if not self.is_available:
182
+ return None
183
+
184
+ try:
185
+ # Mapping des codes de langue vers les noms complets
186
+ lang_names = {
187
+ 'en': 'English', 'fr': 'French', 'es': 'Spanish',
188
+ 'de': 'German', 'it': 'Italian', 'pt': 'Portuguese',
189
+ 'ru': 'Russian', 'ja': 'Japanese', 'ko': 'Korean',
190
+ 'zh': 'Chinese', 'ar': 'Arabic', 'hi': 'Hindi'
191
+ }
192
+
193
+ target_name = lang_names.get(target_lang, target_lang)
194
+
195
+ prompt = f"Translate the following text to {target_name}. Only provide the translation, no explanations:\n\n{text}"
196
+
197
+ response = openai.ChatCompletion.create(
198
+ model=self.model,
199
+ messages=[
200
+ {"role": "system", "content": "You are a professional translator. Provide accurate translations while preserving the original meaning and tone."},
201
+ {"role": "user", "content": prompt}
202
+ ],
203
+ temperature=0.3,
204
+ max_tokens=len(text) * 2 # Estimation généreuse
205
+ )
206
+
207
+ return response.choices[0].message.content.strip()
208
+ except Exception as e:
209
+ self.logger.error(f"Erreur OpenAI: {e}")
210
+ return None
211
+
212
+ class AnthropicEngine(TranslationEngine):
213
+ """Moteur Anthropic Claude (nécessite une clé API)"""
214
+
215
+ def __init__(self, api_key: str = None):
216
+ super().__init__("Anthropic")
217
+ self.api_key = api_key
218
+ self.client = None
219
+ if api_key and anthropic:
220
+ try:
221
+ self.client = anthropic.Anthropic(api_key=api_key)
222
+ except:
223
+ pass
224
+
225
+ def check_availability(self) -> bool:
226
+ return self.client is not None
227
+
228
+ def translate(self, text: str, source_lang: str, target_lang: str) -> Optional[str]:
229
+ if not self.is_available:
230
+ return None
231
+
232
+ try:
233
+ # Mapping des codes de langue
234
+ lang_names = {
235
+ 'en': 'English', 'fr': 'French', 'es': 'Spanish',
236
+ 'de': 'German', 'it': 'Italian', 'pt': 'Portuguese',
237
+ 'ru': 'Russian', 'ja': 'Japanese', 'ko': 'Korean',
238
+ 'zh': 'Chinese', 'ar': 'Arabic', 'hi': 'Hindi'
239
+ }
240
+
241
+ target_name = lang_names.get(target_lang, target_lang)
242
+
243
+ message = self.client.messages.create(
244
+ model="claude-3-sonnet-20240229",
245
+ max_tokens=len(text) * 2,
246
+ temperature=0.3,
247
+ messages=[
248
+ {
249
+ "role": "user",
250
+ "content": f"Translate this text to {target_name}. Provide only the translation:\n\n{text}"
251
+ }
252
+ ]
253
+ )
254
+
255
+ return message.content[0].text
256
+ except Exception as e:
257
+ self.logger.error(f"Erreur Anthropic: {e}")
258
+ return None
259
+
260
+ class MultiEngineTranslator:
261
+ """Gestionnaire principal avec fallback entre moteurs"""
262
+
263
+ def __init__(self, config: Dict[str, Any] = None):
264
+ self.config = config or {}
265
+ self.cache = TranslationCache()
266
+ self.logger = logging.getLogger("MultiEngineTranslator")
267
+ self.engines = []
268
+ self._initialize_engines()
269
+
270
+ def _initialize_engines(self):
271
+ """Initialise tous les moteurs disponibles"""
272
+
273
+ # Moteurs gratuits (translators)
274
+ for provider in ['google', 'bing', 'yandex', 'baidu']:
275
+ engine = TranslatorsEngine(provider)
276
+ if engine.is_available:
277
+ self.engines.append(engine)
278
+
279
+ # Google Translate alternatif
280
+ google_engine = GoogleTransEngine()
281
+ if google_engine.is_available:
282
+ self.engines.append(google_engine)
283
+
284
+ # Moteurs avec API (si les clés sont fournies)
285
+ if self.config.get('deepl_api_key'):
286
+ deepl_engine = DeepLEngine(self.config['deepl_api_key'])
287
+ if deepl_engine.is_available:
288
+ self.engines.insert(0, deepl_engine) # Priorité haute
289
+
290
+ if self.config.get('openai_api_key'):
291
+ openai_engine = OpenAIEngine(
292
+ self.config['openai_api_key'],
293
+ self.config.get('openai_model', 'gpt-3.5-turbo')
294
+ )
295
+ if openai_engine.is_available:
296
+ self.engines.insert(0, openai_engine)
297
+
298
+ if self.config.get('anthropic_api_key'):
299
+ anthropic_engine = AnthropicEngine(self.config['anthropic_api_key'])
300
+ if anthropic_engine.is_available:
301
+ self.engines.insert(0, anthropic_engine)
302
+
303
+ self.logger.info(f"Moteurs disponibles: {[e.name for e in self.engines]}")
304
+
305
+ def translate(self, text: str, source_lang: str = 'auto', target_lang: str = 'fr') -> str:
306
+ """
307
+ Traduit le texte avec fallback automatique entre moteurs
308
+ """
309
+ if not text or not text.strip():
310
+ return text
311
+
312
+ # Vérifier le cache pour chaque moteur
313
+ for engine in self.engines:
314
+ cached = self.cache.get(text, source_lang, target_lang, engine.name)
315
+ if cached:
316
+ self.logger.debug(f"Traduction trouvée en cache ({engine.name})")
317
+ return cached
318
+
319
+ # Essayer chaque moteur dans l'ordre
320
+ for engine in self.engines:
321
+ self.logger.info(f"Tentative avec {engine.name}")
322
+ try:
323
+ result = engine.translate_with_retry(text, source_lang, target_lang)
324
+ if result:
325
+ # Sauvegarder en cache
326
+ self.cache.set(text, result, source_lang, target_lang, engine.name)
327
+ return result
328
+ except Exception as e:
329
+ self.logger.warning(f"Échec {engine.name}: {e}")
330
+ continue
331
+
332
+ # Si tous les moteurs échouent, retourner le texte original
333
+ self.logger.error("Tous les moteurs ont échoué, retour du texte original")
334
+ return text
335
+
336
+ def translate_batch(self, texts: List[str], source_lang: str = 'auto',
337
+ target_lang: str = 'fr', max_workers: int = 3) -> List[str]:
338
+ """
339
+ Traduit plusieurs textes en parallèle
340
+ """
341
+ results = [None] * len(texts)
342
+
343
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
344
+ futures = {
345
+ executor.submit(self.translate, text, source_lang, target_lang): i
346
+ for i, text in enumerate(texts)
347
+ }
348
+
349
+ for future in as_completed(futures):
350
+ index = futures[future]
351
+ try:
352
+ results[index] = future.result()
353
+ except Exception as e:
354
+ self.logger.error(f"Erreur traduction batch index {index}: {e}")
355
+ results[index] = texts[index]
356
+
357
+ return results
358
+
359
+ def get_available_engines(self) -> List[str]:
360
+ """Retourne la liste des moteurs disponibles"""
361
+ return [engine.name for engine in self.engines]
362
+
363
+ def estimate_cost(self, char_count: int) -> Dict[str, float]:
364
+ """Estime le coût de traduction pour les APIs payantes"""
365
+ costs = {}
366
+
367
+ # DeepL: ~20€ pour 1M caractères
368
+ if any(e.name == 'DeepL' for e in self.engines):
369
+ costs['DeepL'] = (char_count / 1_000_000) * 20
370
+
371
+ # OpenAI GPT-3.5: ~$0.002 per 1K tokens (environ 4 caractères par token)
372
+ if any(e.name == 'OpenAI' for e in self.engines):
373
+ token_count = char_count / 4
374
+ costs['OpenAI'] = (token_count / 1000) * 0.002
375
+
376
+ # Anthropic Claude: ~$0.003 per 1K tokens
377
+ if any(e.name == 'Anthropic' for e in self.engines):
378
+ token_count = char_count / 4
379
+ costs['Anthropic'] = (token_count / 1000) * 0.003
380
+
381
+ return costs