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

Create utils.py

Browse files
Files changed (1) hide show
  1. utils.py +291 -0
utils.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 3. utils.py
2
+ """
3
+ Utilitaires pour l'application de traduction
4
+ """
5
+ import re
6
+ import json
7
+ import time
8
+ import random
9
+ import hashlib
10
+ import logging
11
+ from typing import List, Dict, Any, Optional
12
+ from pathlib import Path
13
+ from datetime import datetime
14
+
15
+ class TranslationCache:
16
+ """Gestion du cache de traduction avec persistance"""
17
+
18
+ def __init__(self, cache_dir: str = ".translation_cache"):
19
+ self.cache_dir = Path(cache_dir)
20
+ self.cache_dir.mkdir(exist_ok=True)
21
+ self.memory_cache = {}
22
+ self.cache_file = self.cache_dir / "translations.json"
23
+ self.load_cache()
24
+
25
+ def load_cache(self):
26
+ """Charge le cache depuis le disque"""
27
+ if self.cache_file.exists():
28
+ try:
29
+ with open(self.cache_file, 'r', encoding='utf-8') as f:
30
+ self.memory_cache = json.load(f)
31
+ except Exception as e:
32
+ logging.warning(f"Erreur chargement cache: {e}")
33
+ self.memory_cache = {}
34
+
35
+ def save_cache(self):
36
+ """Sauvegarde le cache sur disque"""
37
+ try:
38
+ with open(self.cache_file, 'w', encoding='utf-8') as f:
39
+ json.dump(self.memory_cache, f, ensure_ascii=False, indent=2)
40
+ except Exception as e:
41
+ logging.error(f"Erreur sauvegarde cache: {e}")
42
+
43
+ def get_cache_key(self, text: str, source_lang: str, target_lang: str, engine: str) -> str:
44
+ """Génère une clé unique pour le cache"""
45
+ content = f"{text}_{source_lang}_{target_lang}_{engine}"
46
+ return hashlib.md5(content.encode()).hexdigest()
47
+
48
+ def get(self, text: str, source_lang: str, target_lang: str, engine: str) -> Optional[str]:
49
+ """Récupère une traduction du cache"""
50
+ key = self.get_cache_key(text, source_lang, target_lang, engine)
51
+ return self.memory_cache.get(key)
52
+
53
+ def set(self, text: str, translation: str, source_lang: str, target_lang: str, engine: str):
54
+ """Ajoute une traduction au cache"""
55
+ key = self.get_cache_key(text, source_lang, target_lang, engine)
56
+ self.memory_cache[key] = translation
57
+ self.save_cache()
58
+
59
+ class TextChunker:
60
+ """Découpe intelligente du texte en chunks"""
61
+
62
+ @staticmethod
63
+ def split_text(text: str, max_chars: int = 3000, preserve_formatting: bool = True) -> List[str]:
64
+ """
65
+ Découpe le texte en chunks intelligents
66
+ - Respecte les paragraphes
67
+ - Préserve la ponctuation
68
+ - Optimise pour la traduction
69
+ """
70
+ if not text or not text.strip():
71
+ return []
72
+
73
+ # Nettoyer le texte
74
+ text = text.strip()
75
+
76
+ # Si le texte est court, retourner tel quel
77
+ if len(text) <= max_chars:
78
+ return [text]
79
+
80
+ chunks = []
81
+
82
+ # Séparer par paragraphes (double saut de ligne)
83
+ paragraphs = re.split(r'\n\n+', text)
84
+
85
+ current_chunk = ""
86
+ for para in paragraphs:
87
+ # Si le paragraphe seul est trop long
88
+ if len(para) > max_chars:
89
+ # Sauvegarder le chunk en cours
90
+ if current_chunk:
91
+ chunks.append(current_chunk.strip())
92
+ current_chunk = ""
93
+
94
+ # Découper le paragraphe par phrases
95
+ sentences = re.split(r'(?<=[.!?])\s+', para)
96
+ for sentence in sentences:
97
+ if len(sentence) > max_chars:
98
+ # Découper par mots si la phrase est trop longue
99
+ words = sentence.split()
100
+ temp_chunk = ""
101
+ for word in words:
102
+ if len(temp_chunk) + len(word) + 1 <= max_chars:
103
+ temp_chunk = f"{temp_chunk} {word}".strip()
104
+ else:
105
+ if temp_chunk:
106
+ chunks.append(temp_chunk)
107
+ temp_chunk = word
108
+ if temp_chunk:
109
+ chunks.append(temp_chunk)
110
+ elif len(current_chunk) + len(sentence) + 1 <= max_chars:
111
+ current_chunk = f"{current_chunk} {sentence}".strip()
112
+ else:
113
+ if current_chunk:
114
+ chunks.append(current_chunk)
115
+ current_chunk = sentence
116
+ # Si ajouter le paragraphe ne dépasse pas la limite
117
+ elif len(current_chunk) + len(para) + 2 <= max_chars:
118
+ if current_chunk:
119
+ current_chunk = f"{current_chunk}\n\n{para}"
120
+ else:
121
+ current_chunk = para
122
+ else:
123
+ # Sauvegarder le chunk actuel et commencer un nouveau
124
+ if current_chunk:
125
+ chunks.append(current_chunk.strip())
126
+ current_chunk = para
127
+
128
+ # Ajouter le dernier chunk
129
+ if current_chunk:
130
+ chunks.append(current_chunk.strip())
131
+
132
+ return chunks
133
+
134
+ class ProgressTracker:
135
+ """Suivi de progression avec sauvegarde"""
136
+
137
+ def __init__(self, total_items: int, task_id: str):
138
+ self.total_items = total_items
139
+ self.task_id = task_id
140
+ self.completed_items = 0
141
+ self.start_time = datetime.now()
142
+ self.checkpoint_file = Path(f".checkpoints/{task_id}.json")
143
+ self.checkpoint_file.parent.mkdir(exist_ok=True)
144
+ self.load_checkpoint()
145
+
146
+ def load_checkpoint(self):
147
+ """Charge le point de sauvegarde"""
148
+ if self.checkpoint_file.exists():
149
+ try:
150
+ with open(self.checkpoint_file, 'r') as f:
151
+ data = json.load(f)
152
+ self.completed_items = data.get('completed', 0)
153
+ except:
154
+ pass
155
+
156
+ def update(self, increment: int = 1):
157
+ """Met à jour la progression"""
158
+ self.completed_items += increment
159
+ self.save_checkpoint()
160
+
161
+ def save_checkpoint(self):
162
+ """Sauvegarde la progression"""
163
+ data = {
164
+ 'task_id': self.task_id,
165
+ 'total': self.total_items,
166
+ 'completed': self.completed_items,
167
+ 'timestamp': datetime.now().isoformat()
168
+ }
169
+ with open(self.checkpoint_file, 'w') as f:
170
+ json.dump(data, f)
171
+
172
+ def get_progress(self) -> float:
173
+ """Retourne le pourcentage de progression"""
174
+ if self.total_items == 0:
175
+ return 100.0
176
+ return (self.completed_items / self.total_items) * 100
177
+
178
+ def get_eta(self) -> str:
179
+ """Estime le temps restant"""
180
+ if self.completed_items == 0:
181
+ return "Calcul en cours..."
182
+
183
+ elapsed = (datetime.now() - self.start_time).total_seconds()
184
+ rate = self.completed_items / elapsed
185
+ remaining = self.total_items - self.completed_items
186
+ eta_seconds = remaining / rate if rate > 0 else 0
187
+
188
+ hours = int(eta_seconds // 3600)
189
+ minutes = int((eta_seconds % 3600) // 60)
190
+
191
+ if hours > 0:
192
+ return f"{hours}h {minutes}min"
193
+ return f"{minutes}min"
194
+
195
+ class RateLimiter:
196
+ """Gestion des limites de taux d'API"""
197
+
198
+ def __init__(self, min_delay: float = 0.5, max_delay: float = 2.0):
199
+ self.min_delay = min_delay
200
+ self.max_delay = max_delay
201
+ self.last_request_time = 0
202
+ self.request_count = 0
203
+ self.error_count = 0
204
+
205
+ def wait(self):
206
+ """Attend avant la prochaine requête"""
207
+ # Calcul du délai adaptatif
208
+ if self.error_count > 0:
209
+ # Augmenter le délai en cas d'erreurs
210
+ delay = min(self.max_delay * (1.5 ** self.error_count), 10.0)
211
+ else:
212
+ # Délai aléatoire normal
213
+ delay = random.uniform(self.min_delay, self.max_delay)
214
+
215
+ # Attendre si nécessaire
216
+ elapsed = time.time() - self.last_request_time
217
+ if elapsed < delay:
218
+ time.sleep(delay - elapsed)
219
+
220
+ self.last_request_time = time.time()
221
+ self.request_count += 1
222
+
223
+ def register_error(self):
224
+ """Enregistre une erreur"""
225
+ self.error_count += 1
226
+
227
+ def reset_errors(self):
228
+ """Réinitialise le compteur d'erreurs"""
229
+ self.error_count = 0
230
+
231
+ def setup_logger(name: str, log_file: str = None) -> logging.Logger:
232
+ """Configure un logger personnalisé"""
233
+ logger = logging.getLogger(name)
234
+ logger.setLevel(logging.DEBUG)
235
+
236
+ # Format détaillé
237
+ formatter = logging.Formatter(
238
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
239
+ datefmt='%Y-%m-%d %H:%M:%S'
240
+ )
241
+
242
+ # Handler console
243
+ console_handler = logging.StreamHandler()
244
+ console_handler.setLevel(logging.INFO)
245
+ console_handler.setFormatter(formatter)
246
+ logger.addHandler(console_handler)
247
+
248
+ # Handler fichier si spécifié
249
+ if log_file:
250
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
251
+ file_handler.setLevel(logging.DEBUG)
252
+ file_handler.setFormatter(formatter)
253
+ logger.addHandler(file_handler)
254
+
255
+ return logger
256
+
257
+ def sanitize_filename(filename: str) -> str:
258
+ """Nettoie un nom de fichier"""
259
+ # Remplacer les caractères interdits
260
+ invalid_chars = '<>:"/\\|?*'
261
+ for char in invalid_chars:
262
+ filename = filename.replace(char, '_')
263
+
264
+ # Limiter la longueur
265
+ name, ext = filename.rsplit('.', 1) if '.' in filename else (filename, '')
266
+ if len(name) > 200:
267
+ name = name[:200]
268
+
269
+ return f"{name}.{ext}" if ext else name
270
+
271
+ def format_file_size(size_bytes: int) -> str:
272
+ """Formate une taille de fichier"""
273
+ for unit in ['B', 'KB', 'MB', 'GB']:
274
+ if size_bytes < 1024.0:
275
+ return f"{size_bytes:.1f} {unit}"
276
+ size_bytes /= 1024.0
277
+ return f"{size_bytes:.1f} TB"
278
+
279
+ def estimate_translation_time(char_count: int, chars_per_second: float = 50) -> str:
280
+ """Estime le temps de traduction"""
281
+ seconds = char_count / chars_per_second
282
+
283
+ if seconds < 60:
284
+ return f"{int(seconds)} secondes"
285
+ elif seconds < 3600:
286
+ minutes = int(seconds / 60)
287
+ return f"{minutes} minutes"
288
+ else:
289
+ hours = int(seconds / 3600)
290
+ minutes = int((seconds % 3600) / 60)
291
+ return f"{hours}h {minutes}min"