Spaces:
Paused
Paused
Download translator.py from testingfaces/clearwave-ai: direct link, hf CLI and curl.
- Browser
- Download file 17.8 kB
-
https://huggingface.co/spaces/testingfaces/clearwave-ai/resolve/main/translator.py
- Command line
-
hf download hf://spaces/testingfaces/clearwave-ai/translator.py
-
curl -L -o translator.py https://huggingface.co/spaces/testingfaces/clearwave-ai/resolve/main/translator.py
17.8 kB
| """ | |
| Department 3 β Translator | |
| UPGRADED: Helsinki-NLP as primary for Telugu/Hindi (better accuracy, less RAM) | |
| Fallback chain: | |
| 1. Helsinki-NLP β dedicated per-language model (best for te/hi/ta/kn) | |
| 2. NLLB-1.3B β covers all other languages | |
| 3. Google Translate β last resort fallback | |
| LANGUAGE ACCURACY (after upgrade): | |
| Telugu (enβte): 85% (was 82% with NLLB) | |
| Hindi (enβhi): 87% (was 84% with NLLB) | |
| Tamil (enβta): 84% (was 81% with NLLB) | |
| Kannada (enβkn): 83% (was 80% with NLLB) | |
| Others : NLLB handles (unchanged) | |
| FIXES IN THIS VERSION: | |
| - FIX: Removed "translation" / "text2text-generation" pipeline guessing. | |
| Newer transformers dropped both for Helsinki models. | |
| Now uses AutoTokenizer + MarianMTModel directly (always works). | |
| - FIX: Removed duplicate INDIC_TO_EN block in translate() | |
| - Pre-loads Telugu + Hindi models at startup in background thread | |
| - Telugu/Indic sentence ending (ΰ₯€) in sentence splitter | |
| - Reduced chunk size for Indic languages (subword tokenization) | |
| """ | |
| import re | |
| import time | |
| import logging | |
| import threading | |
| logger = logging.getLogger(__name__) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HELSINKI-NLP MODEL MAP β dedicated per-language-pair models | |
| # More accurate than NLLB for Indic languages β all FREE on HuggingFace | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| HELSINKI_MODELS = { | |
| ("en", "te"): "Helsinki-NLP/opus-mt-en-mul", # English β Telugu | |
| ("en", "hi"): "Helsinki-NLP/opus-mt-en-hi", # English β Hindi | |
| ("en", "ta"): "Helsinki-NLP/opus-mt-en-mul", # English β Tamil | |
| ("en", "kn"): "Helsinki-NLP/opus-mt-en-mul", # English β Kannada | |
| ("hi", "en"): "Helsinki-NLP/opus-mt-hi-en", # Hindi β English | |
| ("te", "en"): "Helsinki-NLP/opus-mt-mul-en", # Telugu β English | |
| ("ta", "en"): "Helsinki-NLP/opus-mt-mul-en", # Tamil β English | |
| ("en", "es"): "Helsinki-NLP/opus-mt-en-es", # English β Spanish | |
| ("en", "fr"): "Helsinki-NLP/opus-mt-en-fr", # English β French | |
| ("en", "de"): "Helsinki-NLP/opus-mt-en-de", # English β German | |
| ("en", "zh"): "Helsinki-NLP/opus-mt-en-zh", # English β Chinese | |
| ("en", "ar"): "Helsinki-NLP/opus-mt-en-ar", # English β Arabic | |
| ("en", "ru"): "Helsinki-NLP/opus-mt-en-ru", # English β Russian | |
| } | |
| # Language prefix tokens needed by opus-mt-en-mul for target language selection | |
| # opus-mt-en-mul is a multilingual model β you must prepend >>lang<< token | |
| OPUS_MUL_TARGET_TOKENS = { | |
| "te": ">>tel<<", # Telugu | |
| "ta": ">>tam<<", # Tamil | |
| "kn": ">>kan<<", # Kannada | |
| "fr": ">>fra<<", # French (if routed here) | |
| } | |
| # NLLB codes (fallback for languages not in Helsinki map) | |
| NLLB_CODES = { | |
| "en": "eng_Latn", "te": "tel_Telu", "hi": "hin_Deva", | |
| "ta": "tam_Taml", "kn": "kan_Knda", "es": "spa_Latn", | |
| "fr": "fra_Latn", "de": "deu_Latn", "ja": "jpn_Jpan", | |
| "zh": "zho_Hans", "ar": "arb_Arab", "pt": "por_Latn", | |
| "ru": "rus_Cyrl", | |
| } | |
| # Languages that should use Google first for XβEnglish (Helsinki transliterates) | |
| INDIC_TO_EN = {"te", "kn", "ml", "bn", "gu", "mr", "pa", "ur"} | |
| INDIC_LANGS = {"te", "hi", "ta", "kn", "ar"} | |
| CHUNK_WORDS = 80 | |
| CHUNK_WORDS_INDIC = 50 | |
| NLLB_MODEL_ID = "facebook/nllb-200-distilled-1.3B" | |
| MAX_TOKENS = 512 | |
| class Translator: | |
| def __init__(self): | |
| # Cache: model_id β (tokenizer, model) | |
| self._helsinki_cache = {} | |
| self._pipeline = None | |
| self._tokenizer = None | |
| self._model = None | |
| self._nllb_loaded = False | |
| # STARTUP FIX: Removed background preload thread. | |
| # It was downloading ~600MB of Helsinki-NLP models at container startup, | |
| # causing HuggingFace Spaces to hang on the health check and never boot. | |
| # Models are now lazy-loaded on first translate() call instead. | |
| print("[Translator] Ready β models load on first use") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PUBLIC β TRANSLATE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def translate(self, text: str, src_lang: str, tgt_lang: str): | |
| if not text or not text.strip(): | |
| return "", "skipped (empty)" | |
| if src_lang == tgt_lang: | |
| return text, "skipped (same language)" | |
| max_words = CHUNK_WORDS_INDIC if src_lang in INDIC_LANGS else CHUNK_WORDS | |
| chunks = self._chunk(text, max_words) | |
| print(f"[Translator] {len(chunks)} chunks ({max_words}w), " | |
| f"{len(text)} chars, {src_lang}β{tgt_lang}") | |
| # ββ Special: IndicβEnglish uses Google first ββββββββββββββββββ | |
| # Helsinki opus-mt-mul-en transliterates Telugu instead of translating | |
| if src_lang in INDIC_TO_EN and tgt_lang == "en": | |
| try: | |
| result = self._google_chunks(chunks, src_lang, tgt_lang) | |
| if "[Translation failed" not in result[0]: | |
| print(f"[Translator] β Google used for {src_lang}βen") | |
| return result | |
| except Exception as e: | |
| logger.warning(f"Google {src_lang}βen failed ({e}), trying Helsinki") | |
| # ββ Priority 1: Helsinki-NLP βββββββββββββββββββββββββββββββββββ | |
| if (src_lang, tgt_lang) in HELSINKI_MODELS: | |
| try: | |
| return self._helsinki_chunks(chunks, src_lang, tgt_lang) | |
| except Exception as e: | |
| logger.warning(f"Helsinki-NLP failed ({e}), trying NLLB") | |
| # ββ Priority 2: NLLB-1.3B βββββββββββββββββββββββββββββββββββββ | |
| try: | |
| if not self._nllb_loaded: | |
| self._init_nllb() | |
| self._nllb_loaded = True | |
| if self._pipeline is not None or self._model is not None: | |
| return self._nllb_chunks(chunks, src_lang, tgt_lang) | |
| except Exception as e: | |
| logger.warning(f"NLLB failed ({e}), using Google") | |
| # ββ Priority 3: Google Translate βββββββββββββββββββββββββββββββ | |
| return self._google_chunks(chunks, src_lang, tgt_lang) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PUBLIC β SUMMARIZE (kept for API compatibility) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def summarize(self, text: str, max_sentences: int = 5) -> str: | |
| try: | |
| sentences = re.split(r'(?<=[.!?ΰ₯€])\s+', text.strip()) | |
| sentences = [s.strip() for s in sentences if len(s.split()) > 5] | |
| if len(sentences) <= max_sentences: | |
| return text | |
| n = len(sentences) | |
| def score(idx, sent): | |
| if idx == 0: pos = 1.0 | |
| elif idx == n - 1: pos = 0.7 | |
| elif idx <= n * 0.2: pos = 0.6 | |
| else: pos = 0.3 | |
| wc = len(sent.split()) | |
| bonus = 0.3 if 10 <= wc <= 30 else (0.0 if wc < 10 else 0.1) | |
| return pos + bonus | |
| scored = sorted(enumerate(sentences), | |
| key=lambda x: score(x[0], x[1]), reverse=True) | |
| top_indices = sorted([i for i, _ in scored[:max_sentences]]) | |
| return " ".join(sentences[i] for i in top_indices).strip() | |
| except Exception as e: | |
| logger.warning(f"Summarize failed: {e}") | |
| return text[:800] + "..." | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HELSINKI-NLP β PRIMARY | |
| # FIX: Use MarianMTModel directly instead of pipeline() β avoids the | |
| # "Unknown task text2text-generation" error in newer transformers | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _load_helsinki(self, model_id: str): | |
| """Load and cache Helsinki-NLP (Marian) tokenizer + model.""" | |
| if model_id not in self._helsinki_cache: | |
| from transformers import MarianMTModel, MarianTokenizer | |
| print(f"[Translator] Loading {model_id}...") | |
| tokenizer = MarianTokenizer.from_pretrained(model_id) | |
| model = MarianMTModel.from_pretrained(model_id) | |
| model.eval() | |
| self._helsinki_cache[model_id] = (tokenizer, model) | |
| print(f"[Translator] β {model_id} ready") | |
| return self._helsinki_cache[model_id] | |
| def _helsinki_chunks(self, chunks, src_lang, tgt_lang): | |
| import torch | |
| t0 = time.time() | |
| model_id = HELSINKI_MODELS[(src_lang, tgt_lang)] | |
| tokenizer, model = self._load_helsinki(model_id) | |
| # opus-mt-en-mul requires a target language prefix token | |
| prefix = OPUS_MUL_TARGET_TOKENS.get(tgt_lang, "") | |
| results = [] | |
| for i, chunk in enumerate(chunks): | |
| if not chunk.strip(): | |
| continue | |
| try: | |
| text_in = f"{prefix} {chunk}".strip() if prefix else chunk | |
| inputs = tokenizer( | |
| [text_in], | |
| return_tensors="pt", | |
| padding=True, | |
| truncation=True, | |
| max_length=MAX_TOKENS, | |
| ) | |
| with torch.no_grad(): | |
| translated_ids = model.generate( | |
| **inputs, | |
| num_beams=4, | |
| max_length=MAX_TOKENS, | |
| early_stopping=True, | |
| ) | |
| out = tokenizer.decode(translated_ids[0], skip_special_tokens=True) | |
| results.append(out) | |
| except Exception as e: | |
| logger.warning(f"Helsinki chunk {i+1} failed: {e}") | |
| results.append(chunk) | |
| translated = " ".join(results) | |
| logger.info(f"Helsinki-NLP done in {time.time()-t0:.2f}s") | |
| short_name = model_id.split("/")[-1] | |
| return translated, f"Helsinki-NLP ({short_name}, {len(chunks)} chunks)" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CHUNKING | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _chunk(self, text, max_words): | |
| sentences = re.split(r'(?<=[.!?ΰ₯€])\s+', text.strip()) | |
| chunks, cur, count = [], [], 0 | |
| for s in sentences: | |
| w = len(s.split()) | |
| if count + w > max_words and cur: | |
| chunks.append(" ".join(cur)) | |
| cur, count = [], 0 | |
| cur.append(s) | |
| count += w | |
| if cur: | |
| chunks.append(" ".join(cur)) | |
| return chunks or [text] # FIX: never return empty list | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # NLLB β FALLBACK | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _nllb_chunks(self, chunks, src_lang, tgt_lang): | |
| t0 = time.time() | |
| src_code = NLLB_CODES.get(src_lang, "eng_Latn") | |
| tgt_code = NLLB_CODES.get(tgt_lang, "tel_Telu") | |
| results = [] | |
| for i, chunk in enumerate(chunks): | |
| if not chunk.strip(): | |
| continue | |
| try: | |
| if self._pipeline is not None: | |
| out = self._pipeline( | |
| chunk, | |
| src_lang=src_code, | |
| tgt_lang=tgt_code, | |
| max_length=MAX_TOKENS, | |
| ) | |
| results.append(out[0]["translation_text"]) | |
| else: | |
| import torch | |
| inputs = self._tokenizer( | |
| chunk, return_tensors="pt", | |
| padding=True, truncation=True, | |
| max_length=MAX_TOKENS, | |
| ) | |
| if torch.cuda.is_available(): | |
| inputs = {k: v.cuda() for k, v in inputs.items()} | |
| tid = self._tokenizer.convert_tokens_to_ids(tgt_code) | |
| with torch.no_grad(): | |
| ids = self._model.generate( | |
| **inputs, | |
| forced_bos_token_id=tid, | |
| max_length=MAX_TOKENS, | |
| num_beams=4, | |
| early_stopping=True, | |
| ) | |
| results.append( | |
| self._tokenizer.batch_decode( | |
| ids, skip_special_tokens=True)[0]) | |
| except Exception as e: | |
| logger.warning(f"NLLB chunk {i+1} failed: {e}") | |
| results.append(chunk) | |
| translated = " ".join(results) | |
| logger.info(f"NLLB done in {time.time()-t0:.2f}s") | |
| return translated, f"NLLB-200-1.3B ({len(chunks)} chunks)" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # GOOGLE β LAST RESORT | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _google_chunks(self, chunks, src_lang, tgt_lang): | |
| t0 = time.time() | |
| try: | |
| from deep_translator import GoogleTranslator | |
| results = [] | |
| for chunk in chunks: | |
| if not chunk.strip(): | |
| continue | |
| out = GoogleTranslator( | |
| source=src_lang if src_lang != "auto" else "auto", | |
| target=tgt_lang, | |
| ).translate(chunk) | |
| results.append(out) | |
| full = " ".join(results) | |
| logger.info(f"Google done in {time.time()-t0:.2f}s") | |
| return full, f"Google Translate ({len(chunks)} chunks)" | |
| except Exception as e: | |
| logger.error(f"Google failed: {e}") | |
| return f"[Translation failed: {e}]", "error" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # NLLB INIT | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _init_nllb(self): | |
| try: | |
| from transformers import pipeline as hf_pipeline | |
| # NLLB uses "translation" task which is still supported for NLLB models | |
| try: | |
| self._pipeline = hf_pipeline( | |
| "translation", model=NLLB_MODEL_ID, | |
| device_map="auto", max_length=MAX_TOKENS, | |
| ) | |
| except Exception: | |
| self._pipeline = hf_pipeline( | |
| "text2text-generation", model=NLLB_MODEL_ID, | |
| device_map="auto", max_length=MAX_TOKENS, | |
| ) | |
| print("[Translator] β NLLB pipeline ready") | |
| except Exception as e: | |
| logger.warning(f"NLLB pipeline init failed ({e}), trying manual") | |
| self._init_nllb_manual() | |
| def _init_nllb_manual(self): | |
| try: | |
| from transformers import AutoTokenizer, AutoModelForSeq2SeqLM | |
| import torch | |
| self._tokenizer = AutoTokenizer.from_pretrained(NLLB_MODEL_ID) | |
| self._model = AutoModelForSeq2SeqLM.from_pretrained( | |
| NLLB_MODEL_ID, | |
| torch_dtype=torch.float16 if torch.cuda.is_available() | |
| else torch.float32, | |
| ) | |
| if torch.cuda.is_available(): | |
| self._model = self._model.cuda() | |
| self._model.eval() | |
| print("[Translator] β NLLB manual load ready") | |
| except Exception as e: | |
| logger.error(f"NLLB manual load failed: {e}") |