arindae commited on
Commit
2e4550c
·
1 Parent(s): c204ebd

fine tuning + language quality changes

Browse files
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
- 'en': 'eng_Latn', 'es': 'spa_Latn', 'fr': 'fra_Latn', 'de': 'deu_Latn',
21
- 'it': 'ita_Latn', 'pt': 'por_Latn', 'ru': 'rus_Cyrl', 'zh': 'zho_Hans',
22
- 'ja': 'jpn_Jpan', 'ko': 'kor_Hang', 'ar': 'arb_Arab', 'hi': 'hin_Deva',
23
- 'sw': 'swh_Latn', 'yo': 'yor_Latn', 'ha': 'hau_Latn', 'ig': 'ibo_Latn',
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
  def __init__(self):
@@ -28,106 +42,178 @@ class TranslationService:
28
 
29
  def _get_supported_languages(self):
30
  return {
31
- 'en': 'English', 'es': 'Spanish', 'fr': 'French', 'de': 'German',
32
- 'it': 'Italian', 'pt': 'Portuguese', 'ru': 'Russian', 'zh': 'Chinese',
33
- 'ja': 'Japanese', 'ko': 'Korean', 'ar': 'Arabic', 'hi': 'Hindi',
34
- 'sw': 'Swahili', 'yo': 'Yoruba', 'ha': 'Hausa', 'ig': 'Igbo',
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
  def get_supported_languages(self):
38
- return [{'code': code, 'name': name} for code, name in self.supported_languages.items()]
 
 
 
39
 
40
  def detect_language(self, text):
41
- # Deliberately local and deterministic until the ML team supplies a detector.
42
- code = 'en' if all(ord(char) < 128 for char in text) else 'en'
43
- return {'code': code, 'name': self.supported_languages[code], 'confidence': 0.6}
 
 
 
 
 
 
 
 
44
 
45
  def translate(self, text, target_language, source_language=None, model=None):
46
  if not text or not text.strip():
47
- return {'error': 'Text cannot be empty'}
48
 
49
  model = model or settings.TRANSLATION_DEFAULT_MODEL
50
- source_language = source_language or self.detect_language(text)['code']
51
- cache_key = self._generate_cache_key(text, source_language, target_language, model)
 
 
 
 
52
  cached = self._get_from_cache(cache_key)
53
  if cached:
54
- cached['model'] = model
55
  return cached
56
 
57
- if model == 'deepl_fallback':
58
  outcome = self._translate_deepl(text, source_language, target_language)
59
  elif model in settings.TRANSLATION_MODEL_ENGINES:
60
- outcome = self._translate_model_api(text, source_language, target_language, model)
 
 
61
  else:
62
- return {'error': 'The selected translation model is unavailable.'}
63
 
64
- if outcome.get('error'):
65
  return outcome
66
 
67
- translated_text = outcome['translated_text']
68
- self._save_to_cache(cache_key, text, translated_text, source_language, target_language)
 
 
69
  return {
70
- 'source_text': text,
71
- 'translated_text': translated_text,
72
- 'source_language': source_language,
73
- 'target_language': target_language,
74
- 'model': model,
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 {'error': 'ML translation is not configured. Add MODEL_API_URL to your environment.'}
 
 
 
 
 
 
 
 
 
81
  try:
82
  source = self.NLLB_LANGUAGE_CODES[source_language]
83
  target = self.NLLB_LANGUAGE_CODES[target_language]
84
  except KeyError:
85
- return {'error': 'The model service does not support one of these languages.'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  try:
88
  response = requests.post(
89
  settings.MODEL_API_URL,
90
- json={'text': text, 'source': source, 'target': target,
91
- 'engine': settings.TRANSLATION_MODEL_ENGINES[model]},
92
  timeout=60,
93
  )
 
94
  if response.status_code != 200:
95
- logger.warning('Model API translation failed: %s %s', response.status_code, response.text)
96
- return {'error': 'The ML translation service could not complete this request.'}
97
- translated_text = response.json().get('translation')
 
 
 
 
 
 
 
98
  if not translated_text:
99
- return {'error': 'The ML model returned no translation.'}
100
- return {'translated_text': translated_text}
 
101
  except requests.exceptions.Timeout:
102
- return {'error': 'The ML translation request timed out.'}
103
  except requests.exceptions.ConnectionError:
104
- return {'error': 'Could not connect to the ML translation service.'}
105
  except (ValueError, requests.RequestException) as error:
106
- logger.exception('Model API translation failed: %s', error)
107
- return {'error': 'The ML translation service returned an invalid response.'}
108
 
109
  def _translate_deepl(self, text, source_language, target_language):
110
  if not settings.TRANSLATION_API_KEY:
111
- return {'error': 'Traditional fallback is not configured.'}
112
  try:
113
  response = requests.post(
114
  settings.TRANSLATION_API_URL,
115
- headers={'Authorization': f'DeepL-Auth-Key {settings.TRANSLATION_API_KEY}'},
116
- json={'text': [text], 'source_lang': source_language.upper(), 'target_lang': target_language.upper()},
 
 
 
 
 
 
117
  timeout=30,
118
  )
119
  if response.status_code != 200:
120
- return {'error': 'The fallback translation service could not complete this request.'}
121
- translated_text = response.json()['translations'][0]['text']
122
- return {'translated_text': translated_text}
 
 
123
  except (KeyError, IndexError, ValueError, requests.RequestException) as error:
124
- logger.exception('Fallback translation failed: %s', error)
125
- return {'error': 'The fallback translation service returned an invalid response.'}
 
 
126
 
127
  @staticmethod
128
  def _generate_cache_key(text, source_language, target_language, model):
129
- content = f'{text}|{source_language}|{target_language}|{model}'
130
- return hashlib.sha256(content.encode('utf-8')).hexdigest()
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=['access_count', 'last_accessed'])
141
- return {'source_text': entry.source_text, 'translated_text': entry.translated_text,
142
- 'source_language': entry.source_language_code, 'target_language': entry.target_language_code,
143
- 'from_cache': True}
 
 
 
 
144
  except Exception:
145
- logger.exception('Translation cache lookup failed')
146
  return None
147
 
148
  @staticmethod
149
- def _save_to_cache(cache_key, source_text, translated_text, source_language, target_language):
 
 
150
  try:
151
  TranslationCache.objects.update_or_create(
152
  source_text_hash=cache_key,
153
- defaults={'source_text': source_text, 'translated_text': translated_text,
154
- 'source_language_code': source_language, 'target_language_code': target_language},
 
 
 
 
155
  )
156
  except Exception:
157
- logger.exception('Translation cache save failed')
 
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" 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">
8
- <option value="">Detect language</option>{% for language in languages %}<option value="{{ language.code }}"{% if language.code == 'en' %} selected{% endif %}>{{ language.name }}</option>{% endfor %}
 
 
9
  </select>
10
- <button id="swap-languages" type="button" title="Swap languages" class="rounded-full bg-sand px-3 py-2.5 text-primary transition hover:rotate-180">⇄</button>
11
- <select id="target-language" aria-label="Target language" 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">
12
- {% for language in languages %}<option value="{{ language.code }}"{% if language.code == 'sw' %} selected{% endif %}>{{ language.name }}</option>{% endfor %}
 
 
 
13
  </select>
14
- <label 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
15
- <select id="translation-model" aria-label="Translation model" class="border-0 bg-transparent py-0 pl-1 pr-7 text-primary focus:ring-0">
16
- {% for value, label in models %}<option value="{{ value }}"{% if value == default_model %} selected{% endif %}>{{ label }}</option>{% endfor %}
 
 
 
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" 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" placeholder="Enter text to translate..."></textarea>
24
- <div class="mt-3 flex justify-between border-t border-stone-100 pt-3 text-xs text-stone-400"><button id="clear-source" type="button" class="font-semibold text-primary hover:underline">Clear</button><span id="character-count">0 / 5000 characters</span></div>
 
 
 
 
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 class="text-xs font-bold uppercase tracking-widest text-stone-400">Translation</span><button id="copy-result" type="button" class="hidden rounded-full px-3 py-1 text-xs font-bold text-primary hover:bg-mist">Copy</button></div>
29
- <p id="translated-text" aria-live="polite" class="relative flex-1 whitespace-pre-wrap font-display text-2xl leading-relaxed text-primary"><span class="text-stone-300">Your translation will appear here.</span></p>
 
 
 
 
 
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" 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><p id="form-message" role="alert" class="min-h-5 text-sm text-red-700"></p></div>
 
 
 
34
  </form>
35
- <div class="mt-12 text-center"><a href="{% url 'translator:history' %}" class="text-sm font-bold text-primary underline underline-offset-4 hover:text-stone-700">View history</a></div>
 
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
- source.addEventListener('input',()=>count.textContent=`${source.value.length} / 5000 characters`);
41
- document.querySelector('#clear-source').onclick=()=>{source.value='';source.dispatchEvent(new Event('input'));source.focus()};
42
- 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]}};
43
- 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)};
44
- form.addEventListener('submit',async e=>{e.preventDefault();message.textContent='';button.disabled=true;button.textContent='Translating…';result.classList.add('quiet-loading');
45
- 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')}});
 
 
 
 
 
 
 
 
 
 
 
 
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 = ["localhost", "127.0.0.1"]
 
 
 
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", "nllb_600m")
105
  TRANSLATION_MODELS = (
106
- ("nllb_600m", "NLLB-200 (600M · Fast)"),
107
- ("nllb_1.3b", "NLLB-200 (1.3B · Accurate)"),
108
- ("madlad_3b", "MADLAD-400 (3B · Large)"),
109
  )
110
  TRANSLATION_MODEL_ENGINES = {
111
- "nllb_600m": "nllb",
112
- "nllb_1.3b": "nllb_1_3b",
113
- "madlad_3b": "madlad",
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 · Traditional fallback"),)
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'