Spaces:
Sleeping
Sleeping
fine tuning + language quality changes
Browse files- backend/requirements-convert.txt +3 -0
- requirements.txt +1 -0
- translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc +0 -0
- translator_app/translator/services/translation_service.py +156 -60
- translator_app/translator/templates/translator/translator.html +56 -23
- translator_app/translator_project/__pycache__/settings.cpython-314.pyc +0 -0
- translator_app/translator_project/settings.py +12 -9
backend/requirements-convert.txt
CHANGED
|
@@ -2,3 +2,6 @@
|
|
| 2 |
# Runtime inference does not need torch. Install with the CPU wheel index:
|
| 3 |
# pip install -r requirements-convert.txt --extra-index-url https://download.pytorch.org/whl/cpu
|
| 4 |
torch==2.5.1
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
# Runtime inference does not need torch. Install with the CPU wheel index:
|
| 3 |
# pip install -r requirements-convert.txt --extra-index-url https://download.pytorch.org/whl/cpu
|
| 4 |
torch==2.5.1
|
| 5 |
+
transformers
|
| 6 |
+
sentencepiece
|
| 7 |
+
protobuf
|
requirements.txt
CHANGED
|
@@ -2,3 +2,4 @@ Django>=5.2
|
|
| 2 |
djangorestframework>=3.15
|
| 3 |
python-dotenv>=1.0
|
| 4 |
requests>=2.31
|
|
|
|
|
|
| 2 |
djangorestframework>=3.15
|
| 3 |
python-dotenv>=1.0
|
| 4 |
requests>=2.31
|
| 5 |
+
langdetect>=1.0.9
|
translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc
CHANGED
|
Binary files a/translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc and b/translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc differ
|
|
|
translator_app/translator/services/translation_service.py
CHANGED
|
@@ -8,8 +8,10 @@ from django.conf import settings
|
|
| 8 |
from django.utils import timezone
|
| 9 |
|
| 10 |
from ..models import TranslationCache
|
|
|
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
class TranslationService:
|
|
@@ -17,10 +19,22 @@ class TranslationService:
|
|
| 17 |
|
| 18 |
# NLLB requires its FLORES language tags. MADLAD accepts ISO language codes.
|
| 19 |
NLLB_LANGUAGE_CODES = {
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
def __init__(self):
|
|
@@ -28,106 +42,178 @@ class TranslationService:
|
|
| 28 |
|
| 29 |
def _get_supported_languages(self):
|
| 30 |
return {
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
def get_supported_languages(self):
|
| 38 |
-
return [
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
def detect_language(self, text):
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
def translate(self, text, target_language, source_language=None, model=None):
|
| 46 |
if not text or not text.strip():
|
| 47 |
-
return {
|
| 48 |
|
| 49 |
model = model or settings.TRANSLATION_DEFAULT_MODEL
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
cached = self._get_from_cache(cache_key)
|
| 53 |
if cached:
|
| 54 |
-
cached[
|
| 55 |
return cached
|
| 56 |
|
| 57 |
-
if model ==
|
| 58 |
outcome = self._translate_deepl(text, source_language, target_language)
|
| 59 |
elif model in settings.TRANSLATION_MODEL_ENGINES:
|
| 60 |
-
outcome = self._translate_model_api(
|
|
|
|
|
|
|
| 61 |
else:
|
| 62 |
-
return {
|
| 63 |
|
| 64 |
-
if outcome.get(
|
| 65 |
return outcome
|
| 66 |
|
| 67 |
-
translated_text = outcome[
|
| 68 |
-
self._save_to_cache(
|
|
|
|
|
|
|
| 69 |
return {
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
}
|
| 76 |
|
| 77 |
def _translate_model_api(self, text, source_language, target_language, model):
|
| 78 |
-
"""Call the FastAPI model server deployed as a Hugging Face Space."""
|
| 79 |
if not settings.MODEL_API_URL:
|
| 80 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
try:
|
| 82 |
source = self.NLLB_LANGUAGE_CODES[source_language]
|
| 83 |
target = self.NLLB_LANGUAGE_CODES[target_language]
|
| 84 |
except KeyError:
|
| 85 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
try:
|
| 88 |
response = requests.post(
|
| 89 |
settings.MODEL_API_URL,
|
| 90 |
-
json=
|
| 91 |
-
'engine': settings.TRANSLATION_MODEL_ENGINES[model]},
|
| 92 |
timeout=60,
|
| 93 |
)
|
|
|
|
| 94 |
if response.status_code != 200:
|
| 95 |
-
logger.warning(
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
if not translated_text:
|
| 99 |
-
return {
|
| 100 |
-
return {
|
|
|
|
| 101 |
except requests.exceptions.Timeout:
|
| 102 |
-
return {
|
| 103 |
except requests.exceptions.ConnectionError:
|
| 104 |
-
return {
|
| 105 |
except (ValueError, requests.RequestException) as error:
|
| 106 |
-
logger.exception(
|
| 107 |
-
return {
|
| 108 |
|
| 109 |
def _translate_deepl(self, text, source_language, target_language):
|
| 110 |
if not settings.TRANSLATION_API_KEY:
|
| 111 |
-
return {
|
| 112 |
try:
|
| 113 |
response = requests.post(
|
| 114 |
settings.TRANSLATION_API_URL,
|
| 115 |
-
headers={
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
timeout=30,
|
| 118 |
)
|
| 119 |
if response.status_code != 200:
|
| 120 |
-
return {
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
| 123 |
except (KeyError, IndexError, ValueError, requests.RequestException) as error:
|
| 124 |
-
logger.exception(
|
| 125 |
-
return {
|
|
|
|
|
|
|
| 126 |
|
| 127 |
@staticmethod
|
| 128 |
def _generate_cache_key(text, source_language, target_language, model):
|
| 129 |
-
content = f
|
| 130 |
-
return hashlib.sha256(content.encode(
|
| 131 |
|
| 132 |
@staticmethod
|
| 133 |
def _get_from_cache(cache_key):
|
|
@@ -137,21 +223,31 @@ class TranslationService:
|
|
| 137 |
return None
|
| 138 |
entry.access_count += 1
|
| 139 |
entry.last_accessed = timezone.now()
|
| 140 |
-
entry.save(update_fields=[
|
| 141 |
-
return {
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
except Exception:
|
| 145 |
-
logger.exception(
|
| 146 |
return None
|
| 147 |
|
| 148 |
@staticmethod
|
| 149 |
-
def _save_to_cache(
|
|
|
|
|
|
|
| 150 |
try:
|
| 151 |
TranslationCache.objects.update_or_create(
|
| 152 |
source_text_hash=cache_key,
|
| 153 |
-
defaults={
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
)
|
| 156 |
except Exception:
|
| 157 |
-
logger.exception(
|
|
|
|
| 8 |
from django.utils import timezone
|
| 9 |
|
| 10 |
from ..models import TranslationCache
|
| 11 |
+
from langdetect import detect, DetectorFactory, LangDetectException
|
| 12 |
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
+
DetectorFactory.seed = 0
|
| 15 |
|
| 16 |
|
| 17 |
class TranslationService:
|
|
|
|
| 19 |
|
| 20 |
# NLLB requires its FLORES language tags. MADLAD accepts ISO language codes.
|
| 21 |
NLLB_LANGUAGE_CODES = {
|
| 22 |
+
"en": "eng_Latn",
|
| 23 |
+
"es": "spa_Latn",
|
| 24 |
+
"fr": "fra_Latn",
|
| 25 |
+
"de": "deu_Latn",
|
| 26 |
+
"it": "ita_Latn",
|
| 27 |
+
"pt": "por_Latn",
|
| 28 |
+
"ru": "rus_Cyrl",
|
| 29 |
+
"zh": "zho_Hans",
|
| 30 |
+
"ja": "jpn_Jpan",
|
| 31 |
+
"ko": "kor_Hang",
|
| 32 |
+
"ar": "arb_Arab",
|
| 33 |
+
"hi": "hin_Deva",
|
| 34 |
+
"sw": "swh_Latn",
|
| 35 |
+
"yo": "yor_Latn",
|
| 36 |
+
"ha": "hau_Latn",
|
| 37 |
+
"ig": "ibo_Latn",
|
| 38 |
}
|
| 39 |
|
| 40 |
def __init__(self):
|
|
|
|
| 42 |
|
| 43 |
def _get_supported_languages(self):
|
| 44 |
return {
|
| 45 |
+
"en": "English",
|
| 46 |
+
"es": "Spanish",
|
| 47 |
+
"fr": "French",
|
| 48 |
+
"de": "German",
|
| 49 |
+
"it": "Italian",
|
| 50 |
+
"pt": "Portuguese",
|
| 51 |
+
"ru": "Russian",
|
| 52 |
+
"zh": "Chinese",
|
| 53 |
+
"ja": "Japanese",
|
| 54 |
+
"ko": "Korean",
|
| 55 |
+
"ar": "Arabic",
|
| 56 |
+
"hi": "Hindi",
|
| 57 |
+
"sw": "Swahili",
|
| 58 |
+
"yo": "Yoruba",
|
| 59 |
+
"ha": "Hausa",
|
| 60 |
+
"ig": "Igbo",
|
| 61 |
}
|
| 62 |
|
| 63 |
def get_supported_languages(self):
|
| 64 |
+
return [
|
| 65 |
+
{"code": code, "name": name}
|
| 66 |
+
for code, name in self.supported_languages.items()
|
| 67 |
+
]
|
| 68 |
|
| 69 |
def detect_language(self, text):
|
| 70 |
+
try:
|
| 71 |
+
code = detect(text)
|
| 72 |
+
except LangDetectException:
|
| 73 |
+
code = "en"
|
| 74 |
+
if code not in self.supported_languages:
|
| 75 |
+
code = "en" # fall back if detected language isn't one you support
|
| 76 |
+
return {
|
| 77 |
+
"code": code,
|
| 78 |
+
"name": self.supported_languages.get(code, code.upper()),
|
| 79 |
+
"confidence": 0.8 if code in self.supported_languages else 0.3,
|
| 80 |
+
}
|
| 81 |
|
| 82 |
def translate(self, text, target_language, source_language=None, model=None):
|
| 83 |
if not text or not text.strip():
|
| 84 |
+
return {"error": "Text cannot be empty"}
|
| 85 |
|
| 86 |
model = model or settings.TRANSLATION_DEFAULT_MODEL
|
| 87 |
+
if model == "madlad":
|
| 88 |
+
source_language = None # MADLAD auto-detects; any provided value is ignored
|
| 89 |
+
source_language = source_language or self.detect_language(text)["code"]
|
| 90 |
+
cache_key = self._generate_cache_key(
|
| 91 |
+
text, source_language, target_language, model
|
| 92 |
+
)
|
| 93 |
cached = self._get_from_cache(cache_key)
|
| 94 |
if cached:
|
| 95 |
+
cached["model"] = model
|
| 96 |
return cached
|
| 97 |
|
| 98 |
+
if model == "deepl_fallback":
|
| 99 |
outcome = self._translate_deepl(text, source_language, target_language)
|
| 100 |
elif model in settings.TRANSLATION_MODEL_ENGINES:
|
| 101 |
+
outcome = self._translate_model_api(
|
| 102 |
+
text, source_language, target_language, model
|
| 103 |
+
)
|
| 104 |
else:
|
| 105 |
+
return {"error": "The selected translation model is unavailable."}
|
| 106 |
|
| 107 |
+
if outcome.get("error"):
|
| 108 |
return outcome
|
| 109 |
|
| 110 |
+
translated_text = outcome["translated_text"]
|
| 111 |
+
self._save_to_cache(
|
| 112 |
+
cache_key, text, translated_text, source_language, target_language
|
| 113 |
+
)
|
| 114 |
return {
|
| 115 |
+
"source_text": text,
|
| 116 |
+
"translated_text": translated_text,
|
| 117 |
+
"source_language": source_language,
|
| 118 |
+
"target_language": target_language,
|
| 119 |
+
"model": model,
|
| 120 |
}
|
| 121 |
|
| 122 |
def _translate_model_api(self, text, source_language, target_language, model):
|
|
|
|
| 123 |
if not settings.MODEL_API_URL:
|
| 124 |
+
return {
|
| 125 |
+
"error": "ML translation is not configured. Add MODEL_API_URL to your environment."
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
if isinstance(settings.TRANSLATION_MODEL_ENGINES, dict):
|
| 129 |
+
engine_name = settings.TRANSLATION_MODEL_ENGINES.get(model, model)
|
| 130 |
+
else:
|
| 131 |
+
engine_name = model
|
| 132 |
+
|
| 133 |
+
# Every local engine (NLLB, MADLAD) speaks FLORES-200 codes on the wire.
|
| 134 |
try:
|
| 135 |
source = self.NLLB_LANGUAGE_CODES[source_language]
|
| 136 |
target = self.NLLB_LANGUAGE_CODES[target_language]
|
| 137 |
except KeyError:
|
| 138 |
+
return {
|
| 139 |
+
"error": "The model service does not support one of these languages."
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
payload = {
|
| 143 |
+
"text": text,
|
| 144 |
+
"source": source,
|
| 145 |
+
"target": target,
|
| 146 |
+
"engine": engine_name,
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
# DEBUGGING: Print exactly what is being sent to the console
|
| 150 |
+
print(
|
| 151 |
+
f"\n--- API PAYLOAD DEBUG ---\nURL: {settings.MODEL_API_URL}\nPayload: {payload}\n-------------------------\n"
|
| 152 |
+
)
|
| 153 |
+
logger.info(f"Sending translation payload: {payload}")
|
| 154 |
|
| 155 |
try:
|
| 156 |
response = requests.post(
|
| 157 |
settings.MODEL_API_URL,
|
| 158 |
+
json=payload,
|
|
|
|
| 159 |
timeout=60,
|
| 160 |
)
|
| 161 |
+
|
| 162 |
if response.status_code != 200:
|
| 163 |
+
logger.warning(
|
| 164 |
+
"Model API translation failed: %s %s",
|
| 165 |
+
response.status_code,
|
| 166 |
+
response.text,
|
| 167 |
+
)
|
| 168 |
+
return {
|
| 169 |
+
"error": f"The ML service failed with status {response.status_code}."
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
translated_text = response.json().get("translation")
|
| 173 |
if not translated_text:
|
| 174 |
+
return {"error": "The ML model returned no translation."}
|
| 175 |
+
return {"translated_text": translated_text}
|
| 176 |
+
|
| 177 |
except requests.exceptions.Timeout:
|
| 178 |
+
return {"error": "The ML translation request timed out."}
|
| 179 |
except requests.exceptions.ConnectionError:
|
| 180 |
+
return {"error": "Could not connect to the ML translation service."}
|
| 181 |
except (ValueError, requests.RequestException) as error:
|
| 182 |
+
logger.exception("Model API translation failed: %s", error)
|
| 183 |
+
return {"error": "The ML translation service returned an invalid response."}
|
| 184 |
|
| 185 |
def _translate_deepl(self, text, source_language, target_language):
|
| 186 |
if not settings.TRANSLATION_API_KEY:
|
| 187 |
+
return {"error": "Traditional fallback is not configured."}
|
| 188 |
try:
|
| 189 |
response = requests.post(
|
| 190 |
settings.TRANSLATION_API_URL,
|
| 191 |
+
headers={
|
| 192 |
+
"Authorization": f"DeepL-Auth-Key {settings.TRANSLATION_API_KEY}"
|
| 193 |
+
},
|
| 194 |
+
json={
|
| 195 |
+
"text": [text],
|
| 196 |
+
"source_lang": source_language.upper(),
|
| 197 |
+
"target_lang": target_language.upper(),
|
| 198 |
+
},
|
| 199 |
timeout=30,
|
| 200 |
)
|
| 201 |
if response.status_code != 200:
|
| 202 |
+
return {
|
| 203 |
+
"error": "The fallback translation service could not complete this request."
|
| 204 |
+
}
|
| 205 |
+
translated_text = response.json()["translations"][0]["text"]
|
| 206 |
+
return {"translated_text": translated_text}
|
| 207 |
except (KeyError, IndexError, ValueError, requests.RequestException) as error:
|
| 208 |
+
logger.exception("Fallback translation failed: %s", error)
|
| 209 |
+
return {
|
| 210 |
+
"error": "The fallback translation service returned an invalid response."
|
| 211 |
+
}
|
| 212 |
|
| 213 |
@staticmethod
|
| 214 |
def _generate_cache_key(text, source_language, target_language, model):
|
| 215 |
+
content = f"{text}|{source_language}|{target_language}|{model}"
|
| 216 |
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
| 217 |
|
| 218 |
@staticmethod
|
| 219 |
def _get_from_cache(cache_key):
|
|
|
|
| 223 |
return None
|
| 224 |
entry.access_count += 1
|
| 225 |
entry.last_accessed = timezone.now()
|
| 226 |
+
entry.save(update_fields=["access_count", "last_accessed"])
|
| 227 |
+
return {
|
| 228 |
+
"source_text": entry.source_text,
|
| 229 |
+
"translated_text": entry.translated_text,
|
| 230 |
+
"source_language": entry.source_language_code,
|
| 231 |
+
"target_language": entry.target_language_code,
|
| 232 |
+
"from_cache": True,
|
| 233 |
+
}
|
| 234 |
except Exception:
|
| 235 |
+
logger.exception("Translation cache lookup failed")
|
| 236 |
return None
|
| 237 |
|
| 238 |
@staticmethod
|
| 239 |
+
def _save_to_cache(
|
| 240 |
+
cache_key, source_text, translated_text, source_language, target_language
|
| 241 |
+
):
|
| 242 |
try:
|
| 243 |
TranslationCache.objects.update_or_create(
|
| 244 |
source_text_hash=cache_key,
|
| 245 |
+
defaults={
|
| 246 |
+
"source_text": source_text,
|
| 247 |
+
"translated_text": translated_text,
|
| 248 |
+
"source_language_code": source_language,
|
| 249 |
+
"target_language_code": target_language,
|
| 250 |
+
},
|
| 251 |
)
|
| 252 |
except Exception:
|
| 253 |
+
logger.exception("Translation cache save failed")
|
translator_app/translator/templates/translator/translator.html
CHANGED
|
@@ -4,44 +4,77 @@
|
|
| 4 |
<form id="translation-form">
|
| 5 |
{% csrf_token %}
|
| 6 |
<div class="flex flex-wrap justify-center items-center gap-3 mb-5">
|
| 7 |
-
<select id="source-language" aria-label="Source language"
|
| 8 |
-
|
|
|
|
|
|
|
| 9 |
</select>
|
| 10 |
-
<button id="swap-languages" type="button" title="Swap languages"
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
| 13 |
</select>
|
| 14 |
-
<label
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
| 17 |
</select>
|
| 18 |
</label>
|
| 19 |
</div>
|
| 20 |
<div class="rounded-2xl border border-stone-200 bg-white p-5 md:p-7 shadow-xl shadow-stone-200/40 md:flex md:gap-7">
|
| 21 |
<section class="flex min-h-72 flex-1 flex-col">
|
| 22 |
<label for="source-text" class="mb-3 text-xs font-bold uppercase tracking-widest text-stone-400">Source</label>
|
| 23 |
-
<textarea id="source-text" maxlength="5000"
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</section>
|
| 26 |
<div class="my-5 hidden w-px bg-stone-100 md:block"></div>
|
| 27 |
<section class="relative flex min-h-72 flex-1 flex-col">
|
| 28 |
-
<div class="mb-3 flex items-center justify-between"><span
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
<p id="result-meta" class="mt-3 text-xs text-stone-400"></p>
|
| 31 |
</section>
|
| 32 |
</div>
|
| 33 |
-
<div class="mt-6 flex flex-col items-center gap-3"><button id="translate-button"
|
|
|
|
|
|
|
|
|
|
| 34 |
</form>
|
| 35 |
-
<div class="mt-12 text-center"><a href="{% url 'translator:history' %}"
|
|
|
|
| 36 |
</main>
|
| 37 |
<script>
|
| 38 |
-
const form=document.querySelector('#translation-form'), source=document.querySelector('#source-text'), result=document.querySelector('#translated-text'), message=document.querySelector('#form-message'), button=document.querySelector('#translate-button'), count=document.querySelector('#character-count');
|
| 39 |
-
const csrf=document.querySelector('[name=csrfmiddlewaretoken]').value;
|
| 40 |
-
|
| 41 |
-
document.querySelector('#
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
</script>
|
| 47 |
-
{% endblock %}
|
|
|
|
| 4 |
<form id="translation-form">
|
| 5 |
{% csrf_token %}
|
| 6 |
<div class="flex flex-wrap justify-center items-center gap-3 mb-5">
|
| 7 |
+
<select id="source-language" aria-label="Source language"
|
| 8 |
+
class="rounded-full border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-primary shadow-sm focus:border-primary focus:ring-primary">
|
| 9 |
+
<option value="">Detect language</option>{% for language in languages %}<option value="{{ language.code }}" {%
|
| 10 |
+
if language.code=='en' %} selected{% endif %}>{{ language.name }}</option>{% endfor %}
|
| 11 |
</select>
|
| 12 |
+
<button id="swap-languages" type="button" title="Swap languages"
|
| 13 |
+
class="rounded-full bg-sand px-3 py-2.5 text-primary transition hover:rotate-180">⇄</button>
|
| 14 |
+
<select id="target-language" aria-label="Target language"
|
| 15 |
+
class="rounded-full border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-primary shadow-sm focus:border-primary focus:ring-primary">
|
| 16 |
+
{% for language in languages %}<option value="{{ language.code }}" {% if language.code=='sw' %} selected{% endif
|
| 17 |
+
%}>{{ language.name }}</option>{% endfor %}
|
| 18 |
</select>
|
| 19 |
+
<label
|
| 20 |
+
class="flex items-center gap-2 rounded-full border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-600 shadow-sm">Model
|
| 21 |
+
<select id="translation-model" aria-label="Translation model"
|
| 22 |
+
class="border-0 bg-transparent py-0 pl-1 pr-7 text-primary focus:ring-0">
|
| 23 |
+
{% for value, label in models %}<option value="{{ value }}" {% if value==default_model %} selected{% endif %}>
|
| 24 |
+
{{ label }}</option>{% endfor %}
|
| 25 |
</select>
|
| 26 |
</label>
|
| 27 |
</div>
|
| 28 |
<div class="rounded-2xl border border-stone-200 bg-white p-5 md:p-7 shadow-xl shadow-stone-200/40 md:flex md:gap-7">
|
| 29 |
<section class="flex min-h-72 flex-1 flex-col">
|
| 30 |
<label for="source-text" class="mb-3 text-xs font-bold uppercase tracking-widest text-stone-400">Source</label>
|
| 31 |
+
<textarea id="source-text" maxlength="5000"
|
| 32 |
+
class="min-h-52 w-full flex-1 resize-none border-0 p-0 font-display text-2xl leading-relaxed text-primary placeholder:text-stone-300 focus:ring-0"
|
| 33 |
+
placeholder="Enter text to translate..."></textarea>
|
| 34 |
+
<div class="mt-3 flex justify-between border-t border-stone-100 pt-3 text-xs text-stone-400"><button
|
| 35 |
+
id="clear-source" type="button" class="font-semibold text-primary hover:underline">Clear</button><span
|
| 36 |
+
id="character-count">0 / 5000 characters</span></div>
|
| 37 |
</section>
|
| 38 |
<div class="my-5 hidden w-px bg-stone-100 md:block"></div>
|
| 39 |
<section class="relative flex min-h-72 flex-1 flex-col">
|
| 40 |
+
<div class="mb-3 flex items-center justify-between"><span
|
| 41 |
+
class="text-xs font-bold uppercase tracking-widest text-stone-400">Translation</span><button
|
| 42 |
+
id="copy-result" type="button"
|
| 43 |
+
class="hidden rounded-full px-3 py-1 text-xs font-bold text-primary hover:bg-mist">Copy</button></div>
|
| 44 |
+
<p id="translated-text" aria-live="polite"
|
| 45 |
+
class="relative flex-1 whitespace-pre-wrap font-display text-2xl leading-relaxed text-primary"><span
|
| 46 |
+
class="text-stone-300">Your translation will appear here.</span></p>
|
| 47 |
<p id="result-meta" class="mt-3 text-xs text-stone-400"></p>
|
| 48 |
</section>
|
| 49 |
</div>
|
| 50 |
+
<div class="mt-6 flex flex-col items-center gap-3"><button id="translate-button"
|
| 51 |
+
class="rounded-full bg-primary px-10 py-4 font-display text-lg font-bold uppercase tracking-wider text-white shadow-lg shadow-primary/30 transition hover:scale-105 disabled:cursor-wait disabled:opacity-70">Translate</button>
|
| 52 |
+
<p id="form-message" role="alert" class="min-h-5 text-sm text-red-700"></p>
|
| 53 |
+
</div>
|
| 54 |
</form>
|
| 55 |
+
<div class="mt-12 text-center"><a href="{% url 'translator:history' %}"
|
| 56 |
+
class="text-sm font-bold text-primary underline underline-offset-4 hover:text-stone-700">View history</a></div>
|
| 57 |
</main>
|
| 58 |
<script>
|
| 59 |
+
const form = document.querySelector('#translation-form'), source = document.querySelector('#source-text'), result = document.querySelector('#translated-text'), message = document.querySelector('#form-message'), button = document.querySelector('#translate-button'), count = document.querySelector('#character-count');
|
| 60 |
+
const csrf = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
| 61 |
+
const modelSelect = document.querySelector('#translation-model');
|
| 62 |
+
const sourceSelect = document.querySelector('#source-language');
|
| 63 |
+
|
| 64 |
+
function syncSourceForModel() {
|
| 65 |
+
const isMadlad = modelSelect.value === 'madlad';
|
| 66 |
+
sourceSelect.disabled = isMadlad;
|
| 67 |
+
if (isMadlad) sourceSelect.value = '';
|
| 68 |
+
}
|
| 69 |
+
modelSelect.addEventListener('change', syncSourceForModel);
|
| 70 |
+
syncSourceForModel(); // run once on load in case MADLAD is the default
|
| 71 |
+
source.addEventListener('input', () => count.textContent = `${source.value.length} / 5000 characters`);
|
| 72 |
+
document.querySelector('#clear-source').onclick = () => { source.value = ''; source.dispatchEvent(new Event('input')); source.focus() };
|
| 73 |
+
document.querySelector('#swap-languages').onclick = () => { const a = document.querySelector('#source-language'), b = document.querySelector('#target-language'); if (a.value) { [a.value, b.value] = [b.value, a.value] } };
|
| 74 |
+
document.querySelector('#copy-result').onclick = async () => { await navigator.clipboard.writeText(result.textContent); document.querySelector('#copy-result').textContent = 'Copied'; setTimeout(() => document.querySelector('#copy-result').textContent = 'Copy', 1200) };
|
| 75 |
+
form.addEventListener('submit', async e => {
|
| 76 |
+
e.preventDefault(); message.textContent = ''; button.disabled = true; button.textContent = 'Translating…'; result.classList.add('quiet-loading');
|
| 77 |
+
try { const response = await fetch("{% url 'translator:translate_api' %}", { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrf }, body: JSON.stringify({ text: source.value, source_language: document.querySelector('#source-language').value, target_language: document.querySelector('#target-language').value, model: document.querySelector('#translation-model').value }) }); const data = await response.json(); if (!response.ok) throw Error(data.error || 'Translation could not be completed.'); result.textContent = data.translated_text; document.querySelector('#result-meta').textContent = `${data.model_label || 'Translation'} · saved to history`; document.querySelector('#copy-result').classList.remove('hidden') } catch (error) { message.textContent = error.message; result.innerHTML = '<span class="text-stone-300">Your translation will appear here.</span>' } finally { button.disabled = false; button.textContent = 'Translate'; result.classList.remove('quiet-loading') }
|
| 78 |
+
});
|
| 79 |
</script>
|
| 80 |
+
{% endblock %}
|
translator_app/translator_project/__pycache__/settings.cpython-314.pyc
CHANGED
|
Binary files a/translator_app/translator_project/__pycache__/settings.cpython-314.pyc and b/translator_app/translator_project/__pycache__/settings.cpython-314.pyc differ
|
|
|
translator_app/translator_project/settings.py
CHANGED
|
@@ -12,7 +12,10 @@ SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "your-secret-key-here")
|
|
| 12 |
|
| 13 |
DEBUG = True
|
| 14 |
|
| 15 |
-
ALLOWED_HOSTS = [
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
# Application definition
|
| 18 |
INSTALLED_APPS = [
|
|
@@ -101,16 +104,16 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
| 101 |
# ML translation service. Point this at the deployed Hugging Face Space, e.g.
|
| 102 |
# https://<account>-<space>.hf.space/api/translate
|
| 103 |
MODEL_API_URL = os.getenv("MODEL_API_URL")
|
| 104 |
-
TRANSLATION_DEFAULT_MODEL = os.getenv("TRANSLATION_DEFAULT_MODEL", "
|
| 105 |
TRANSLATION_MODELS = (
|
| 106 |
-
("
|
| 107 |
-
("
|
| 108 |
-
("
|
| 109 |
)
|
| 110 |
TRANSLATION_MODEL_ENGINES = {
|
| 111 |
-
"
|
| 112 |
-
"
|
| 113 |
-
"
|
| 114 |
}
|
| 115 |
|
| 116 |
# Optional traditional-provider fallback. Add this option to TRANSLATION_MODELS
|
|
@@ -120,6 +123,6 @@ TRANSLATION_API_URL = os.getenv(
|
|
| 120 |
"TRANSLATION_API_URL", "https://api-free.deepl.com/v2/translate"
|
| 121 |
)
|
| 122 |
if TRANSLATION_API_KEY:
|
| 123 |
-
TRANSLATION_MODELS += (("deepl_fallback", "DeepL
|
| 124 |
# Or for Google Translate:
|
| 125 |
# TRANSLATION_API_URL = 'https://translation.googleapis.com/language/translate/v2'
|
|
|
|
| 12 |
|
| 13 |
DEBUG = True
|
| 14 |
|
| 15 |
+
ALLOWED_HOSTS = [
|
| 16 |
+
"localhost",
|
| 17 |
+
"127.0.0.1",
|
| 18 |
+
]
|
| 19 |
|
| 20 |
# Application definition
|
| 21 |
INSTALLED_APPS = [
|
|
|
|
| 104 |
# ML translation service. Point this at the deployed Hugging Face Space, e.g.
|
| 105 |
# https://<account>-<space>.hf.space/api/translate
|
| 106 |
MODEL_API_URL = os.getenv("MODEL_API_URL")
|
| 107 |
+
TRANSLATION_DEFAULT_MODEL = os.getenv("TRANSLATION_DEFAULT_MODEL", "nllb")
|
| 108 |
TRANSLATION_MODELS = (
|
| 109 |
+
("nllb", "NLLB-200 (600M · Fast)"),
|
| 110 |
+
("nllb_1_3b", "NLLB-200 (1.3B · Accurate)"),
|
| 111 |
+
("madlad", "MADLAD-400 (3B · Large)"),
|
| 112 |
)
|
| 113 |
TRANSLATION_MODEL_ENGINES = {
|
| 114 |
+
"nllb": "nllb",
|
| 115 |
+
"nllb_1_3b": "nllb_1_3b",
|
| 116 |
+
"madlad": "madlad",
|
| 117 |
}
|
| 118 |
|
| 119 |
# Optional traditional-provider fallback. Add this option to TRANSLATION_MODELS
|
|
|
|
| 123 |
"TRANSLATION_API_URL", "https://api-free.deepl.com/v2/translate"
|
| 124 |
)
|
| 125 |
if TRANSLATION_API_KEY:
|
| 126 |
+
TRANSLATION_MODELS += (("deepl_fallback", "DeepL"),)
|
| 127 |
# Or for Google Translate:
|
| 128 |
# TRANSLATION_API_URL = 'https://translation.googleapis.com/language/translate/v2'
|