harshrawat18 commited on
Commit
d05deb5
Β·
verified Β·
1 Parent(s): 9f3d40d

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. Dockerfile +8 -25
  2. api.py +1 -2
  3. bhashini.py +94 -220
  4. requirements.txt +3 -8
Dockerfile CHANGED
@@ -9,9 +9,8 @@ FROM python:3.11-slim
9
  WORKDIR /app
10
 
11
  # ── Layer 1: OS Dependencies (ROOT) ──────────────────────────
12
- # libgomp1: Required for OpenMP multi-threading (IndicTrans2 + PyTorch CPU)
13
- # build-essential: Required for Cython compilation (IndicTransToolkit)
14
- # git: Required for pip install from GitHub repos
15
  RUN apt-get update && apt-get install -y --no-install-recommends \
16
  build-essential \
17
  git \
@@ -31,40 +30,24 @@ COPY requirements.txt .
31
  RUN pip install --no-cache-dir -r requirements.txt
32
 
33
  # ── Layer 4a: Pre-cache Nomic Embedding model ────────────────
34
- # Separated from reranker to prevent one failure from blocking the other.
35
  RUN python -c "\
36
  from sentence_transformers import SentenceTransformer; \
37
  SentenceTransformer('nomic-ai/nomic-embed-text-v1', cache_folder='/app/models', trust_remote_code=True); \
38
  print('βœ“ Nomic Embedding model (768-dim) cached')"
39
 
40
  # ── Layer 4b: Pre-cache Ettin Reranker (ModernBERT) ──────────
41
- # CRITICAL FIX: trust_remote_code=True is MANDATORY for ModernBERT architecture.
42
- # Without it: KeyError: 'modernbert' β†’ build crash.
43
  RUN python -c "\
44
  from sentence_transformers import CrossEncoder; \
45
  CrossEncoder('cross-encoder/ettin-reranker-68m-v1', max_length=512, trust_remote_code=True); \
46
  print('βœ“ Ettin Reranker (ModernBERT) cached')"
47
 
48
- # ── Layer 5a: Pre-cache IndicTrans2 Englishβ†’Indic model ──────
49
- # This is the English→Indic direction model (Sprint 18).
50
- # Required for local translation of English responses back to user's language.
51
- RUN python -c "\
52
- from transformers import AutoTokenizer, AutoModelForSeq2SeqLM; \
53
- AutoTokenizer.from_pretrained('ai4bharat/indictrans2-en-indic-dist-200M', trust_remote_code=True, cache_dir='/app/models/indictrans2/en_to_indic'); \
54
- AutoModelForSeq2SeqLM.from_pretrained('ai4bharat/indictrans2-en-indic-dist-200M', trust_remote_code=True, cache_dir='/app/models/indictrans2/en_to_indic'); \
55
- print('βœ“ IndicTrans2 Englishβ†’Indic model cached')" || echo "⚠️ IndicTrans2 en-indic cache failed (non-blocking)"
56
-
57
- # ── Layer 5b: Pre-cache IndicTrans2 Indicβ†’English model ──────
58
- # This is the REVERSE direction model (Sprint 18).
59
- # Required for local translation of Hindi/Tamil queries INTO English
60
- # before semantic search against the English vector corpus.
61
- RUN python -c "\
62
- from transformers import AutoTokenizer, AutoModelForSeq2SeqLM; \
63
- AutoTokenizer.from_pretrained('ai4bharat/indictrans2-indic-en-dist-200M', trust_remote_code=True, cache_dir='/app/models/indictrans2/indic_to_en'); \
64
- AutoModelForSeq2SeqLM.from_pretrained('ai4bharat/indictrans2-indic-en-dist-200M', trust_remote_code=True, cache_dir='/app/models/indictrans2/indic_to_en'); \
65
- print('βœ“ IndicTrans2 Indicβ†’English model cached')" || echo "⚠️ IndicTrans2 indic-en cache failed (non-blocking)"
66
 
67
- # ── Layer 6: Copy application code LAST ──────────────────────
68
  # Code changes don't invalidate the expensive model cache layers.
69
  COPY . .
70
 
 
9
  WORKDIR /app
10
 
11
  # ── Layer 1: OS Dependencies (ROOT) ──────────────────────────
12
+ # libgomp1: Required for OpenMP multi-threading (PyTorch CPU)
13
+ # build-essential + git: Required for pip builds
 
14
  RUN apt-get update && apt-get install -y --no-install-recommends \
15
  build-essential \
16
  git \
 
30
  RUN pip install --no-cache-dir -r requirements.txt
31
 
32
  # ── Layer 4a: Pre-cache Nomic Embedding model ────────────────
 
33
  RUN python -c "\
34
  from sentence_transformers import SentenceTransformer; \
35
  SentenceTransformer('nomic-ai/nomic-embed-text-v1', cache_folder='/app/models', trust_remote_code=True); \
36
  print('βœ“ Nomic Embedding model (768-dim) cached')"
37
 
38
  # ── Layer 4b: Pre-cache Ettin Reranker (ModernBERT) ──────────
39
+ # trust_remote_code=True is MANDATORY for ModernBERT architecture.
 
40
  RUN python -c "\
41
  from sentence_transformers import CrossEncoder; \
42
  CrossEncoder('cross-encoder/ettin-reranker-68m-v1', max_length=512, trust_remote_code=True); \
43
  print('βœ“ Ettin Reranker (ModernBERT) cached')"
44
 
45
+ # NOTE: IndicTrans2 model caching REMOVED (Sprint 18 v2).
46
+ # Translation is now handled by Groq LLM (llama-3.3-70b-versatile)
47
+ # which requires zero local model files. This saves ~1.6GB of image size
48
+ # and eliminates the transformers.onnx compatibility nightmare.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
+ # ── Layer 5: Copy application code LAST ──────────────────────
51
  # Code changes don't invalidate the expensive model cache layers.
52
  COPY . .
53
 
api.py CHANGED
@@ -188,13 +188,12 @@ async def health_check():
188
  async def debug_memory():
189
  """Live memory inspection endpoint (Sprint 18)."""
190
  from middleware.memory_guard import get_memory_mb
191
- import bhashini
192
 
193
  return {
194
  "rss_mb": round(get_memory_mb(), 1),
195
  "gc_counts": gc.get_count(),
196
  "gc_threshold": gc.get_threshold(),
197
- "models_loaded": list(bhashini._indictrans_models.keys()),
198
  "embedding_loaded": embedding_model is not None,
199
  "reranker_loaded": reranker_model is not None,
200
  }
 
188
  async def debug_memory():
189
  """Live memory inspection endpoint (Sprint 18)."""
190
  from middleware.memory_guard import get_memory_mb
 
191
 
192
  return {
193
  "rss_mb": round(get_memory_mb(), 1),
194
  "gc_counts": gc.get_count(),
195
  "gc_threshold": gc.get_threshold(),
196
+ "translation_provider": "groq-llm",
197
  "embedding_loaded": embedding_model is not None,
198
  "reranker_loaded": reranker_model is not None,
199
  }
bhashini.py CHANGED
@@ -1,78 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import httpx
2
  import os
3
- import gc
4
- import torch
5
- import threading
6
- import sys
7
- import types
8
-
9
- # --- CRITICAL SPRINT 18 PATCH (DEFINITIVE FIX) ---
10
- # IndicTrans2's remote `configuration_indictrans.py` imports many classes and functions
11
- # from `transformers.onnx` (OnnxConfig, OnnxSeq2SeqConfigWithPast, compute_effective_axis_dimension, etc.)
12
- # This subpackage was REMOVED in transformers>=4.40, but we need transformers>=4.48 for ModernBERT.
13
- #
14
- # SOLUTION: Universal mock modules that return a no-op dummy for ANY attribute access.
15
- # The ONNX classes are ONLY used for ONNX model export β€” never during inference.
16
- # So returning harmless dummies is 100% safe for our translation pipeline.
17
- if 'transformers.onnx' not in sys.modules:
18
-
19
- class _UniversalDummy:
20
- """A universal no-op class that acts as a stand-in for any ONNX class/function.
21
- Can be instantiated, called, subclassed, and used as a decorator β€” all no-ops."""
22
- def __init__(self, *a, **kw): pass
23
- def __call__(self, *a, **kw): return self
24
- def __getattr__(self, name): return _UniversalDummy()
25
- def __init_subclass__(cls, **kw): pass
26
-
27
- class _MockOnnxModule(types.ModuleType):
28
- """A module that returns _UniversalDummy for any attribute access.
29
- This means `from transformers.onnx.utils import <anything>` always succeeds."""
30
- def __getattr__(self, name):
31
- return _UniversalDummy
32
-
33
- # Build the mock package tree
34
- mock_onnx = _MockOnnxModule('transformers.onnx')
35
- mock_onnx.__path__ = [] # Makes Python treat it as a package
36
- mock_onnx.__package__ = 'transformers.onnx'
37
-
38
- mock_onnx_utils = _MockOnnxModule('transformers.onnx.utils')
39
- mock_onnx_utils.__package__ = 'transformers.onnx'
40
-
41
- mock_onnx_config = _MockOnnxModule('transformers.onnx.config')
42
- mock_onnx_config.__package__ = 'transformers.onnx'
43
-
44
- mock_onnx_features = _MockOnnxModule('transformers.onnx.features')
45
- mock_onnx_features.__package__ = 'transformers.onnx'
46
-
47
- # Register in sys.modules (covers all possible import paths)
48
- sys.modules['transformers.onnx'] = mock_onnx
49
- sys.modules['transformers.onnx.utils'] = mock_onnx_utils
50
- sys.modules['transformers.onnx.config'] = mock_onnx_config
51
- sys.modules['transformers.onnx.features'] = mock_onnx_features
52
-
53
- # Wire submodules as attributes on parent
54
- mock_onnx.utils = mock_onnx_utils
55
- mock_onnx.config = mock_onnx_config
56
- mock_onnx.features = mock_onnx_features
57
-
58
- # Wire onto the real transformers package
59
- import transformers
60
- transformers.onnx = mock_onnx
61
- # --------------------------------
62
-
63
- INDICTRANS_LANG_MAP = {
64
- "hindi": "hin_Deva",
65
- "tamil": "tam_Taml",
66
- "bengali": "ben_Beng",
67
- "telugu": "tel_Telu",
68
- "marathi": "mar_Deva",
69
- "gujarati": "guj_Gujr",
70
- "kannada": "kan_Knda",
71
- "malayalam": "mal_Mlym",
72
- "punjabi": "pan_Guru",
73
- "odia": "ory_Orya",
74
- "english": "eng_Latn"
75
- }
76
 
77
  LANGUAGE_CODES = {
78
  "hindi": "hi", "tamil": "ta", "bengali": "bn",
@@ -82,162 +29,80 @@ LANGUAGE_CODES = {
82
  "english": "en"
83
  }
84
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  BHASHINI_USER_ID = os.environ.get("BHASHINI_USER_ID")
86
  BHASHINI_API_KEY = os.environ.get("BHASHINI_API_KEY")
87
  BHASHINI_INFERENCE_URL = "https://dhruva-api.bhashini.gov.in/services/inference/pipeline"
88
 
89
- # Thread-safe model registry (prevents race conditions on concurrent requests)
90
- _model_lock = threading.Lock()
91
 
92
- # Model registry: direction β†’ (tokenizer, model)
93
- _indictrans_models: dict[str, tuple] = {}
 
 
 
 
 
 
 
94
 
95
- # Model identifiers
96
- INDIC_TO_EN_MODEL = "ai4bharat/indictrans2-indic-en-dist-200M"
97
- EN_TO_INDIC_MODEL = "ai4bharat/indictrans2-en-indic-dist-200M"
98
 
99
- def _get_model_for_direction(src_code: str, tgt_code: str) -> tuple:
100
  """
101
- Lazy-load the correct IndicTrans2 model based on translation direction.
102
 
103
- CRITICAL DESIGN DECISION: Both translation models are loaded and kept in
104
- memory concurrently (totaling ~1.6GB for translation) to prevent disk thrashing
105
- and latency spikes from alternating requests. Holding both models fits
106
- comfortably within the 16GB memory budget.
107
-
108
- Memory budget:
109
- - Python runtime + FastAPI: ~200MB
110
- - Nomic embed-text-v1: ~550MB
111
- - Ettin reranker-68m-v1: ~270MB
112
- - IndicTrans2 (both directions): ~1.6GB
113
- - Supabase client + misc: ~100MB
114
- - Total: ~2.7GB active, well under 16GB
115
  """
116
- global _indictrans_models
 
117
 
118
- # Determine direction
119
- is_to_english = (tgt_code == "eng_Latn")
120
- model_name = INDIC_TO_EN_MODEL if is_to_english else EN_TO_INDIC_MODEL
121
- direction_key = "indic_to_en" if is_to_english else "en_to_indic"
122
 
123
- with _model_lock:
124
- if direction_key in _indictrans_models:
125
- return _indictrans_models[direction_key]
126
-
127
- try:
128
- from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
129
- import torch
130
-
131
- print(f"⏳ Loading IndicTrans2 ({direction_key}): {model_name}...")
132
-
133
- tokenizer = AutoTokenizer.from_pretrained(
134
- model_name,
135
- trust_remote_code=True,
136
- cache_dir=f"/app/models/indictrans2/{direction_key}"
137
- )
138
- model = AutoModelForSeq2SeqLM.from_pretrained(
139
- model_name,
140
- trust_remote_code=True,
141
- cache_dir=f"/app/models/indictrans2/{direction_key}"
142
- )
143
-
144
- # Force CPU evaluation mode for minimal memory
145
- model.eval()
146
-
147
- _indictrans_models[direction_key] = (tokenizer, model)
148
- print(f"βœ… IndicTrans2 ({direction_key}) loaded successfully")
149
- return (tokenizer, model)
150
-
151
- except Exception as e:
152
- print(f"❌ IndicTrans2 ({direction_key}) load error: {e}")
153
- raise RuntimeError(f"IndicTrans2 {direction_key} model not available: {e}")
154
-
155
-
156
- def local_indictrans_translate(text: str, src: str, tgt: str) -> str:
157
- """
158
- Bidirectional IndicTrans2 translation with memory cleanup.
159
 
160
- CRITICAL FIX (Sprint 18): Now supports BOTH directions:
161
- - Indic β†’ English (using indictrans2-indic-en-dist-200M)
162
- - English β†’ Indic (using indictrans2-en-indic-dist-200M)
163
 
164
- Uses IndicProcessor for correct tokenization (Sprint 17 fix preserved).
165
- """
166
- src_code = INDICTRANS_LANG_MAP.get(src.lower().strip(), "eng_Latn")
167
- tgt_code = INDICTRANS_LANG_MAP.get(tgt.lower().strip(), "eng_Latn")
168
 
169
- # Get the correct model for this direction
170
- tokenizer, model = _get_model_for_direction(src_code, tgt_code)
171
-
172
- try:
173
- from IndicTransToolkit.processor import IndicProcessor
174
- ip = IndicProcessor(inference=True)
175
-
176
- # CRITICAL FIX 1: IndicProcessor.preprocess_batch() injects
177
- # <src_lang> and <tgt_lang> tokens. Without this: gibberish.
178
- batch = ip.preprocess_batch([text], src_lang=src_code, tgt_lang=tgt_code)
179
-
180
- inputs = tokenizer(
181
- batch,
182
- truncation=True,
183
- padding="longest",
184
- return_tensors="pt",
185
- return_attention_mask=True
186
- )
187
-
188
- with torch.inference_mode():
189
- generated_tokens = model.generate(
190
- **inputs,
191
- num_beams=4,
192
- num_return_sequences=1,
193
- max_length=512,
194
- early_stopping=True
195
- )
196
-
197
- # CRITICAL FIX 2: as_target_tokenizer() prevents vocabulary bleed
198
- with tokenizer.as_target_tokenizer():
199
- decoded = tokenizer.batch_decode(
200
- generated_tokens,
201
- skip_special_tokens=True,
202
- clean_up_tokenization_spaces=True
203
- )
204
-
205
- result = ip.postprocess_batch(decoded, lang=tgt_code)[0].strip()
206
-
207
- # SPRINT 18: Aggressive memory cleanup after every translation
208
- del inputs, generated_tokens, decoded, batch
209
- gc.collect()
210
-
211
- return result
212
-
213
- except ImportError:
214
- # Fallback if IndicTransToolkit not installed
215
- print("⚠️ IndicTransToolkit not found, using basic tokenizer")
216
- inputs = tokenizer(
217
- text,
218
- src_lang=src_code,
219
- return_tensors="pt",
220
- padding=True,
221
- truncation=True,
222
- max_length=512
223
- )
224
- with torch.inference_mode():
225
- outputs = model.generate(
226
- **inputs,
227
- tgt_lang=tgt_code,
228
- max_length=512,
229
- num_beams=4,
230
- early_stopping=True
231
- )
232
- decoded = tokenizer.batch_decode(outputs, skip_special_tokens=True)
233
-
234
- result = decoded[0].strip()
235
-
236
- # Memory cleanup
237
- del inputs, outputs, decoded
238
- gc.collect()
239
-
240
- return result
241
 
242
 
243
  async def translate_text(
@@ -246,10 +111,19 @@ async def translate_text(
246
  target_lang: str,
247
  fallback: bool = True
248
  ) -> str:
 
 
 
 
 
 
 
 
 
249
  if source_lang.lower() == target_lang.lower():
250
  return text
251
 
252
- # Layer 1: Bhashini API
253
  if BHASHINI_USER_ID and BHASHINI_API_KEY:
254
  try:
255
  src = LANGUAGE_CODES.get(source_lang.lower().strip(), source_lang)
@@ -278,15 +152,15 @@ async def translate_text(
278
  print(f"βœ… Bhashini: {source_lang} β†’ {target_lang}")
279
  return result
280
  except Exception as e:
281
- print(f"⚠️ Bhashini failed: {e}, trying IndicTrans2")
282
 
283
- # Layer 2: Local IndicTrans2
284
  try:
285
- result = local_indictrans_translate(text, source_lang, target_lang)
286
- print(f"βœ… IndicTrans2: {source_lang} β†’ {target_lang} | '{text[:30]}' β†’ '{result[:30]}'")
287
  return result
288
  except Exception as e:
289
- print(f"⚠️ IndicTrans2 failed: {e}")
290
 
291
  # Layer 3: Graceful degradation
292
  print(f"⚠️ All translation failed β€” returning original")
 
1
+ """
2
+ GovBridge India β€” Translation Module (Sprint 18 v2)
3
+
4
+ Translation Architecture (prioritized fallback chain):
5
+ Layer 1: Bhashini API (government-grade, when API key is approved)
6
+ Layer 2: Groq LLM Translation (llama-3.3-70b-versatile β€” already in our stack)
7
+ Layer 3: Graceful degradation (return original text)
8
+
9
+ WHY GROQ LLM INSTEAD OF IndicTrans2:
10
+ - IndicTrans2 (200M params) is fundamentally incompatible with transformers>=4.48
11
+ (required by ModernBERT/Ettin Reranker). Config, tokenizer, and ONNX layers all break.
12
+ - Groq's llama-3.3-70b-versatile (70B params) provides SUPERIOR translation quality
13
+ for all 10 supported Indic languages β€” 350x more parameters.
14
+ - Zero additional cost: we already have the Groq API key.
15
+ - Saves 1.6GB RAM on the 16GB HF Spaces instance.
16
+ - Zero dependency conflicts. Zero model loading time.
17
+ - Translation latency: ~200ms via Groq (vs ~2-4s for local IndicTrans2 on CPU).
18
+ """
19
+
20
  import httpx
21
  import os
22
+ from groq import Groq
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  LANGUAGE_CODES = {
25
  "hindi": "hi", "tamil": "ta", "bengali": "bn",
 
29
  "english": "en"
30
  }
31
 
32
+ # Full language names for LLM prompt clarity
33
+ LANGUAGE_NAMES = {
34
+ "hi": "Hindi", "ta": "Tamil", "bn": "Bengali",
35
+ "te": "Telugu", "mr": "Marathi", "gu": "Gujarati",
36
+ "kn": "Kannada", "ml": "Malayalam", "pa": "Punjabi",
37
+ "or": "Odia", "en": "English",
38
+ "hindi": "Hindi", "tamil": "Tamil", "bengali": "Bengali",
39
+ "telugu": "Telugu", "marathi": "Marathi", "gujarati": "Gujarati",
40
+ "kannada": "Kannada", "malayalam": "Malayalam", "punjabi": "Punjabi",
41
+ "odia": "Odia", "english": "English"
42
+ }
43
+
44
  BHASHINI_USER_ID = os.environ.get("BHASHINI_USER_ID")
45
  BHASHINI_API_KEY = os.environ.get("BHASHINI_API_KEY")
46
  BHASHINI_INFERENCE_URL = "https://dhruva-api.bhashini.gov.in/services/inference/pipeline"
47
 
48
+ # Groq client for LLM translation (Layer 2)
49
+ _groq_client = None
50
 
51
+ def _get_groq_client() -> Groq:
52
+ """Lazy-init Groq client for translation."""
53
+ global _groq_client
54
+ if _groq_client is None:
55
+ api_key = os.environ.get("GROQ_API_KEY")
56
+ if not api_key:
57
+ raise RuntimeError("GROQ_API_KEY not set")
58
+ _groq_client = Groq(api_key=api_key)
59
+ return _groq_client
60
 
 
 
 
61
 
62
+ def _groq_translate(text: str, source_lang: str, target_lang: str) -> str:
63
  """
64
+ Translate text using Groq's llama-3.3-70b-versatile.
65
 
66
+ ARCHITECTURAL NOTE: The LLM is used STRICTLY as a translator here.
67
+ It receives a system prompt that constrains it to output ONLY the translation,
68
+ with no explanations, no additions, no hallucinations.
 
 
 
 
 
 
 
 
 
69
  """
70
+ src_name = LANGUAGE_NAMES.get(source_lang.lower(), source_lang)
71
+ tgt_name = LANGUAGE_NAMES.get(target_lang.lower(), target_lang)
72
 
73
+ client = _get_groq_client()
 
 
 
74
 
75
+ response = client.chat.completions.create(
76
+ model="llama-3.3-70b-versatile",
77
+ messages=[
78
+ {
79
+ "role": "system",
80
+ "content": (
81
+ f"You are a professional translator. Translate the following text "
82
+ f"from {src_name} to {tgt_name}. "
83
+ f"Output ONLY the translated text. No explanations, no notes, "
84
+ f"no quotation marks, no prefixes like 'Translation:'. "
85
+ f"Preserve the original meaning, tone, and formatting exactly."
86
+ )
87
+ },
88
+ {
89
+ "role": "user",
90
+ "content": text
91
+ }
92
+ ],
93
+ temperature=0.1, # Low temperature for consistent, accurate translations
94
+ max_tokens=1024,
95
+ stream=False
96
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ result = (response.choices[0].message.content or "").strip()
 
 
99
 
100
+ # Clean up any accidental prefixes the LLM might add
101
+ for prefix in ["Translation:", "translation:", "Translated:", "translated:"]:
102
+ if result.startswith(prefix):
103
+ result = result[len(prefix):].strip()
104
 
105
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
 
108
  async def translate_text(
 
111
  target_lang: str,
112
  fallback: bool = True
113
  ) -> str:
114
+ """
115
+ Main translation entry point. Used by api.py for the full pipeline:
116
+ User Query (Indic) β†’ English β†’ RAG Search β†’ English Answer β†’ Indic Response
117
+
118
+ Fallback chain:
119
+ 1. Bhashini API (when approved)
120
+ 2. Groq LLM (llama-3.3-70b β€” immediate, free, high quality)
121
+ 3. Return original text (graceful degradation)
122
+ """
123
  if source_lang.lower() == target_lang.lower():
124
  return text
125
 
126
+ # Layer 1: Bhashini API (government-grade translation)
127
  if BHASHINI_USER_ID and BHASHINI_API_KEY:
128
  try:
129
  src = LANGUAGE_CODES.get(source_lang.lower().strip(), source_lang)
 
152
  print(f"βœ… Bhashini: {source_lang} β†’ {target_lang}")
153
  return result
154
  except Exception as e:
155
+ print(f"⚠️ Bhashini failed: {e}, trying Groq LLM translation")
156
 
157
+ # Layer 2: Groq LLM Translation (zero cost β€” already in our stack)
158
  try:
159
+ result = _groq_translate(text, source_lang, target_lang)
160
+ print(f"βœ… Groq LLM: {source_lang} β†’ {target_lang} | '{text[:30]}' β†’ '{result[:30]}'")
161
  return result
162
  except Exception as e:
163
+ print(f"⚠️ Groq translation failed: {e}")
164
 
165
  # Layer 3: Graceful degradation
166
  print(f"⚠️ All translation failed β€” returning original")
requirements.txt CHANGED
@@ -15,14 +15,10 @@ sentence-transformers>=4.0.0
15
  einops
16
  huggingface-hub
17
 
18
- # Upgraded to >=4.48.0 to support ModernBERT-based Ettin Reranker.
19
- # IndicTransToolkit runs successfully with this version.
20
  transformers>=4.48.0
21
- sentencepiece>=0.2.0
22
- sacremoses>=0.1.1
23
- IndicTransToolkit
24
 
25
- # --- LLM Inference ---
26
  groq
27
 
28
  # --- Database ---
@@ -44,5 +40,4 @@ pydantic-settings
44
  psutil
45
 
46
  # --- Deterministic Rules Engine (Sprint 19) ---
47
- openfisca-core>=44.0.0
48
-
 
15
  einops
16
  huggingface-hub
17
 
18
+ # transformers>=4.48.0 required for ModernBERT-based Ettin Reranker
 
19
  transformers>=4.48.0
 
 
 
20
 
21
+ # --- LLM Inference (also used for translation via Groq) ---
22
  groq
23
 
24
  # --- Database ---
 
40
  psutil
41
 
42
  # --- Deterministic Rules Engine (Sprint 19) ---
43
+ openfisca-core>=44.0.0