-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmarian_mt_translator.py
More file actions
1386 lines (1166 loc) · 70.8 KB
/
Copy pathmarian_mt_translator.py
File metadata and controls
1386 lines (1166 loc) · 70.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import threading
import time
import gc
import re
import os
import traceback
import shutil
import subprocess
import hashlib
import zipfile
import urllib.request
from pathlib import Path
from logger import log_debug
from resource_handler import get_resource_path
# --- Configuration ---
ENABLE_PYTORCH_THREAD_LIMITING = False # Set to False to disable PyTorch thread limiting
# --- For MarianMT Translation with GPU Support ---
try:
log_debug("MarianMT: Attempting to import transformers...")
from transformers import MarianMTModel, MarianTokenizer
log_debug("MarianMT: transformers import successful")
log_debug("MarianMT: Attempting to import torch...")
import torch
log_debug("MarianMT: torch import successful")
# Detailed GPU and CUDA Library Detection
# Check if PyTorch was compiled with CUDA support (works for both compiled and Python modes)
CUDA_LIBRARIES_AVAILABLE = (hasattr(torch, 'version') and
hasattr(torch.version, 'cuda') and
torch.version.cuda is not None)
if CUDA_LIBRARIES_AVAILABLE:
GPU_HARDWARE_AVAILABLE = torch.cuda.is_available()
if GPU_HARDWARE_AVAILABLE:
# Scenario 4: GPU-ready app on PC with GPU - USE GPU
GPU_AVAILABLE = True
GPU_DEVICE = torch.device("cuda:0")
try:
GPU_NAME = torch.cuda.get_device_name(0)
GPU_MEMORY = torch.cuda.get_device_properties(0).total_memory / 1024**3
cuda_version = torch.version.cuda if torch.version.cuda else "Unknown"
log_debug(f"GPU: found, GPU libraries: found (CUDA {cuda_version}), MarianMT running on GPU.")
log_debug(f"GPU detected: {GPU_NAME} ({GPU_MEMORY:.1f} GB VRAM)")
except:
# Fallback if GPU detection fails
GPU_NAME = "Unknown GPU"
GPU_MEMORY = 0
cuda_version = torch.version.cuda if torch.version.cuda else "Unknown"
log_debug(f"GPU: found, GPU libraries: found (CUDA {cuda_version}), MarianMT running on GPU.")
log_debug("GPU detected but details unavailable")
else:
# Scenario 3: GPU-ready app on CPU-only PC - USE CPU
GPU_AVAILABLE = False
GPU_DEVICE = torch.device("cpu")
GPU_NAME = None
GPU_MEMORY = 0
cuda_version = torch.version.cuda if torch.version.cuda else "Unknown"
log_debug(f"GPU: not found, GPU libraries: found (CUDA {cuda_version}), MarianMT running on CPU.")
else:
# Scenarios 1 & 2: CPU-only app (no CUDA libraries) - USE CPU
GPU_AVAILABLE = False
GPU_DEVICE = torch.device("cpu")
GPU_NAME = None
GPU_MEMORY = 0
# Try to detect if GPU hardware exists even without CUDA libraries
gpu_hardware_detected = False
gpu_name_detected = "Unknown"
try:
# Try alternative GPU detection methods for logging purposes only
import subprocess
result = subprocess.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader,nounits'],
capture_output=True, text=True, timeout=3)
if result.returncode == 0 and result.stdout.strip():
gpu_hardware_detected = True
gpu_name_detected = result.stdout.strip().split('\n')[0]
except:
pass
if gpu_hardware_detected:
# Scenario 2: CPU-only app on PC with GPU
log_debug(f"GPU: found ({gpu_name_detected}), GPU libraries: not found, MarianMT running on CPU.")
else:
# Scenario 1: CPU-only app on CPU-only PC
log_debug("GPU: not found, GPU libraries: not found, MarianMT running on CPU.")
MARIANMT_AVAILABLE = True
log_debug("MarianMT: All imports successful, MARIANMT_AVAILABLE = True")
except ImportError as import_error:
MARIANMT_AVAILABLE = False
torch = None
GPU_AVAILABLE = False
GPU_DEVICE = None
GPU_NAME = None
GPU_MEMORY = 0
log_debug(f"MarianMT: Import failed with ImportError: {import_error}")
log_debug("GPU: unknown, GPU libraries: not found, MarianMT not available (transformers/torch not installed).")
except Exception as general_error:
MARIANMT_AVAILABLE = False
torch = None
GPU_AVAILABLE = False
GPU_DEVICE = None
GPU_NAME = None
GPU_MEMORY = 0
log_debug(f"MarianMT: Import failed with unexpected error: {general_error}")
log_debug("GPU: unknown, GPU libraries: not found, MarianMT not available (unexpected import error).")
class MarianMTTranslator:
"""GPU-accelerated MarianMT translator with automatic CPU fallback."""
torch = torch # Class attribute to make torch accessible if imported
def __init__(self, cache_dir=None, num_beams=2):
"""
Initialize the translator with GPU/CPU device detection.
Args:
cache_dir: Directory to store downloaded models. If None, uses default Hugging Face cache.
num_beams: Beam search value for translation quality (1-8)
"""
# Verify imports are available
if not MARIANMT_AVAILABLE:
raise ImportError("MarianMT modules not available. Make sure transformers is installed.")
# GPU/CPU Device Configuration - PERMANENT DECISION
self.device = GPU_DEVICE if GPU_AVAILABLE else torch.device("cpu")
self.gpu_enabled = GPU_AVAILABLE
self.gpu_name = GPU_NAME
self.gpu_memory = GPU_MEMORY
# Store permanent device configuration (never changes after initialization)
self.permanent_device = self.device
self.permanent_gpu_enabled = self.gpu_enabled
# Smart fallback system - allows temporary CPU fallback for GPU OOM
self.current_device = self.permanent_device # Current active device (can temporarily change)
self.temporary_cpu_fallback = False # Flag to track if we're in temporary CPU mode
# Flag to track when cache has been cleared and model needs reloading
self.cache_cleared_flag = False
# Log device configuration with explicit scenario information
if self.gpu_enabled:
cuda_version = torch.version.cuda if (torch and hasattr(torch, 'version') and torch.version.cuda) else "Unknown"
log_debug(f"MarianMT initialized with GPU acceleration (PERMANENT)")
log_debug(f"GPU: {self.gpu_name} ({self.gpu_memory:.1f} GB VRAM)")
log_debug(f"Scenario 4: GPU-ready application on PC with GPU CUDA support (CUDA {cuda_version})")
else:
log_debug("MarianMT initialized with CPU processing (PERMANENT)")
# Determine which of the first 3 scenarios we're in using improved detection
if (torch and hasattr(torch, 'version') and
hasattr(torch.version, 'cuda') and
torch.version.cuda is not None):
cuda_version = torch.version.cuda
log_debug(f"Scenario 3: GPU-ready application on CPU-only PC (CUDA {cuda_version} available but no GPU)")
else:
# Check if GPU hardware exists for more specific logging
gpu_hardware_detected = False
gpu_name = "Unknown"
try:
import subprocess
result = subprocess.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader,nounits'],
capture_output=True, text=True, timeout=3)
if result.returncode == 0 and result.stdout.strip():
gpu_hardware_detected = True
gpu_name = result.stdout.strip().split('\n')[0]
log_debug(f"Scenario 2: CPU-only application on PC with GPU CUDA support (GPU: {gpu_name})")
else:
log_debug("Scenario 1: CPU-only application on CPU-only PC")
except:
log_debug("Scenario 1: CPU-only application on CPU-only PC")
# Add a thread lock for model operations
self.model_lock = threading.RLock()
self.active_model_key = None
self.active_tokenizer = None
self.active_model = None
self.active_pivot = None
self.cache_dir = cache_dir
self.num_beams = num_beams # Store beam search value (1-8)
# Log CPU thread configuration
if torch: # Check if torch was imported successfully
cpu_count = os.cpu_count() or 2
if ENABLE_PYTORCH_THREAD_LIMITING and cpu_count > 2:
# Limit PyTorch to half of available CPU cores for better system performance
max_torch_threads = max(1, cpu_count // 2)
torch.set_num_threads(max_torch_threads)
log_debug(f"MarianMT limited PyTorch to {max_torch_threads} threads (out of {cpu_count} CPU cores)")
else:
if not ENABLE_PYTORCH_THREAD_LIMITING:
log_debug(f"MarianMT PyTorch thread limiting DISABLED - using default {torch.get_num_threads()} threads")
else:
log_debug(f"MarianMT initialized with {torch.get_num_threads()} CPU threads (no limiting on {cpu_count} cores)")
else:
log_debug("MarianMT initialized, but PyTorch not available for thread count.")
# Define supported language pairs and their model names
self.supported_langs = set() # Will be populated dynamically
self.direct_pairs = {
# Note: No ('en', 'pl') entry here as it uses special handling
('pl', 'en'): 'Helsinki-NLP/opus-mt-pl-en',
('en', 'de'): 'Helsinki-NLP/opus-mt-en-de',
('de', 'en'): 'Helsinki-NLP/opus-mt-de-en',
('en', 'fr'): 'Helsinki-NLP/opus-mt-en-fr',
('fr', 'en'): 'Helsinki-NLP/opus-mt-fr-en',
('fr', 'de'): 'Helsinki-NLP/opus-mt-fr-de',
('de', 'fr'): 'Helsinki-NLP/opus-mt-de-fr',
('pl', 'de'): 'Helsinki-NLP/opus-mt-pl-de',
('de', 'pl'): 'Helsinki-NLP/opus-mt-de-pl',
('pl', 'fr'): 'Helsinki-NLP/opus-mt-pl-fr',
('fr', 'pl'): 'Helsinki-NLP/opus-mt-fr-pl'
}
# Populate supported languages set
for source, target in self.direct_pairs.keys():
self.supported_langs.add(source)
self.supported_langs.add(target)
# Add special support for English to Polish (handled separately)
self.supported_langs.add('en')
self.supported_langs.add('pl')
# Define model name to language pair mapping (for explicitly adding models)
# This helps when we need to dynamically select specific models
self.model_to_langs = {}
for (source, target), model in self.direct_pairs.items():
self.model_to_langs[model] = (source, target)
def notify_cache_cleared(self):
"""
Notify the translator that the translation cache has been cleared.
This will force the model to be reloaded on next translation to ensure proper state.
"""
with self.model_lock:
self.cache_cleared_flag = True
device_type = "GPU" if self.permanent_gpu_enabled else "CPU"
log_debug(f"MarianMT notified of cache clearing - will force model reload on {device_type}")
def _translate_batch_sentences(self, sentences, source_lang, target_lang):
"""
Translate multiple sentences in a single batch operation.
Args:
sentences: List of sentences to translate
source_lang: Source language code
target_lang: Target language code
Returns:
List of translated sentences
"""
try:
batch_start_time = time.monotonic()
# Filter out empty sentences but keep track of indices
non_empty_sentences = []
sentence_indices = []
for i, sentence in enumerate(sentences):
clean_sentence = sentence.strip()
if clean_sentence:
non_empty_sentences.append(clean_sentence)
sentence_indices.append(i)
if not non_empty_sentences:
return [""] * len(sentences)
log_debug(f"Batch translating {len(non_empty_sentences)} non-empty sentences from {len(sentences)} total")
# Use the enhanced _translate_text_cached method with batch input
model_key = (source_lang, target_lang)
translated_batch = self._translate_text_cached(non_empty_sentences, model_key, self.num_beams)
# Handle case where translation failed
if isinstance(translated_batch, str):
# Single string error message - apply to all sentences
log_debug(f"Batch translation failed with error: {translated_batch}")
return [translated_batch] * len(sentences)
# Reconstruct full results list including empty sentences
results = [""] * len(sentences)
for i, translated in enumerate(translated_batch):
original_index = sentence_indices[i]
results[original_index] = translated
batch_time = time.monotonic() - batch_start_time
current_device_name = "GPU" if (self.current_device.type == 'cuda') else "CPU"
fallback_info = " (temporary fallback)" if self.temporary_cpu_fallback else ""
log_debug(f"Batch translation complete on {current_device_name}{fallback_info}: {len(non_empty_sentences)} sentences in {batch_time:.3f} seconds")
return results
except Exception as e:
error_msg = f"Error in batch translation: {str(e)}"
log_debug(error_msg)
log_debug(traceback.format_exc())
# Return original sentences on error
return sentences
def _unload_current_model(self):
"""Unload the current model to free memory (GPU and CPU) while preserving device configuration."""
# This method is called from within _try_load_direct_model which already
# holds the lock, so we don't need to acquire it again here
device_type = "GPU" if self.permanent_gpu_enabled else "CPU"
fallback_status = " (temporary CPU fallback)" if self.temporary_cpu_fallback else ""
log_debug(f"Unloading current MarianMT model and tokenizer from {device_type}{fallback_status}.")
self.active_model_key = None
self.active_tokenizer = None
self.active_model = None
self.active_pivot = None
# Reset to permanent device configuration and clear fallback status
self.current_device = self.permanent_device
self.temporary_cpu_fallback = False
# Force garbage collection to release memory
gc.collect()
# Clear GPU cache if available and we're using GPU permanently
if torch and torch.cuda.is_available() and self.permanent_gpu_enabled:
torch.cuda.empty_cache()
log_debug("GPU cache cleared after model unload.")
else:
log_debug("CPU memory released after model unload.")
def _try_load_direct_model(self, source_lang, target_lang):
"""Try to load a direct translation model for the language pair with thread safety."""
model_key = (source_lang, target_lang)
device_type = "GPU" if self.permanent_gpu_enabled else "CPU"
log_debug(f"Attempting to load translation model for language pair: '{source_lang}' to '{target_lang}' on {device_type}")
# Use a lock to ensure thread safety
with self.model_lock:
# Check if cache was cleared - if so, force reload even if model seems loaded
if self.cache_cleared_flag:
log_debug(f"Cache was cleared - forcing model reload on {device_type}")
self._unload_current_model()
self.cache_cleared_flag = False # Reset the flag
# Check if this model pair is already loaded and cache wasn't cleared
elif self.active_model_key == model_key and self.active_model is not None:
log_debug(f"Model for '{source_lang}' to '{target_lang}' already loaded and active on {device_type}.")
return True
# Unload existing model to save memory if a different model is active or needs to be loaded
if self.active_model_key != model_key or self.active_model is None: # More precise condition for unload
log_debug(f"Unloading previous model (if any) before loading {source_lang}->{target_lang} on {device_type}.")
self._unload_current_model() # Unconditional unload logic moved into _unload_current_model
# Special handling for English to Polish model
if source_lang == 'en' and target_lang == 'pl':
special_model_path = self._ensure_special_en_pl_model()
if special_model_path:
model_name = special_model_path
log_debug(f"Using special English to Polish model from: {model_name}")
else:
log_debug("Special English to Polish model not available, no fallback exists")
return False
# Regular model loading logic for all other language pairs
elif model_key in self.direct_pairs:
model_name = self.direct_pairs[model_key]
log_debug(f"Found predefined model '{model_name}' for language pair '{source_lang}' to '{target_lang}'")
# Verify it's a Helsinki-NLP model
if not model_name.startswith('Helsinki-NLP/opus-mt'):
log_debug(f"Model '{model_name}' is not a Helsinki-NLP/opus-mt model, skipping")
return False
else:
# Try to dynamically construct a Helsinki-NLP model name
model_name = f"Helsinki-NLP/opus-mt-{source_lang}-{target_lang}"
log_debug(f"No predefined model, trying to dynamically load: '{model_name}'")
try:
log_debug(f"Attempting to download and load model: {model_name} on {device_type}")
start_time = time.time()
# Load tokenizer (always on CPU)
self.active_tokenizer = MarianTokenizer.from_pretrained(model_name, cache_dir=self.cache_dir)
# Load model with device-specific configuration
self.active_model = MarianMTModel.from_pretrained(
model_name,
cache_dir=self.cache_dir,
low_cpu_mem_usage=True, # More memory-efficient loading
torch_dtype=torch.float16 if self.permanent_gpu_enabled else torch.float32, # Use FP16 on GPU
)
# Move model to appropriate device with smart fallback
try:
# Always try permanent device first (GPU if configured)
if self.temporary_cpu_fallback and self.permanent_gpu_enabled:
# We were in CPU fallback mode, try to restore GPU
log_debug("Attempting to restore GPU after temporary CPU fallback")
self.current_device = self.permanent_device
self.temporary_cpu_fallback = False
self.active_model = self.active_model.to(self.current_device)
load_time = time.time() - start_time
device_name = "GPU" if (self.current_device.type == 'cuda') else "CPU"
log_debug(f"Model {model_name} loaded to {device_name} in {load_time:.2f} seconds")
if self.current_device.type == 'cuda':
# Log GPU memory usage
if torch.cuda.is_available():
memory_allocated = torch.cuda.memory_allocated(0) / 1024**3
log_debug(f"GPU memory allocated: {memory_allocated:.2f} GB")
# Reset fallback flag since GPU loading succeeded
if self.temporary_cpu_fallback:
self.temporary_cpu_fallback = False
log_debug("Successfully restored GPU operation after temporary fallback")
except (RuntimeError, torch.cuda.OutOfMemoryError) as e:
# Smart fallback: only fall back to CPU if we were configured for GPU
if self.permanent_gpu_enabled and not self.temporary_cpu_fallback:
log_debug(f"GPU loading failed ({e}), implementing temporary CPU fallback")
log_debug("This is a temporary fallback - will attempt GPU again on next model load")
# Set temporary CPU fallback mode
self.current_device = torch.device("cpu")
self.temporary_cpu_fallback = True
try:
# Try loading on CPU
self.active_model = self.active_model.to(self.current_device)
load_time = time.time() - start_time
log_debug(f"Model {model_name} loaded to CPU (temporary fallback) in {load_time:.2f} seconds")
except Exception as cpu_error:
log_debug(f"CPU fallback also failed: {cpu_error}")
return False
else:
# We're either permanently CPU-configured or already in fallback mode
if self.permanent_gpu_enabled:
log_debug(f"CPU fallback also failed: {e}")
else:
log_debug(f"CPU loading failed (permanently configured for CPU): {e}")
return False
self.active_model_key = model_key
# Add to supported languages if successful
self.supported_langs.add(source_lang)
self.supported_langs.add(target_lang)
# Add to direct pairs if it wasn't there before
if model_key not in self.direct_pairs:
self.direct_pairs[model_key] = model_name
log_debug(f"Added new model to direct_pairs: {model_key} -> {model_name}")
return True
except Exception as e:
log_debug(f"Could not load model {model_name}: {e}")
# Will continue to pivot translation
return False
# MODIFIED: Removed @lru_cache decorator - caching now handled by unified cache
def _translate_text_cached(self, text, model_key, beam_value): # Added beam_value parameter
"""Perform the actual translation with caching and thread safety. Supports both single text and batch input."""
# Use a lock for thread-safe model access
with self.model_lock:
# This method assumes the correct model is already loaded
if self.active_model_key != model_key or self.active_tokenizer is None or self.active_model is None:
log_debug(f"Model key mismatch or model not loaded. Active: {self.active_model_key}, Requested: {model_key}")
return f"Error: Model {model_key} not loaded correctly"
# Determine if this is batch input (list) or single input (string)
is_batch_input = isinstance(text, list)
if is_batch_input:
return self._translate_batch_input(text, beam_value)
else:
return self._translate_single_input(text, beam_value)
def _translate_single_input(self, text, beam_value):
"""Handle single text input translation."""
try:
# IMPROVED: Handle potential token limits by checking text length
total_chars = len(text)
# For very short text, just translate directly with high quality settings
if total_chars < 2:
inputs = self.active_tokenizer([text], return_tensors="pt", padding=True)
# Move inputs to current device (GPU or CPU, includes fallback handling)
inputs = {k: v.to(self.current_device) for k, v in inputs.items()}
if torch: # Check torch exists
with torch.no_grad():
try:
if self.current_device.type == 'cuda':
# GPU generation with mixed precision for speed
with torch.cuda.amp.autocast():
translated = self.active_model.generate(
**inputs,
max_length=512,
num_beams=beam_value,
length_penalty=1.0,
no_repeat_ngram_size=2
)
else:
# CPU generation
translated = self.active_model.generate(
**inputs,
max_length=512,
num_beams=beam_value,
length_penalty=1.0,
no_repeat_ngram_size=2
)
except torch.cuda.OutOfMemoryError:
# Smart fallback for GPU OOM during translation
if self.permanent_gpu_enabled and self.current_device.type == 'cuda':
log_debug("GPU out of memory during translation, falling back to CPU temporarily")
# Move everything to CPU for this translation
inputs_cpu = {k: v.cpu() for k, v in inputs.items()}
model_cpu = self.active_model.cpu()
self.current_device = torch.device("cpu")
self.temporary_cpu_fallback = True
# Perform translation on CPU
translated = model_cpu.generate(
**inputs_cpu,
max_length=512,
num_beams=beam_value,
length_penalty=1.0,
no_repeat_ngram_size=2
)
# Keep model on CPU for subsequent translations until next model load
self.active_model = model_cpu
log_debug("Translation completed on CPU fallback. GPU will be retried on next model load.")
else:
# Either not GPU-configured or already in CPU mode
log_debug("Out of memory error in CPU mode or non-GPU configuration")
return "Error: Out of memory during translation."
result = self.active_tokenizer.batch_decode(translated, skip_special_tokens=True)[0]
return result
else: # Fallback if torch is not available (should not happen if MARIANMT_AVAILABLE is True)
return "Error: PyTorch not available for translation."
# For longer text, ensure we don't exceed tokenizer limits
# First normalize spacing and punctuation
text = re.sub(r'\s+', ' ', text).strip()
# IMPROVED: Get token count before translation to check limits
# This helps diagnose potential truncation issues
token_info = self.active_tokenizer.encode(text, add_special_tokens=True)
token_count = len(token_info)
log_debug(f"Text has {token_count} tokens for {total_chars} characters")
# IMPROVED: Handle very long text with potential token limit issues
if token_count > 450: # Most models have ~512 token limits
log_debug(f"Warning: Text exceeds recommended token limit ({token_count} tokens)")
if token_count > 900: # Critical limit, force chunking
log_debug("Critical token limit exceeded, forcing text truncation")
# Get a safely-sized substring
tokens_to_use = token_info[:400] # Use first ~400 tokens
# Convert tokens back to text to preserve complete sentences
truncated_text = self.active_tokenizer.decode(tokens_to_use, skip_special_tokens=True)
log_debug(f"Truncated text from {total_chars} to {len(truncated_text)} chars")
text = truncated_text
# Create a clean input for translation with the possibly truncated text
inputs = self.active_tokenizer([text], return_tensors="pt", padding=True)
# Move inputs to current device (GPU or CPU, includes fallback handling)
inputs = {k: v.to(self.current_device) for k, v in inputs.items()}
# Use no_grad for better memory usage during inference
if torch: # Check torch exists
with torch.no_grad():
log_debug(f"Using beam search value: {beam_value}")
try:
if self.current_device.type == 'cuda':
# GPU generation with mixed precision for speed and memory efficiency
with torch.cuda.amp.autocast():
translated = self.active_model.generate(
**inputs,
max_length=512, # Increased max_length for longer content
num_beams=beam_value,
length_penalty=1.0, # Higher penalty = longer outputs
no_repeat_ngram_size=2, # Prevent repetition
min_length=0,
early_stopping=(beam_value > 1),
repetition_penalty=1.1 # Further discourage repetition/truncation
)
else:
# CPU generation
translated = self.active_model.generate(
**inputs,
max_length=512, # Increased max_length for longer content
num_beams=beam_value,
length_penalty=1.0, # Higher penalty = longer outputs
no_repeat_ngram_size=2, # Prevent repetition
min_length=0,
early_stopping=(beam_value > 1),
repetition_penalty=1.1 # Further discourage repetition/truncation
)
except torch.cuda.OutOfMemoryError:
# Smart fallback for GPU OOM during longer text translation
if self.permanent_gpu_enabled and self.current_device.type == 'cuda':
log_debug("GPU out of memory during longer text translation, falling back to CPU temporarily")
# Move everything to CPU for this translation
inputs_cpu = {k: v.cpu() for k, v in inputs.items()}
model_cpu = self.active_model.cpu()
self.current_device = torch.device("cpu")
self.temporary_cpu_fallback = True
# Perform translation on CPU
translated = model_cpu.generate(
**inputs_cpu,
max_length=512,
num_beams=beam_value,
length_penalty=1.0,
no_repeat_ngram_size=2,
min_length=0,
early_stopping=(beam_value > 1),
repetition_penalty=1.1
)
# Keep model on CPU for subsequent translations until next model load
self.active_model = model_cpu
log_debug("Longer text translation completed on CPU fallback. GPU will be retried on next model load.")
else:
# Either not GPU-configured or already in CPU mode
log_debug("Out of memory error during longer text translation in CPU mode or non-GPU configuration")
return "Error: Out of memory during translation."
# Decode the result
result = self.active_tokenizer.batch_decode(translated, skip_special_tokens=True)[0]
# Enhanced logging and validation
input_sentences = len(re.findall(r'[.!?]\s+|\n+', text)) + 1
output_sentences = len(re.findall(r'[.!?]\s+|\n+', result)) + 1
log_ratio = len(result) / max(1, len(text))
# Log device used for performance tracking
current_device_name = "GPU" if (self.current_device.type == 'cuda') else "CPU"
fallback_info = " (temporary fallback)" if self.temporary_cpu_fallback else ""
log_debug(f"Translation completed on {current_device_name}{fallback_info}: {len(text)} → {len(result)} chars")
# IMPROVED: Check for potentially incomplete translations
if (len(result) < len(text) * 0.7 and len(text) > 50) or \
(output_sentences < input_sentences and input_sentences > 1) or \
(len(result) < 10 and len(text) > 20):
log_debug(f"Warning: Translation may be incomplete. Input: {len(text)} chars ({input_sentences} sentences), Output: {len(result)} chars ({output_sentences} sentences), Ratio: {log_ratio:.2f}")
log_debug(f"Input text: '{text}'")
log_debug(f"Result: '{result}'")
return result
else: # Fallback if torch is not available
return "Error: PyTorch not available for translation."
except Exception as e:
# import traceback # Already imported at the top of the file
error_details = traceback.format_exc()
log_debug(f"Translation error: {e}\n{error_details}")
return f"Translation error: {str(e)}"
def _translate_batch_input(self, text_list, beam_value):
"""Handle batch text input translation."""
try:
if not text_list:
return []
# Log batch processing start
batch_size = len(text_list)
log_debug(f"Processing batch of {batch_size} sentences for translation")
# Prepare inputs for batch processing
inputs = self.active_tokenizer(text_list, return_tensors="pt", padding=True, truncation=True, max_length=450)
# Move inputs to current device (GPU or CPU, includes fallback handling)
inputs = {k: v.to(self.current_device) for k, v in inputs.items()}
# Use no_grad for better memory usage during inference
if torch: # Check torch exists
with torch.no_grad():
log_debug(f"Using beam search value: {beam_value} for batch translation")
try:
if self.current_device.type == 'cuda':
# GPU generation with mixed precision for speed and memory efficiency
with torch.cuda.amp.autocast():
translated = self.active_model.generate(
**inputs,
max_length=512, # Increased max_length for longer content
num_beams=beam_value,
length_penalty=1.0, # Higher penalty = longer outputs
no_repeat_ngram_size=2, # Prevent repetition
min_length=0,
early_stopping=(beam_value > 1),
repetition_penalty=1.1 # Further discourage repetition/truncation
)
else:
# CPU generation
translated = self.active_model.generate(
**inputs,
max_length=512, # Increased max_length for longer content
num_beams=beam_value,
length_penalty=1.0, # Higher penalty = longer outputs
no_repeat_ngram_size=2, # Prevent repetition
min_length=0,
early_stopping=(beam_value > 1),
repetition_penalty=1.1 # Further discourage repetition/truncation
)
except torch.cuda.OutOfMemoryError:
# Smart fallback for GPU OOM during batch translation
if self.permanent_gpu_enabled and self.current_device.type == 'cuda':
log_debug("GPU out of memory during batch translation, falling back to CPU temporarily")
# Move everything to CPU for this translation
inputs_cpu = {k: v.cpu() for k, v in inputs.items()}
model_cpu = self.active_model.cpu()
self.current_device = torch.device("cpu")
self.temporary_cpu_fallback = True
# Perform translation on CPU
translated = model_cpu.generate(
**inputs_cpu,
max_length=512,
num_beams=beam_value,
length_penalty=1.0,
no_repeat_ngram_size=2,
min_length=0,
early_stopping=(beam_value > 1),
repetition_penalty=1.1
)
# Keep model on CPU for subsequent translations until next model load
self.active_model = model_cpu
log_debug("Batch translation completed on CPU fallback. GPU will be retried on next model load.")
else:
# Either not GPU-configured or already in CPU mode
log_debug("Out of memory error during batch translation in CPU mode or non-GPU configuration")
return ["Error: Out of memory during translation."] * batch_size
# Decode all results at once
results = self.active_tokenizer.batch_decode(translated, skip_special_tokens=True)
# Log device used for performance tracking
current_device_name = "GPU" if (self.current_device.type == 'cuda') else "CPU"
fallback_info = " (temporary fallback)" if self.temporary_cpu_fallback else ""
log_debug(f"Batch translation completed on {current_device_name}{fallback_info}: {batch_size} sentences processed")
return results
else: # Fallback if torch is not available
return ["Error: PyTorch not available for translation."] * batch_size
except Exception as e:
# import traceback # Already imported at the top of the file
error_details = traceback.format_exc()
log_debug(f"Batch translation error: {e}\n{error_details}")
return [f"Translation error: {str(e)}"] * len(text_list)
def translate(self, text, source_lang, target_lang):
"""
Translate text from source language to target language using batch processing.
Args:
text: Text to translate
source_lang: Source language code
target_lang: Target language code
Returns:
Translated text or error message
"""
translation_start_time = time.monotonic() # Start timing the entire process
# Basic validation
source_lang = source_lang.lower()
target_lang = target_lang.lower()
if source_lang == target_lang:
return text # No translation needed
# Try to load direct model or dynamically attempt the pair
model_key = (source_lang, target_lang)
direct_model_loaded = self._try_load_direct_model(source_lang, target_lang)
if not direct_model_loaded:
# If we couldn't load the model, provide a specific error message
# Get language names from codes for a more user-friendly message
# Use a simple capitalize for names, specific handling for common ones if needed
source_lang_name = source_lang.capitalize()
target_lang_name = target_lang.capitalize()
common_lang_map = {
'en': 'English', 'pl': 'Polish', 'de': 'German', 'fr': 'French',
'es': 'Spanish', 'it': 'Italian'
}
source_lang_name = common_lang_map.get(source_lang, source_lang_name)
target_lang_name = common_lang_map.get(target_lang, target_lang_name)
error_msg = f"The {source_lang_name} to {target_lang_name} translation is not supported by MarianMT models. Consider switching to Google Translate."
log_debug(error_msg)
return error_msg
# Ensure we're working with a clean string
if not text or not isinstance(text, str):
return "" if text is None else str(text)
# Clean up and normalize the text
text = re.sub(r'\s+', ' ', text).strip()
if not text:
return ""
# Log the text being translated for debugging
current_device_name = "GPU" if (self.current_device.type == 'cuda') else "CPU"
fallback_info = " (temporary fallback)" if self.temporary_cpu_fallback else ""
log_debug(f"MarianMT translating on {current_device_name}{fallback_info}: \"{text}\" from {source_lang} to {target_lang}")
# Split text into sentences
sentences = self._split_into_sentences(text)
# Handle single-sentence case directly
if len(sentences) <= 1 or len(text) < 30:
log_debug(f"Using direct translation for single sentence or short text: \"{text}\"")
result = self._translate_batch(text, source_lang, target_lang)
# Log completion time
translation_time = time.monotonic() - translation_start_time
current_device_name = "GPU" if (self.current_device.type == 'cuda') else "CPU"
fallback_info = " (temporary fallback)" if self.temporary_cpu_fallback else ""
log_debug(f"MarianMT translation complete in {translation_time:.3f} seconds on {current_device_name}{fallback_info}")
log_debug(f"The completed translation is displayed: \"{result}\"")
return result
# Log that we're using batch translation
current_device_name = "GPU" if (self.current_device.type == 'cuda') else "CPU"
fallback_info = " (temporary fallback)" if self.temporary_cpu_fallback else ""
log_debug(f"Translating {len(sentences)} sentences in batch mode on {current_device_name}{fallback_info}")
try:
# Use batch translation for multiple sentences
translated_sentences = self._translate_batch_sentences(sentences, source_lang, target_lang)
# Join translated sentences with appropriate spacing
result = " ".join(translated_sentences)
# Calculate and log total time
translation_time = time.monotonic() - translation_start_time
current_device_name = "GPU" if (self.current_device.type == 'cuda') else "CPU"
fallback_info = " (temporary fallback)" if self.temporary_cpu_fallback else ""
log_debug(f"Batch translation complete on {current_device_name}{fallback_info}: {len(translated_sentences)} sentences processed in {translation_time:.3f} seconds")
log_debug(f"The completed subtitle is displayed: \"{result}\"")
return result
except Exception as e:
error_msg = f"Error in batch translation: {str(e)}"
log_debug(error_msg)
log_debug(traceback.format_exc())
# Fall back to sequential translation on error
log_debug("Falling back to sequential translation")
return self._sequential_fallback_translate(text, sentences, source_lang, target_lang)
def _sequential_fallback_translate(self, full_text, sentences, source_lang, target_lang):
"""Fallback method to translate sequentially if batch translation fails."""
sequential_start_time = time.monotonic() # Start timing the sequential process
try:
# If sentences list is empty or invalid, translate the full text directly
if not sentences:
log_debug(f"No sentences to translate, using full text: \"{full_text}\"")
result = self._translate_batch(full_text, source_lang, target_lang)
log_debug(f"Full text translated in fallback mode: \"{result}\"")
return result
# Process sentences sequentially using the single input method
log_debug(f"Sequential translation of {len(sentences)} sentences:")
translated_parts = []
for i, sentence in enumerate(sentences):
if sentence.strip():
log_debug(f"Sequentially translating sentence {i+1}: \"{sentence}\"")
# Track individual sentence translation time
sentence_start_time = time.monotonic()
translated = self._translate_batch(sentence, source_lang, target_lang)
sentence_translation_time = time.monotonic() - sentence_start_time
if translated and not self._is_error_message(translated):
log_debug(f"Sentence {i+1}: \"{sentence}\" has been translated by MarianMT in {sentence_translation_time:.2f}s")
log_debug(f"Translation result: \"{translated}\"")
translated_parts.append(translated)
else:
log_debug(f"Sentence {i+1} translation failed, using original: \"{sentence}\"")
translated_parts.append(sentence)
else:
# Add empty sentence as-is
translated_parts.append(sentence)
# Join results
if translated_parts:
result = " ".join(translated_parts)
translation_time = time.monotonic() - sequential_start_time
log_debug(f"Sequential translation complete: {len(translated_parts)} sentences processed in {translation_time:.3f} seconds")
log_debug(f"The completed subtitle is displayed: \"{result}\"")
return result
else:
# Last resort - translate full text
log_debug(f"No translated parts, using full text as last resort: \"{full_text}\"")
result = self._translate_batch(full_text, source_lang, target_lang)
translation_time = time.monotonic() - sequential_start_time
log_debug(f"Full text translated in {translation_time:.3f} seconds: \"{result}\"")
return result
except Exception as e:
log_debug(f"Sequential fallback translation failed: {e}")
# Return original text in case of complete failure
return full_text
def _is_error_message(self, text):
"""Check if a translation result is an error message."""
if not isinstance(text, str):
return True
error_indicators = [
"error:", "translation error", "not initialized", "missing", "failed",
"not available", "not supported", "invalid result", "empty result"
]
text_lower = text.lower()
return any(indicator in text_lower for indicator in error_indicators)
def _translate_batch(self, text, source_lang, target_lang):
"""Translate a single batch of text using appropriate model. (Pivot translation removed)."""
try:
log_debug(f"Attempting to translate batch: \"{text}\" from {source_lang} to {target_lang}")
# --- Direct Translation Attempt Only ---
model_key = (source_lang, target_lang)
# Check if we need to force load the model (if it changed)
force_load_needed = (self.active_model_key != model_key or self.active_model is None) # Also check if model is None
if force_load_needed:
log_debug(f"Model key change or model not loaded: {self.active_model_key} -> {model_key}, Model loaded: {self.active_model is not None}")
# Try to load the direct model for the requested pair
direct_model_loaded = False
# Attempt load if needed or if the correct model isn't already active
if force_load_needed : # Simplified condition
direct_model_loaded = self._try_load_direct_model(source_lang, target_lang)
elif self.active_model_key == model_key and self.active_model is not None : # Model is correct and loaded
direct_model_loaded = True
log_debug(f"Using already loaded model for {source_lang}->{target_lang}")
else: # Model key matches but model is None (e.g. previous load failed)
direct_model_loaded = self._try_load_direct_model(source_lang, target_lang)
# --- Perform Translation if Direct Model Loaded ---
if direct_model_loaded:
log_debug(f"Using direct translation model for {source_lang}->{target_lang}")
# Pass self.num_beams as the third argument to the cached function
translated = self._translate_text_cached(text, model_key, self.num_beams)
log_debug(f"Direct translation result: \"{translated}\"")
return translated
# --- Handle Failure: Direct Model Not Available (No Pivot Fallback) ---
else:
log_debug(f"Direct model for {source_lang}->{target_lang} failed to load or is not available.")
# Generate user-friendly language names for the error message
source_lang_name = source_lang.capitalize()
target_lang_name = target_lang.capitalize()
# Add common language names for better messages
common_langs = {'en': 'English', 'pl': 'Polish', 'de': 'German', 'fr': 'French', 'es': 'Spanish', 'it': 'Italian'}
source_lang_name = common_langs.get(source_lang, source_lang_name)
target_lang_name = common_langs.get(target_lang, target_lang_name)
# Construct the specific error message indicating lack of support
error_msg = f"The {source_lang_name} to {target_lang_name} translation is not supported by MarianMT models. Consider switching to Google Translate."
log_debug(error_msg) # Log the error
return error_msg # Return the error message to the caller
# --- General Exception Handling ---
except Exception as e:
# Log any unexpected errors during the process
log_debug(f"MarianMT translation error in _translate_batch: {type(e).__name__} - {str(e)}")
log_debug(traceback.format_exc())
# Return a generic error message
return f"Translation error: {type(e).__name__} - {str(e)}"
def _split_into_sentences(self, text):
"""Split text into sentences for parallel translation processing."""
if not text:
return []
# Normalize whitespace first
text = re.sub(r'\s+', ' ', text).strip()
# Log the full text before splitting