Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +8 -25
- api.py +1 -2
- bhashini.py +94 -220
- 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 (
|
| 13 |
-
# build-essential: Required for
|
| 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 |
-
#
|
| 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 |
-
#
|
| 49 |
-
#
|
| 50 |
-
#
|
| 51 |
-
|
| 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
|
| 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 |
-
"
|
| 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
|
| 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 |
-
#
|
| 90 |
-
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 100 |
"""
|
| 101 |
-
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 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 |
-
|
|
|
|
| 117 |
|
| 118 |
-
|
| 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 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 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 |
-
|
| 161 |
-
- Indic β English (using indictrans2-indic-en-dist-200M)
|
| 162 |
-
- English β Indic (using indictrans2-en-indic-dist-200M)
|
| 163 |
|
| 164 |
-
|
| 165 |
-
"""
|
| 166 |
-
|
| 167 |
-
|
| 168 |
|
| 169 |
-
|
| 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
|
| 282 |
|
| 283 |
-
# Layer 2:
|
| 284 |
try:
|
| 285 |
-
result =
|
| 286 |
-
print(f"β
|
| 287 |
return result
|
| 288 |
except Exception as e:
|
| 289 |
-
print(f"β οΈ
|
| 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 |
-
#
|
| 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
|
|
|