arindae commited on
Commit
5006761
·
verified ·
1 Parent(s): 43d1ec9

unnecessary dir

Browse files
Files changed (37) hide show
  1. translator_app/manage.py +0 -22
  2. translator_app/translator/__init__.py +0 -0
  3. translator_app/translator/__pycache__/__init__.cpython-314.pyc +0 -0
  4. translator_app/translator/__pycache__/admin.cpython-314.pyc +0 -0
  5. translator_app/translator/__pycache__/apps.cpython-314.pyc +0 -0
  6. translator_app/translator/__pycache__/models.cpython-314.pyc +0 -0
  7. translator_app/translator/__pycache__/tests.cpython-314.pyc +0 -0
  8. translator_app/translator/__pycache__/urls.cpython-314.pyc +0 -0
  9. translator_app/translator/__pycache__/views.cpython-314.pyc +0 -0
  10. translator_app/translator/admin.py +0 -23
  11. translator_app/translator/apps.py +0 -5
  12. translator_app/translator/forms.py +0 -83
  13. translator_app/translator/migrations/0001_initial.py +0 -75
  14. translator_app/translator/migrations/0002_translationhistory_translation_model.py +0 -15
  15. translator_app/translator/migrations/__init__.py +0 -1
  16. translator_app/translator/migrations/__pycache__/0001_initial.cpython-314.pyc +0 -0
  17. translator_app/translator/migrations/__pycache__/0002_translationhistory_translation_model.cpython-314.pyc +0 -0
  18. translator_app/translator/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  19. translator_app/translator/models.py +0 -91
  20. translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc +0 -0
  21. translator_app/translator/services/language_support.py +0 -104
  22. translator_app/translator/services/translation_service.py +0 -270
  23. translator_app/translator/templates/translator/base.html +0 -21
  24. translator_app/translator/templates/translator/history.html +0 -9
  25. translator_app/translator/templates/translator/translator.html +0 -458
  26. translator_app/translator/tests.py +0 -35
  27. translator_app/translator/urls.py +0 -15
  28. translator_app/translator/views.py +0 -200
  29. translator_app/translator_project/__init__.py +0 -0
  30. translator_app/translator_project/__pycache__/__init__.cpython-314.pyc +0 -0
  31. translator_app/translator_project/__pycache__/settings.cpython-314.pyc +0 -0
  32. translator_app/translator_project/__pycache__/urls.cpython-314.pyc +0 -0
  33. translator_app/translator_project/__pycache__/wsgi.cpython-314.pyc +0 -0
  34. translator_app/translator_project/asgi.py +0 -16
  35. translator_app/translator_project/settings.py +0 -134
  36. translator_app/translator_project/urls.py +0 -23
  37. translator_app/translator_project/wsgi.py +0 -16
translator_app/manage.py DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env python
2
- """Django's command-line utility for administrative tasks."""
3
- import os
4
- import sys
5
-
6
-
7
- def main():
8
- """Run administrative tasks."""
9
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'translator_project.settings')
10
- try:
11
- from django.core.management import execute_from_command_line
12
- except ImportError as exc:
13
- raise ImportError(
14
- "Couldn't import Django. Are you sure it's installed and "
15
- "available on your PYTHONPATH environment variable? Did you "
16
- "forget to activate a virtual environment?"
17
- ) from exc
18
- execute_from_command_line(sys.argv)
19
-
20
-
21
- if __name__ == '__main__':
22
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/__init__.py DELETED
File without changes
translator_app/translator/__pycache__/__init__.cpython-314.pyc DELETED
Binary file (171 Bytes)
 
translator_app/translator/__pycache__/admin.cpython-314.pyc DELETED
Binary file (1.68 kB)
 
translator_app/translator/__pycache__/apps.cpython-314.pyc DELETED
Binary file (479 Bytes)
 
translator_app/translator/__pycache__/models.cpython-314.pyc DELETED
Binary file (5.83 kB)
 
translator_app/translator/__pycache__/tests.cpython-314.pyc DELETED
Binary file (2.82 kB)
 
translator_app/translator/__pycache__/urls.cpython-314.pyc DELETED
Binary file (846 Bytes)
 
translator_app/translator/__pycache__/views.cpython-314.pyc DELETED
Binary file (10.6 kB)
 
translator_app/translator/admin.py DELETED
@@ -1,23 +0,0 @@
1
- from django.contrib import admin
2
- from .models import Language, TranslationCache, TranslationHistory
3
-
4
-
5
- @admin.register(Language)
6
- class LanguageAdmin(admin.ModelAdmin):
7
- list_display = ('name', 'code', 'native_name', 'is_active')
8
- list_filter = ('is_active',)
9
- search_fields = ('name', 'native_name', 'code')
10
-
11
-
12
- @admin.register(TranslationHistory)
13
- class TranslationHistoryAdmin(admin.ModelAdmin):
14
- list_display = ('source_text', 'source_language', 'target_language', 'translation_model', 'created_at')
15
- list_filter = ('source_language', 'target_language', 'translation_model')
16
- search_fields = ('source_text', 'translated_text')
17
- readonly_fields = ('character_count', 'word_count', 'created_at')
18
-
19
-
20
- @admin.register(TranslationCache)
21
- class TranslationCacheAdmin(admin.ModelAdmin):
22
- list_display = ('source_text', 'target_language_code', 'access_count', 'last_accessed')
23
- search_fields = ('source_text', 'translated_text')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/apps.py DELETED
@@ -1,5 +0,0 @@
1
- from django.apps import AppConfig
2
-
3
-
4
- class TranslatorConfig(AppConfig):
5
- name = 'translator'
 
 
 
 
 
 
translator_app/translator/forms.py DELETED
@@ -1,83 +0,0 @@
1
- # translator/forms.py
2
-
3
- from django import forms
4
- from django.core.validators import MinLengthValidator, MaxLengthValidator
5
- from .models import Language
6
-
7
- class TranslationForm(forms.Form):
8
- """Form for translation input"""
9
-
10
- source_text = forms.CharField(
11
- widget=forms.Textarea(attrs={
12
- 'rows': 5,
13
- 'placeholder': 'Enter text to translate...',
14
- 'class': 'form-control',
15
- 'id': 'source-text'
16
- }),
17
- validators=[
18
- MinLengthValidator(1, message='Text cannot be empty'),
19
- MaxLengthValidator(5000, message='Text is too long (maximum 5000 characters)')
20
- ],
21
- label='Text to Translate'
22
- )
23
-
24
- source_language = forms.ChoiceField(
25
- choices=[],
26
- required=False,
27
- widget=forms.Select(attrs={
28
- 'class': 'form-select',
29
- 'id': 'source-language'
30
- }),
31
- label='Source Language (Optional)'
32
- )
33
-
34
- target_language = forms.ChoiceField(
35
- choices=[],
36
- widget=forms.Select(attrs={
37
- 'class': 'form-select',
38
- 'id': 'target-language'
39
- }),
40
- label='Target Language'
41
- )
42
-
43
- auto_detect = forms.BooleanField(
44
- required=False,
45
- initial=True,
46
- widget=forms.CheckboxInput(attrs={
47
- 'class': 'form-check-input',
48
- 'id': 'auto-detect'
49
- }),
50
- label='Auto-detect source language'
51
- )
52
-
53
- def __init__(self, *args, **kwargs):
54
- super().__init__(*args, **kwargs)
55
-
56
- # Get languages from database or service
57
- from .services.translation_service import TranslationService
58
- service = TranslationService()
59
- languages = service.get_supported_languages()
60
-
61
- # Build language choices
62
- language_choices = [('', 'Auto-detect')]
63
- language_choices += [(lang['code'], lang['name']) for lang in languages]
64
-
65
- self.fields['source_language'].choices = language_choices
66
- self.fields['target_language'].choices = language_choices[1:] # Exclude auto-detect for target
67
-
68
- # Set default target language
69
- if not self.fields['target_language'].initial:
70
- self.fields['target_language'].initial = 'en'
71
-
72
- def clean(self):
73
- cleaned_data = super().clean()
74
- source_text = cleaned_data.get('source_text')
75
- target_language = cleaned_data.get('target_language')
76
-
77
- if not source_text:
78
- raise forms.ValidationError('Please enter text to translate')
79
-
80
- if not target_language:
81
- raise forms.ValidationError('Please select a target language')
82
-
83
- return cleaned_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/migrations/0001_initial.py DELETED
@@ -1,75 +0,0 @@
1
- # Generated by Django 6.0.7 on 2026-07-22 08:44
2
-
3
- import django.db.models.deletion
4
- import django.utils.timezone
5
- from django.conf import settings
6
- from django.db import migrations, models
7
-
8
-
9
- class Migration(migrations.Migration):
10
-
11
- initial = True
12
-
13
- dependencies = [
14
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
- ]
16
-
17
- operations = [
18
- migrations.CreateModel(
19
- name='Language',
20
- fields=[
21
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
- ('code', models.CharField(max_length=10, unique=True)),
23
- ('name', models.CharField(max_length=50)),
24
- ('native_name', models.CharField(blank=True, max_length=50)),
25
- ('is_active', models.BooleanField(default=True)),
26
- ('created_at', models.DateTimeField(auto_now_add=True)),
27
- ('updated_at', models.DateTimeField(auto_now=True)),
28
- ],
29
- options={
30
- 'verbose_name': 'Language',
31
- 'verbose_name_plural': 'Languages',
32
- 'ordering': ['name'],
33
- },
34
- ),
35
- migrations.CreateModel(
36
- name='TranslationCache',
37
- fields=[
38
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39
- ('source_text_hash', models.CharField(max_length=64, unique=True)),
40
- ('source_text', models.TextField()),
41
- ('translated_text', models.TextField()),
42
- ('source_language_code', models.CharField(max_length=10)),
43
- ('target_language_code', models.CharField(max_length=10)),
44
- ('created_at', models.DateTimeField(auto_now_add=True)),
45
- ('last_accessed', models.DateTimeField(auto_now=True)),
46
- ('access_count', models.IntegerField(default=0)),
47
- ],
48
- options={
49
- 'verbose_name': 'Translation Cache',
50
- 'verbose_name_plural': 'Translation Caches',
51
- 'indexes': [models.Index(fields=['source_text_hash', 'target_language_code'], name='translator__source__c37921_idx')],
52
- },
53
- ),
54
- migrations.CreateModel(
55
- name='TranslationHistory',
56
- fields=[
57
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
58
- ('source_text', models.TextField()),
59
- ('translated_text', models.TextField()),
60
- ('detected_source_language', models.CharField(blank=True, max_length=10)),
61
- ('character_count', models.IntegerField(default=0)),
62
- ('word_count', models.IntegerField(default=0)),
63
- ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
64
- ('source_language', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_translations', to='translator.language')),
65
- ('target_language', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='target_translations', to='translator.language')),
66
- ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to=settings.AUTH_USER_MODEL)),
67
- ],
68
- options={
69
- 'verbose_name': 'Translation History',
70
- 'verbose_name_plural': 'Translation Histories',
71
- 'ordering': ['-created_at'],
72
- 'indexes': [models.Index(fields=['user', '-created_at'], name='translator__user_id_18b742_idx')],
73
- },
74
- ),
75
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/migrations/0002_translationhistory_translation_model.py DELETED
@@ -1,15 +0,0 @@
1
- # Generated by Django 6.0.7 on 2026-07-22
2
-
3
- from django.db import migrations, models
4
-
5
-
6
- class Migration(migrations.Migration):
7
- dependencies = [('translator', '0001_initial')]
8
-
9
- operations = [
10
- migrations.AddField(
11
- model_name='translationhistory',
12
- name='translation_model',
13
- field=models.CharField(blank=True, max_length=100),
14
- ),
15
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/migrations/__init__.py DELETED
@@ -1 +0,0 @@
1
-
 
 
translator_app/translator/migrations/__pycache__/0001_initial.cpython-314.pyc DELETED
Binary file (4.39 kB)
 
translator_app/translator/migrations/__pycache__/0002_translationhistory_translation_model.cpython-314.pyc DELETED
Binary file (849 Bytes)
 
translator_app/translator/migrations/__pycache__/__init__.cpython-314.pyc DELETED
Binary file (182 Bytes)
 
translator_app/translator/models.py DELETED
@@ -1,91 +0,0 @@
1
- # translator/models.py
2
-
3
- from django.db import models
4
- from django.contrib.auth.models import User
5
- from django.utils import timezone
6
-
7
- class Language(models.Model):
8
- """Model to store supported languages"""
9
- code = models.CharField(max_length=10, unique=True)
10
- name = models.CharField(max_length=50)
11
- native_name = models.CharField(max_length=50, blank=True)
12
- is_active = models.BooleanField(default=True)
13
- created_at = models.DateTimeField(auto_now_add=True)
14
- updated_at = models.DateTimeField(auto_now=True)
15
-
16
- class Meta:
17
- ordering = ['name']
18
- verbose_name = 'Language'
19
- verbose_name_plural = 'Languages'
20
-
21
- def __str__(self):
22
- return f"{self.name} ({self.code})"
23
-
24
- class TranslationHistory(models.Model):
25
- """Model to store translation history"""
26
- user = models.ForeignKey(
27
- User,
28
- on_delete=models.CASCADE,
29
- null=True,
30
- blank=True,
31
- related_name='translations'
32
- )
33
- source_text = models.TextField()
34
- translated_text = models.TextField()
35
- source_language = models.ForeignKey(
36
- Language,
37
- on_delete=models.SET_NULL,
38
- null=True,
39
- related_name='source_translations'
40
- )
41
- target_language = models.ForeignKey(
42
- Language,
43
- on_delete=models.SET_NULL,
44
- null=True,
45
- related_name='target_translations'
46
- )
47
- detected_source_language = models.CharField(max_length=10, blank=True)
48
- translation_model = models.CharField(max_length=100, blank=True)
49
- character_count = models.IntegerField(default=0)
50
- word_count = models.IntegerField(default=0)
51
- created_at = models.DateTimeField(default=timezone.now)
52
-
53
- class Meta:
54
- ordering = ['-created_at']
55
- verbose_name = 'Translation History'
56
- verbose_name_plural = 'Translation Histories'
57
- indexes = [
58
- models.Index(fields=['user', '-created_at']),
59
- ]
60
-
61
- def __str__(self):
62
- user_str = self.user.username if self.user else 'Anonymous'
63
- return f"{user_str} - {self.source_text[:50]}... -> {self.target_language}"
64
-
65
- def save(self, *args, **kwargs):
66
- """Calculate character and word counts before saving"""
67
- if self.source_text:
68
- self.character_count = len(self.source_text)
69
- self.word_count = len(self.source_text.split())
70
- super().save(*args, **kwargs)
71
-
72
- class TranslationCache(models.Model):
73
- """Model to cache translations for performance"""
74
- source_text_hash = models.CharField(max_length=64, unique=True)
75
- source_text = models.TextField()
76
- translated_text = models.TextField()
77
- source_language_code = models.CharField(max_length=10)
78
- target_language_code = models.CharField(max_length=10)
79
- created_at = models.DateTimeField(auto_now_add=True)
80
- last_accessed = models.DateTimeField(auto_now=True)
81
- access_count = models.IntegerField(default=0)
82
-
83
- class Meta:
84
- verbose_name = 'Translation Cache'
85
- verbose_name_plural = 'Translation Caches'
86
- indexes = [
87
- models.Index(fields=['source_text_hash', 'target_language_code']),
88
- ]
89
-
90
- def __str__(self):
91
- return f"{self.source_text[:50]}... -> {self.target_language_code}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc DELETED
Binary file (12 kB)
 
translator_app/translator/services/language_support.py DELETED
@@ -1,104 +0,0 @@
1
- # translator/services/language_support.py
2
- """
3
- Explicit per-model language support.
4
-
5
- This is deliberately separate from `TranslationService.NLLB_LANGUAGE_CODES`
6
- in translation_service.py. That dict answers "does the model API have a wire
7
- code for this language at all". This file answers a different question:
8
- "does this specific model actually produce usable output for this language",
9
- which is what was silently broken.
10
-
11
- TWO CONCRETE BUGS THIS FIXES
12
- -----------------------------
13
- 1. "luo" has no valid FLORES-200 / SALT tag. ISO 639-2 "luo" is a
14
- *macrolanguage* umbrella covering Dholuo, Lango, Alur, Adhola and Acholi
15
- as distinct languages - it was never a real NLLB/MADLAD language code.
16
- That's why translation requests tagged "luo" against nllb / nllb_1_3b /
17
- madlad were failing outright: "luo_Latn" doesn't exist on the model's
18
- side. If you meant Acholi, that's already covered correctly by "ach".
19
- If you specifically need Dholuo/Lango/Alur, none of your current local
20
- models support them - only the Google/DeepL fallback paths can.
21
-
22
- 2. Your language roster (English, Swahili, Luganda, Runyankole, Acholi,
23
- Ateso, Lugbara) lines up closely with the Sunbird SALT East African
24
- language project rather than any generic multilingual checkpoint. If
25
- your deployed HF Space is a SALT-style fine-tune of NLLB, MADLAD-400
26
- (a separate, generically-trained Google model) very likely was never
27
- fine-tuned on those five Ugandan low-resource languages at all, even
28
- though the UI happily offered them for every model.
29
-
30
- HOW TO VERIFY / MAINTAIN THIS
31
- ------------------------------
32
- This table is a best-effort starting point, not a guarantee - I can't see
33
- your actual deployed HF Space's training data. Two ways to firm it up:
34
- a) If your Space exposes a /languages or /health endpoint listing what
35
- it was actually fine-tuned/evaluated on, wire `_languages()` in
36
- views.py to read it live instead of hardcoding this table.
37
- b) Otherwise, test each (model, language) pair once against your Space
38
- and adjust the sets below accordingly - a wrong entry here just means
39
- a language gets offered when it shouldn't (or hidden when it's fine),
40
- not a crash, since translate_api() falls back to Google either way.
41
- """
42
-
43
- # None = unrestricted (accepts any language pair the frontend can offer).
44
- MODEL_LANGUAGE_SUPPORT = {
45
- "nllb": {"en", "sw", "lg", "nyn", "ach", "teo", "lgg", "rw", "fr", "ar"},
46
- "nllb_1_3b": {"en", "sw", "lg", "nyn", "ach", "teo", "lgg", "rw", "fr", "ar"},
47
- # Not fine-tuned on the five Ugandan low-resource languages - confirm
48
- # against your Space and trim further if it's even narrower than this.
49
- "madlad": {"en", "sw", "rw", "fr", "ar"},
50
- # DeepL has no coverage at all for sw/lg/nyn/ach/teo/lgg/rw.
51
- "deepl_fallback": {"en", "fr", "ar"},
52
- "google": None,
53
- }
54
-
55
- FALLBACK_MODEL = "google"
56
-
57
- # ElevenLabs eleven_multilingual_v2's real language list (per their docs as
58
- # of mid-2026). None of your East African languages are on it - only en/fr/ar
59
- # from your roster are. Requests for anything else should never hit the
60
- # ElevenLabs API; fall back to the browser's speechSynthesis instead.
61
- ELEVENLABS_MULTILINGUAL_V2_LANGUAGES = {
62
- "en", "zh", "es", "hi", "pt", "fr", "de", "ja", "ar", "ko", "id", "it",
63
- "nl", "tr", "pl", "sv", "fil", "ms", "ru", "ro", "uk", "el", "cs", "da",
64
- "fi", "bg", "hr", "sk", "ta",
65
- }
66
-
67
- ELEVENLABS_V3_LANGUAGES = {
68
- "af", "sq", "am", "ar", "hy", "as", "az", "ba", "eu", "be",
69
- "bn", "bos", "bg", "my", "ca", "ceb", "ny", "zh", "hr", "cs",
70
- "da", "nl", "en", "eo", "et", "fil", "fi", "fr", "gl", "ka",
71
- "de", "el", "gu", "ht", "ha", "haw", "he", "hi", "hmn", "hu",
72
- "is", "ig", "id", "ga", "it", "ja", "jv", "kn", "kk", "km",
73
- "rw", "ko", "ku", "ky", "lo", "la", "lv", "ln", "lt", "lb",
74
- "mk", "mg", "ms", "ml", "mt", "mi", "mr", "mn", "ne", "no",
75
- "or", "om", "ps", "fa", "pl", "pt", "pa", "ro", "ru", "sm",
76
- "gd", "sr", "st", "sn", "sd", "si", "sk", "sl", "so", "es",
77
- "su", "sw", "sv", "tg", "ta", "tt", "te", "th", "tr", "tk",
78
- "uk", "ur", "ug", "uz", "vi", "cy", "xh", "yi", "yo", "zu"
79
- }
80
-
81
-
82
-
83
- def supported_codes(model):
84
- """Set of codes a model supports, or None if unrestricted."""
85
- return MODEL_LANGUAGE_SUPPORT.get(model, set())
86
-
87
-
88
- def model_supports(model, *codes):
89
- """True if `model` supports every non-empty code given."""
90
- allowed = supported_codes(model)
91
- if allowed is None:
92
- return True
93
- return all(code in allowed for code in codes if code)
94
-
95
-
96
- def resolve_model(requested_model, source_code, target_code):
97
- """
98
- Return (model_to_use, fell_back). If the requested model can't handle
99
- this language pair, silently route to the Google fallback rather than
100
- erroring, and tell the caller that happened.
101
- """
102
- if model_supports(requested_model, source_code, target_code):
103
- return requested_model, False
104
- return FALLBACK_MODEL, True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/services/translation_service.py DELETED
@@ -1,270 +0,0 @@
1
- """Translation providers behind one stable application-level contract."""
2
-
3
- import hashlib
4
- import logging
5
-
6
- import requests
7
- from django.conf import settings
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:
18
- """Run the selected ML model, with an optional DeepL-compatible fallback."""
19
-
20
- # NLLB requires its FLORES language tags. MADLAD accepts ISO language codes.
21
- NLLB_LANGUAGE_CODES = {
22
- "en": "eng_Latn",
23
- "sw": "swh_Latn",
24
- "lg": "lug_Latn",
25
- "luo": "luo_Latn",
26
- "nyn": "nyn_Latn",
27
- "ach": "ach_Latn",
28
- "teo": "teo_Latn",
29
- "lgg": "lgg_Latn",
30
- "rw": "kin_Latn",
31
- "fr": "fra_Latn",
32
- "ar": "arb_Arab",
33
- }
34
-
35
- def __init__(self):
36
- self.supported_languages = self._get_supported_languages()
37
-
38
- def _get_supported_languages(self):
39
- return {
40
- "en": "English",
41
- "sw": "Swahili (Kiswahili)",
42
- "lg": "Luganda (Ganda)",
43
- "luo": "Luo (Acholi / Lango)",
44
- "nyn": "Runyankole / Rukiga",
45
- "ach": "Acholi",
46
- "teo": "Ateso (Teso)",
47
- "lgg": "Lugbara",
48
- "rw": "Kinyarwanda",
49
- "fr": "French",
50
- "ar": "Arabic",
51
- }
52
-
53
- def get_supported_languages(self):
54
- return [
55
- {"code": code, "name": name}
56
- for code, name in self.supported_languages.items()
57
- ]
58
-
59
- def detect_language(self, text):
60
- try:
61
- code = detect(text)
62
- except LangDetectException:
63
- code = "en"
64
- if code not in self.supported_languages:
65
- code = "en" # fall back if detected language isn't one you support
66
- return {
67
- "code": code,
68
- "name": self.supported_languages.get(code, code.upper()),
69
- "confidence": 0.8 if code in self.supported_languages else 0.3,
70
- }
71
-
72
- def translate(self, text, target_language, source_language=None, model=None):
73
- if not text or not text.strip():
74
- return {"error": "Text cannot be empty"}
75
-
76
- model = model or settings.TRANSLATION_DEFAULT_MODEL
77
- if model == "madlad":
78
- source_language = None # MADLAD auto-detects; any provided value is ignored
79
- source_language = source_language or self.detect_language(text)["code"]
80
- cache_key = self._generate_cache_key(
81
- text, source_language, target_language, model
82
- )
83
- cached = self._get_from_cache(cache_key)
84
- if cached:
85
- cached["model"] = model
86
- return cached
87
-
88
- if model == "deepl_fallback":
89
- outcome = self._translate_deepl(text, source_language, target_language)
90
- elif model == "google":
91
- # Explicit, user-selectable Google fallback. Reuses the exact
92
- # same free-fallback call already used internally elsewhere in
93
- # this class - no new translation logic introduced.
94
- outcome = self._translate_free_fallback(
95
- text, source_language, target_language
96
- )
97
- elif model in settings.TRANSLATION_MODEL_ENGINES:
98
- outcome = self._translate_model_api(
99
- text, source_language, target_language, model
100
- )
101
- else:
102
- return {"error": "The selected translation model is unavailable."}
103
-
104
- if outcome.get("error"):
105
- return outcome
106
-
107
- translated_text = outcome["translated_text"]
108
- self._save_to_cache(
109
- cache_key, text, translated_text, source_language, target_language
110
- )
111
- return {
112
- "source_text": text,
113
- "translated_text": translated_text,
114
- "source_language": source_language,
115
- "target_language": target_language,
116
- "model": model,
117
- }
118
-
119
- def _translate_model_api(self, text, source_language, target_language, model):
120
- if not settings.MODEL_API_URL:
121
- logger.info("MODEL_API_URL not set; falling back to free translation engine.")
122
- return self._translate_free_fallback(text, source_language, target_language)
123
-
124
- if isinstance(settings.TRANSLATION_MODEL_ENGINES, dict):
125
- engine_name = settings.TRANSLATION_MODEL_ENGINES.get(model, model)
126
- else:
127
- engine_name = model
128
-
129
- # Every local engine (NLLB, MADLAD) speaks FLORES-200 codes on the wire.
130
- try:
131
- source = self.NLLB_LANGUAGE_CODES[source_language]
132
- target = self.NLLB_LANGUAGE_CODES[target_language]
133
- except KeyError:
134
- return {
135
- "error": "The model service does not support one of these languages."
136
- }
137
-
138
- payload = {
139
- "text": text,
140
- "source": source,
141
- "target": target,
142
- "engine": engine_name,
143
- }
144
-
145
- # DEBUGGING: Print exactly what is being sent to the console
146
- print(
147
- f"\n--- API PAYLOAD DEBUG ---\nURL: {settings.MODEL_API_URL}\nPayload: {payload}\n-------------------------\n"
148
- )
149
- logger.info(f"Sending translation payload: {payload}")
150
-
151
- try:
152
- response = requests.post(
153
- settings.MODEL_API_URL,
154
- json=payload,
155
- timeout=3,
156
- )
157
-
158
- if response.status_code != 200:
159
- logger.warning(
160
- "Model API translation failed: %s %s",
161
- response.status_code,
162
- response.text,
163
- )
164
- return self._translate_free_fallback(text, source_language, target_language)
165
-
166
- translated_text = response.json().get("translation")
167
- if not translated_text:
168
- return self._translate_free_fallback(text, source_language, target_language)
169
- return {"translated_text": translated_text}
170
-
171
- except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
172
- logger.warning("ML translation service unreachable; falling back to free translation engine.")
173
- return self._translate_free_fallback(text, source_language, target_language)
174
- except (ValueError, requests.RequestException) as error:
175
- logger.exception("Model API translation failed: %s", error)
176
- return self._translate_free_fallback(text, source_language, target_language)
177
-
178
- def _translate_free_fallback(self, text, source_language, target_language):
179
- """Free public fallback translation when the ML model API is unconfigured/unavailable."""
180
- try:
181
- url = "https://translate.googleapis.com/translate_a/single"
182
- params = {
183
- "client": "gtx",
184
- "sl": source_language or "auto",
185
- "tl": target_language,
186
- "dt": "t",
187
- "q": text,
188
- }
189
- res = requests.get(url, params=params, timeout=10)
190
- if res.status_code == 200:
191
- data = res.json()
192
- translated_text = "".join([chunk[0] for chunk in data[0] if chunk[0]])
193
- if translated_text:
194
- return {"translated_text": translated_text}
195
- except Exception as err:
196
- logger.warning("Free fallback translation failed: %s", err)
197
-
198
- return {
199
- "error": "Translation service is currently unavailable. Please check your network or backend settings."
200
- }
201
-
202
- def _translate_deepl(self, text, source_language, target_language):
203
- if not settings.TRANSLATION_API_KEY:
204
- return {"error": "Traditional fallback is not configured."}
205
- try:
206
- response = requests.post(
207
- settings.TRANSLATION_API_URL,
208
- headers={
209
- "Authorization": f"DeepL-Auth-Key {settings.TRANSLATION_API_KEY}"
210
- },
211
- json={
212
- "text": [text],
213
- "source_lang": source_language.upper(),
214
- "target_lang": target_language.upper(),
215
- },
216
- timeout=30,
217
- )
218
- if response.status_code != 200:
219
- return {
220
- "error": "The fallback translation service could not complete this request."
221
- }
222
- translated_text = response.json()["translations"][0]["text"]
223
- return {"translated_text": translated_text}
224
- except (KeyError, IndexError, ValueError, requests.RequestException) as error:
225
- logger.exception("Fallback translation failed: %s", error)
226
- return {
227
- "error": "The fallback translation service returned an invalid response."
228
- }
229
-
230
- @staticmethod
231
- def _generate_cache_key(text, source_language, target_language, model):
232
- content = f"{text}|{source_language}|{target_language}|{model}"
233
- return hashlib.sha256(content.encode("utf-8")).hexdigest()
234
-
235
- @staticmethod
236
- def _get_from_cache(cache_key):
237
- try:
238
- entry = TranslationCache.objects.filter(source_text_hash=cache_key).first()
239
- if not entry:
240
- return None
241
- entry.access_count += 1
242
- entry.last_accessed = timezone.now()
243
- entry.save(update_fields=["access_count", "last_accessed"])
244
- return {
245
- "source_text": entry.source_text,
246
- "translated_text": entry.translated_text,
247
- "source_language": entry.source_language_code,
248
- "target_language": entry.target_language_code,
249
- "from_cache": True,
250
- }
251
- except Exception:
252
- logger.exception("Translation cache lookup failed")
253
- return None
254
-
255
- @staticmethod
256
- def _save_to_cache(
257
- cache_key, source_text, translated_text, source_language, target_language
258
- ):
259
- try:
260
- TranslationCache.objects.update_or_create(
261
- source_text_hash=cache_key,
262
- defaults={
263
- "source_text": source_text,
264
- "translated_text": translated_text,
265
- "source_language_code": source_language,
266
- "target_language_code": target_language,
267
- },
268
- )
269
- except Exception:
270
- logger.exception("Translation cache save failed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/templates/translator/base.html DELETED
@@ -1,21 +0,0 @@
1
- {% load static %}
2
- <!doctype html>
3
- <html lang="en">
4
- <head>
5
- <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>{% block title %}The Translator App{% endblock %}</title>
7
- <script src="https://cdn.tailwindcss.com?plugins=forms"></script>
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@500;600;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
10
- <script>tailwind.config={theme:{extend:{colors:{primary:'#84532e',ink:'#22303a',cream:'#fffaf6',sand:'#ffbe91',mist:'#f3e9e1'},fontFamily:{display:['Bricolage Grotesque'],body:['Plus Jakarta Sans']}}}}</script>
11
- <style>body{background:#fffaf6}.quiet-loading:after{content:'';position:absolute;bottom:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent,#84532e,transparent);animation:load 1.4s infinite}@keyframes load{from{transform:translateX(-100%)}to{transform:translateX(100%)}}</style>
12
- {% block extra_head %}{% endblock %}
13
- </head>
14
- <body class="min-h-screen font-body text-ink flex flex-col">
15
- <header class="w-full max-w-6xl mx-auto px-5 pt-9 pb-7 text-center">
16
- <a href="{% url 'translator:translator' %}" class="font-display text-3xl md:text-5xl font-extrabold tracking-tight text-primary">The Translator App</a>
17
- </header>
18
- {% block content %}{% endblock %}
19
- <footer class="py-10 text-center text-sm text-stone-500"><p>© {% now 'Y' %} The Translator App</p></footer>
20
- </body>
21
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/templates/translator/history.html DELETED
@@ -1,9 +0,0 @@
1
- {% extends 'translator/base.html' %}
2
- {% block title %}History · The Translator App{% endblock %}
3
- {% block content %}
4
- <main class="flex-1 w-full max-w-3xl mx-auto px-5 pb-12">
5
- <div class="mb-7 flex items-center justify-between"><h1 class="font-display text-3xl font-bold text-primary">History</h1>{% if translations %}<form method="post" action="{% url 'translator:clear_history' %}">{% csrf_token %}<button class="text-sm font-bold text-red-700 hover:underline">Clear all</button></form>{% endif %}</div>
6
- <div class="space-y-4">{% for item in translations %}<article class="group rounded-xl border border-stone-100 bg-white p-5 shadow-sm"><div class="mb-3 flex justify-between gap-4 text-xs font-bold uppercase tracking-wider text-stone-400"><span>{{ item.source_language.name|default:item.detected_source_language|default:'Detected' }} → {{ item.target_language.name|default:'Unknown' }}</span><time>{{ item.created_at|timesince }} ago</time></div><p class="text-stone-700">{{ item.source_text }}</p><p class="mt-2 font-medium italic text-primary">{{ item.translated_text }}</p><div class="mt-3 flex items-center justify-between"><span class="text-xs text-stone-400">{{ item.model_label|default:'Translation' }}</span><form method="post" action="{% url 'translator:delete_history' item.pk %}">{% csrf_token %}<button class="text-xs font-bold text-stone-400 hover:text-red-700">Delete</button></form></div></article>{% empty %}<div class="rounded-xl border border-dashed border-stone-300 p-12 text-center text-stone-500">No translations yet. <a class="font-bold text-primary underline" href="{% url 'translator:translator' %}">Start translating</a>.</div>{% endfor %}</div>
7
- <div class="mt-10 text-center"><a class="rounded-full bg-primary px-6 py-3 text-sm font-bold text-white" href="{% url 'translator:translator' %}">Back to translate</a></div>
8
- </main>
9
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
translator_app/translator/templates/translator/translator.html DELETED
@@ -1,458 +0,0 @@
1
- {% extends 'translator/base.html' %}
2
- {% block content %}
3
- <main class="flex-1 w-full max-w-5xl mx-auto px-5 pb-12">
4
- <form id="translation-form">
5
- {% csrf_token %}
6
- {{ model_language_support_json|json_script:"model-language-support" }}
7
- {{ elevenlabs_languages_json|json_script:"elevenlabs-languages" }}
8
-
9
- <!-- Header Control Bar -->
10
- <div class="flex flex-wrap justify-center items-center gap-3 mb-6">
11
- <select id="source-language" aria-label="Source language"
12
- 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 transition hover:border-stone-300">
13
- <option value="">Detect language</option>
14
- {% for language in languages %}
15
- <option value="{{ language.code }}" {% if language.code == 'en' %}selected{% endif %}>
16
- {{ language.name }}
17
- </option>
18
- {% endfor %}
19
- </select>
20
-
21
- <button id="swap-languages" type="button" title="Swap languages"
22
- class="rounded-full bg-sand px-3 py-2.5 text-primary shadow-sm transition hover:rotate-180 hover:bg-stone-200">⇄</button>
23
-
24
- <select id="target-language" aria-label="Target language"
25
- 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 transition hover:border-stone-300">
26
- {% for language in languages %}
27
- <option value="{{ language.code }}" {% if language.code == 'sw' %} selected{% endif%}>
28
- {{ language.name }}
29
- </option>
30
- {% endfor %}
31
- </select>
32
-
33
- <label
34
- 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">
35
- <span class="text-xs uppercase tracking-wider text-stone-400">Model</span>
36
- <select id="translation-model" aria-label="Translation model"
37
- class="border-0 bg-transparent py-0 pl-1 pr-7 text-primary font-bold focus:ring-0">
38
- {% for value, label in models %}
39
- <option value="{{ value }}" {% if value == default_model %} selected{% endif %}>
40
- {{ label }}
41
- </option>
42
- {% endfor %}
43
- </select>
44
- </label>
45
- </div>
46
-
47
- <!-- Main Translation Panels -->
48
- <div class="rounded-3xl border border-stone-200 bg-white p-5 md:p-8 shadow-2xl shadow-stone-200/50 md:flex md:gap-8 transition-all">
49
- <!-- Source Panel -->
50
- <section class="flex min-h-72 flex-1 flex-col">
51
- <div class="mb-3 flex items-center justify-between">
52
- <span class="text-xs font-bold uppercase tracking-widest text-stone-400">Source Text</span>
53
- <button id="listen-source" type="button" title="Listen to source text"
54
- class="hidden items-center gap-1 rounded-full bg-mist px-3 py-1 text-xs font-bold text-primary hover:bg-stone-200 transition">
55
- 🔊 Listen
56
- </button>
57
- </div>
58
- <textarea id="source-text" maxlength="5000"
59
- 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"
60
- placeholder="Type or use the Voice Assistant below to dictate..."></textarea>
61
-
62
- <!-- Source Footer Bar with Text Translate Button -->
63
- <div class="mt-4 flex items-center justify-between border-t border-stone-100 pt-3 text-xs text-stone-400">
64
- <div class="flex items-center gap-3">
65
- <button id="clear-source" type="button" class="font-semibold text-primary hover:underline">Clear text</button>
66
- <span id="character-count">0 / 5000 characters</span>
67
- </div>
68
- <button id="translate-text-btn" type="submit"
69
- class="translate-trigger flex items-center gap-1.5 rounded-full bg-primary px-5 py-2 text-xs font-bold uppercase tracking-wider text-white shadow-md shadow-primary/20 transition hover:scale-105 active:scale-95 disabled:cursor-wait disabled:opacity-70">
70
- <span>✨</span> <span>Translate Text</span>
71
- </button>
72
- </div>
73
- </section>
74
-
75
- <div class="my-5 hidden w-px bg-stone-100 md:block"></div>
76
-
77
- <!-- Translation Output Panel -->
78
- <section class="relative flex min-h-72 flex-1 flex-col">
79
- <div class="mb-3 flex items-center justify-between">
80
- <span class="text-xs font-bold uppercase tracking-widest text-stone-400">Translation</span>
81
- <div class="flex items-center gap-2">
82
- <button id="listen-result" type="button" title="Listen to translation"
83
- class="hidden items-center gap-1 rounded-full bg-mist px-3 py-1 text-xs font-bold text-primary hover:bg-stone-200 transition">
84
- 🔊 Listen
85
- </button>
86
- <button id="copy-result" type="button"
87
- class="hidden rounded-full bg-mist px-3 py-1 text-xs font-bold text-primary hover:bg-stone-200 transition">
88
- Copy
89
- </button>
90
- </div>
91
- </div>
92
- <p id="translated-text" aria-live="polite"
93
- 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>
94
- </p>
95
- <p id="result-meta" class="mt-4 text-xs text-stone-400 border-t border-stone-100 pt-3"></p>
96
- </section>
97
- </div>
98
-
99
- <!-- Modern Voice Assistant Chatbot Card -->
100
- <div class="mt-6 rounded-3xl border border-stone-200/80 bg-gradient-to-br from-stone-50 via-white to-sand/40 p-5 md:p-6 shadow-xl shadow-stone-200/40">
101
- <div class="flex flex-col md:flex-row items-center justify-between gap-5">
102
-
103
- <!-- Left Info & Waveform -->
104
- <div class="flex items-center gap-4 text-center md:text-left">
105
- <!-- Mic Floating Action Hub -->
106
- <div class="relative">
107
- <button id="voice-record" type="button" title="Toggle Voice Dictation"
108
- class="relative z-10 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-xl text-white shadow-md shadow-primary/30 transition-all hover:scale-105 active:scale-95 focus:outline-none">
109
- <span id="mic-icon">🎙️</span>
110
- </button>
111
- <!-- Animated Pulsing Ring when Recording -->
112
- <span id="pulse-ring" class="hidden absolute inset-0 -z-10 rounded-full bg-red-400/40 animate-ping"></span>
113
- </div>
114
-
115
- <div>
116
- <div class="flex items-center gap-2 justify-center md:justify-start">
117
- <h3 id="record-status-heading" class="font-display font-bold text-primary text-sm">Voice AI Assistant</h3>
118
- <span id="recording-badge" class="hidden rounded-full bg-red-100 px-2 py-0.5 text-[10px] font-bold uppercase text-red-700 animate-pulse">
119
- 🔴 Live Dictating
120
- </span>
121
- </div>
122
- <p id="record-subtext" class="text-xs text-stone-500 mt-0.5">Tap the mic to dictate live in any supported language.</p>
123
- </div>
124
- </div>
125
-
126
- <!-- Center Sound Wave Visualization (Appears when active) -->
127
- <div id="sound-wave" class="hidden flex items-center gap-1.5 h-6">
128
- <span class="w-1 bg-primary rounded-full animate-[bounce_1s_infinite_100ms] h-3"></span>
129
- <span class="w-1 bg-primary rounded-full animate-[bounce_1s_infinite_300ms] h-6"></span>
130
- <span class="w-1 bg-primary rounded-full animate-[bounce_1s_infinite_200ms] h-4"></span>
131
- <span class="w-1 bg-primary rounded-full animate-[bounce_1s_infinite_400ms] h-6"></span>
132
- <span class="w-1 bg-primary rounded-full animate-[bounce_1s_infinite_150ms] h-3"></span>
133
- </div>
134
-
135
- <!-- Dedicated Voice Translate Action Button -->
136
- <div class="flex items-center gap-2">
137
- <button id="translate-voice-btn" type="submit"
138
- class="translate-trigger flex items-center gap-1.5 rounded-full bg-primary px-5 py-2 text-xs font-bold uppercase tracking-wider text-white shadow-md shadow-primary/20 transition hover:scale-105 active:scale-95 disabled:cursor-wait disabled:opacity-70">
139
- <span>⚡</span> <span>Translate Voice</span>
140
- </button>
141
- </div>
142
- </div>
143
- </div>
144
-
145
- <!-- Alert / Message bar -->
146
- <div class="mt-4 text-center">
147
- <p id="form-message" role="alert" class="min-h-5 text-sm font-semibold text-red-600"></p>
148
- </div>
149
- </form>
150
-
151
- <!-- History Footer Navigation -->
152
- <div class="mt-10 text-center">
153
- <a href="{% url 'translator:history' %}"
154
- class="inline-flex items-center gap-2 rounded-full border border-stone-200 bg-white px-6 py-2.5 text-sm font-bold text-primary shadow-sm hover:bg-sand transition">
155
- 📜 View Translation History
156
- </a>
157
- </div>
158
- </main>
159
-
160
- <script>
161
- 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');
162
- const csrf = document.querySelector('[name=csrfmiddlewaretoken]').value;
163
- const modelSelect = document.querySelector('#translation-model');
164
- const sourceSelect = document.querySelector('#source-language');
165
- const targetSelect = document.querySelector('#target-language');
166
- const recordBtn = document.querySelector('#voice-record');
167
- const micIcon = document.querySelector('#mic-icon');
168
- const pulseRing = document.querySelector('#pulse-ring');
169
- const recordBadge = document.querySelector('#recording-badge');
170
- const recordSubtext = document.querySelector('#record-subtext');
171
- const soundWave = document.querySelector('#sound-wave');
172
- const listenSourceBtn = document.querySelector('#listen-source');
173
- const listenResultBtn = document.querySelector('#listen-result');
174
- const copyBtn = document.querySelector('#copy-result');
175
-
176
- // --- Model-aware language filtering ------------------------------------
177
- // modelLanguageSupport: { model_code: [lang codes] | null (unrestricted) }
178
- const modelLanguageSupport = JSON.parse(document.querySelector('#model-language-support').textContent);
179
- const elevenlabsLanguages = new Set(JSON.parse(document.querySelector('#elevenlabs-languages').textContent));
180
-
181
- function supportedSet(model) {
182
- const codes = modelLanguageSupport[model];
183
- return codes === null || codes === undefined ? null : new Set(codes);
184
- }
185
-
186
- // Hide (not remove) options unsupported by the current model, so swapping
187
- // models back later doesn't lose the full list. Always keeps "Detect
188
- // language" (empty value) visible in the source select.
189
- function filterSelectForModel(selectEl, allowed) {
190
- let selectedStillValid = false;
191
- Array.from(selectEl.options).forEach(opt => {
192
- const ok = !opt.value || allowed === null || allowed.has(opt.value);
193
- opt.hidden = !ok;
194
- opt.disabled = !ok;
195
- if (ok && opt.value === selectEl.value) selectedStillValid = true;
196
- });
197
- if (!selectedStillValid) {
198
- const firstOk = Array.from(selectEl.options).find(o => !o.hidden && o.value);
199
- if (firstOk) selectEl.value = firstOk.value;
200
- }
201
- }
202
-
203
- function syncLanguagesForModel() {
204
- const isMadlad = modelSelect.value === 'madlad';
205
- sourceSelect.disabled = isMadlad;
206
- if (isMadlad) sourceSelect.value = '';
207
-
208
- const allowed = supportedSet(modelSelect.value);
209
- filterSelectForModel(targetSelect, allowed);
210
- if (!isMadlad) filterSelectForModel(sourceSelect, allowed);
211
- }
212
-
213
- // If the person picks a language the current model can't handle, hop the
214
- // model dropdown to Google fallback automatically rather than letting the
215
- // request fail server-side.
216
- function ensureModelSupportsSelection() {
217
- const allowed = supportedSet(modelSelect.value);
218
- if (allowed === null) return;
219
- const srcOk = modelSelect.value === 'madlad' || !sourceSelect.value || allowed.has(sourceSelect.value);
220
- const tgtOk = !targetSelect.value || allowed.has(targetSelect.value);
221
- if (!srcOk || !tgtOk) {
222
- modelSelect.value = 'google';
223
- syncLanguagesForModel();
224
- message.textContent = 'Switched to Google Translate (fallback) - the previous model doesn\'t support this language.';
225
- }
226
- }
227
-
228
- modelSelect.addEventListener('change', syncLanguagesForModel);
229
- sourceSelect.addEventListener('change', ensureModelSupportsSelection);
230
- targetSelect.addEventListener('change', ensureModelSupportsSelection);
231
- syncLanguagesForModel();
232
-
233
- function updateSourceControls() {
234
- count.textContent = `${source.value.length} / 5000 characters`;
235
- if (source.value.trim().length > 0) {
236
- listenSourceBtn.classList.remove('hidden');
237
- listenSourceBtn.classList.add('inline-flex');
238
- } else {
239
- listenSourceBtn.classList.add('hidden');
240
- listenSourceBtn.classList.remove('inline-flex');
241
- }
242
- }
243
- source.addEventListener('input', updateSourceControls);
244
-
245
- document.querySelector('#clear-source').onclick = () => {
246
- source.value = '';
247
- updateSourceControls();
248
- source.focus();
249
- };
250
-
251
- document.querySelector('#swap-languages').onclick = () => {
252
- if (sourceSelect.value) {
253
- [sourceSelect.value, targetSelect.value] = [targetSelect.value, sourceSelect.value];
254
- }
255
- };
256
-
257
- // Text-to-Speech Audio Reader
258
- // Tries ElevenLabs first (server-side call, key never touches the
259
- // browser); falls back to the browser's built-in speechSynthesis when
260
- // ElevenLabs has no voice for the language, or the request fails.
261
- function speakWithBrowser(text, langCode) {
262
- if (!('speechSynthesis' in window) || !text || !text.trim()) return;
263
- window.speechSynthesis.cancel();
264
- const utterance = new SpeechSynthesisUtterance(text);
265
- if (langCode) utterance.lang = langCode;
266
- window.speechSynthesis.speak(utterance);
267
- }
268
-
269
- let currentAudio = null;
270
-
271
- async function speakText(text, langCode, triggerBtn) {
272
- if (!text || !text.trim()) return;
273
- const lang = (langCode || '').toLowerCase();
274
-
275
- // Skip the network round trip entirely when we already know ElevenLabs
276
- // has no voice for this language.
277
- if (!lang || !elevenlabsLanguages.has(lang)) {
278
- speakWithBrowser(text, lang || 'en');
279
- return;
280
- }
281
-
282
- const originalLabel = triggerBtn ? triggerBtn.innerHTML : null;
283
- if (triggerBtn) {
284
- triggerBtn.disabled = true;
285
- triggerBtn.innerHTML = '⏳ Loading...';
286
- }
287
-
288
- try {
289
- const response = await fetch("{% url 'translator:tts_api' %}", {
290
- method: 'POST',
291
- headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrf },
292
- body: JSON.stringify({ text, language: lang }),
293
- });
294
-
295
- if (!response.ok) {
296
- // Unsupported language, unconfigured key, or API failure - fall
297
- // back quietly rather than surfacing an error for a "Listen" click.
298
- speakWithBrowser(text, lang);
299
- return;
300
- }
301
-
302
- const blob = await response.blob();
303
- if (currentAudio) currentAudio.pause();
304
- currentAudio = new Audio(URL.createObjectURL(blob));
305
- currentAudio.play();
306
- } catch (err) {
307
- speakWithBrowser(text, lang);
308
- } finally {
309
- if (triggerBtn) {
310
- triggerBtn.disabled = false;
311
- triggerBtn.innerHTML = originalLabel;
312
- }
313
- }
314
- }
315
-
316
- listenSourceBtn.onclick = () => speakText(source.value, sourceSelect.value || 'en', listenSourceBtn);
317
- listenResultBtn.onclick = () => speakText(result.textContent, targetSelect.value || 'sw', listenResultBtn);
318
-
319
- // Speech-to-Text Voice Dictation Assistant
320
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
321
- let recognition = null;
322
- let isRecording = false;
323
-
324
- function stopRecordingUI() {
325
- isRecording = false;
326
- pulseRing.classList.add('hidden');
327
- recordBadge.classList.add('hidden');
328
- soundWave.classList.add('hidden');
329
- recordSubtext.textContent = 'Tap the mic to dictate your message live in any language.';
330
- micIcon.textContent = '🎙️';
331
- recordBtn.classList.remove('bg-red-600');
332
- recordBtn.classList.add('bg-primary');
333
- }
334
-
335
- if (SpeechRecognition) {
336
- recognition = new SpeechRecognition();
337
- recognition.continuous = true;
338
- recognition.interimResults = true;
339
-
340
- recognition.onstart = () => {
341
- isRecording = true;
342
- pulseRing.classList.remove('hidden');
343
- recordBadge.classList.remove('hidden');
344
- soundWave.classList.remove('hidden');
345
- soundWave.classList.add('flex');
346
- recordSubtext.textContent = 'Speak clearly into your microphone...';
347
- micIcon.textContent = '⏹️';
348
- recordBtn.classList.remove('bg-primary');
349
- recordBtn.classList.add('bg-red-600');
350
- };
351
-
352
- recognition.onresult = (event) => {
353
- let transcript = '';
354
- for (let i = event.resultIndex; i < event.results.length; i++) {
355
- transcript += event.results[i][0].transcript;
356
- }
357
- source.value = transcript;
358
- updateSourceControls();
359
- };
360
-
361
- recognition.onerror = (event) => {
362
- console.error('Speech recognition error:', event.error);
363
- stopRecordingUI();
364
- if (event.error !== 'no-speech') {
365
- message.textContent = `Voice dictation status: ${event.error}`;
366
- }
367
- };
368
-
369
- recognition.onend = () => {
370
- stopRecordingUI();
371
- };
372
-
373
- recordBtn.addEventListener('click', () => {
374
- if (isRecording) {
375
- recognition.stop();
376
- } else {
377
- message.textContent = '';
378
- const langCode = sourceSelect.value || 'en';
379
- recognition.lang = langCode;
380
- try {
381
- recognition.start();
382
- } catch (e) {
383
- console.error(e);
384
- }
385
- }
386
- });
387
- } else {
388
- recordBtn.addEventListener('click', () => {
389
- message.textContent = 'Voice dictation is supported in Google Chrome, Microsoft Edge, Safari, and Opera.';
390
- });
391
- }
392
-
393
- copyBtn.onclick = async () => {
394
- await navigator.clipboard.writeText(result.textContent);
395
- copyBtn.textContent = 'Copied!';
396
- setTimeout(() => copyBtn.textContent = '📋 Copy', 1200);
397
- };
398
-
399
- const triggerBtns = document.querySelectorAll('.translate-trigger');
400
-
401
- form.addEventListener('submit', async e => {
402
- e.preventDefault();
403
- message.textContent = '';
404
-
405
- // Immediately stop recording & reset Voice UI to default when translating
406
- if (recognition && isRecording) {
407
- try { recognition.stop(); } catch (err) {}
408
- }
409
- stopRecordingUI();
410
-
411
- triggerBtns.forEach(btn => {
412
- btn.disabled = true;
413
- btn.classList.add('opacity-50');
414
- });
415
-
416
- result.classList.add('quiet-loading');
417
-
418
- try {
419
- const response = await fetch("{% url 'translator:translate_api' %}", {
420
- method: 'POST',
421
- headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrf },
422
- body: JSON.stringify({
423
- text: source.value,
424
- source_language: sourceSelect.value,
425
- target_language: targetSelect.value,
426
- model: modelSelect.value
427
- })
428
- });
429
-
430
- const data = await response.json();
431
- if (!response.ok) throw Error(data.error || 'Translation could not be completed.');
432
-
433
- result.textContent = data.translated_text;
434
- if (data.model_used && data.model_used !== modelSelect.value) {
435
- modelSelect.value = data.model_used;
436
- syncLanguagesForModel();
437
- }
438
- const fallbackNote = data.fell_back_to_google ? ' (auto-switched: selected model doesn\'t support this language pair)' : '';
439
- document.querySelector('#result-meta').textContent = `Engine: ${data.model_label || 'Translation'}${fallbackNote} · Saved to history`;
440
- copyBtn.classList.remove('hidden');
441
- copyBtn.classList.add('inline-flex');
442
- listenResultBtn.classList.remove('hidden');
443
- listenResultBtn.classList.add('inline-flex');
444
- } catch (error) {
445
- message.textContent = error.message;
446
- result.innerHTML = '<span class="text-stone-300">Your translation will appear here.</span>';
447
- listenResultBtn.classList.add('hidden');
448
- listenResultBtn.classList.remove('inline-flex');
449
- } finally {
450
- triggerBtns.forEach(btn => {
451
- btn.disabled = false;
452
- btn.classList.remove('opacity-50');
453
- });
454
- result.classList.remove('quiet-loading');
455
- }
456
- });
457
- </script>
458
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/tests.py DELETED
@@ -1,35 +0,0 @@
1
- from django.test import TestCase
2
- from unittest.mock import patch
3
-
4
- from django.urls import reverse
5
-
6
- from .models import TranslationHistory
7
-
8
- class TranslatorViewsTests(TestCase):
9
- def test_translator_page_renders_model_picker(self):
10
- response = self.client.get(reverse('translator:translator'))
11
- self.assertContains(response, 'Translation model')
12
- self.assertContains(response, 'NLLB-200 (600M')
13
-
14
- @patch('translator.views.TranslationService.translate')
15
- def test_translation_api_saves_history(self, translate):
16
- translate.return_value = {
17
- 'source_text': 'Hello', 'translated_text': 'Jambo',
18
- 'source_language': 'en', 'target_language': 'sw', 'model': 'nllb',
19
- }
20
- response = self.client.post(
21
- reverse('translator:translate_api'),
22
- data='{"text":"Hello","source_language":"en","target_language":"sw","model":"nllb"}',
23
- content_type='application/json',
24
- )
25
- self.assertEqual(response.status_code, 200)
26
- self.assertEqual(response.json()['translated_text'], 'Jambo')
27
- history = TranslationHistory.objects.get()
28
- self.assertEqual(history.translation_model, 'nllb')
29
- self.assertEqual(history.source_text, 'Hello')
30
-
31
- def test_translation_api_rejects_missing_text(self):
32
- response = self.client.post(
33
- reverse('translator:translate_api'), data='{}', content_type='application/json'
34
- )
35
- self.assertEqual(response.status_code, 400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/urls.py DELETED
@@ -1,15 +0,0 @@
1
- from django.urls import path
2
-
3
- from . import views
4
-
5
-
6
- app_name = 'translator'
7
-
8
- urlpatterns = [
9
- path('', views.translator, name='translator'),
10
- path('history/', views.history, name='history'),
11
- path('api/translate/', views.translate_api, name='translate_api'),
12
- path('api/tts/', views.text_to_speech, name='tts_api'),
13
- path('history/<int:pk>/delete/', views.delete_history, name='delete_history'),
14
- path('history/clear/', views.clear_history, name='clear_history'),
15
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator/views.py DELETED
@@ -1,200 +0,0 @@
1
- import json
2
-
3
- import requests
4
- from django.conf import settings
5
- from django.http import HttpResponse, JsonResponse
6
- from django.shortcuts import get_object_or_404, redirect, render
7
- from django.views.decorators.http import require_POST
8
-
9
- from .models import Language, TranslationHistory
10
- from .services import language_support
11
- from .services.translation_service import TranslationService
12
-
13
-
14
- def _languages():
15
- """Sync curated East African languages with TranslationService and return them."""
16
- service_languages = TranslationService().get_supported_languages()
17
- active_codes = {item['code'] for item in service_languages}
18
-
19
- # Mark old non-East African languages as inactive in DB
20
- Language.objects.exclude(code__in=active_codes).update(is_active=False)
21
-
22
- for item in service_languages:
23
- Language.objects.update_or_create(
24
- code=item['code'],
25
- defaults={'name': item['name'], 'is_active': True}
26
- )
27
- return list(Language.objects.filter(is_active=True).order_by('name').values('code', 'name'))
28
-
29
-
30
- def translator(request):
31
- languages = _languages()
32
- model_language_support = {
33
- model: (sorted(codes) if codes is not None else None)
34
- for model, codes in language_support.MODEL_LANGUAGE_SUPPORT.items()
35
- }
36
- return render(request, 'translator/translator.html', {
37
- 'languages': languages,
38
- 'models': settings.TRANSLATION_MODELS,
39
- 'default_model': settings.TRANSLATION_DEFAULT_MODEL,
40
- 'model_language_support_json': model_language_support,
41
- 'elevenlabs_languages_json': sorted(language_support.ELEVENLABS_V3_LANGUAGES),
42
- })
43
-
44
-
45
- def history(request):
46
- translations = TranslationHistory.objects.select_related(
47
- 'source_language', 'target_language'
48
- )[:100]
49
- model_labels = dict(settings.TRANSLATION_MODELS)
50
- for item in translations:
51
- item.model_label = model_labels.get(item.translation_model, item.translation_model)
52
- return render(request, 'translator/history.html', {
53
- 'translations': translations,
54
- })
55
-
56
-
57
- @require_POST
58
- def translate_api(request):
59
- try:
60
- payload = json.loads(request.body)
61
- except (TypeError, json.JSONDecodeError):
62
- return JsonResponse({'error': 'Send a valid JSON request.'}, status=400)
63
-
64
- text = (payload.get('text') or '').strip()
65
- source_code = (payload.get('source_language') or '').lower()
66
- target_code = (payload.get('target_language') or '').lower()
67
- model = payload.get('model') or settings.TRANSLATION_DEFAULT_MODEL
68
- allowed_models = dict(settings.TRANSLATION_MODELS)
69
-
70
- if not text:
71
- return JsonResponse({'error': 'Enter text to translate.'}, status=400)
72
- if len(text) > 5000:
73
- return JsonResponse({'error': 'Text is limited to 5,000 characters.'}, status=400)
74
- if not target_code:
75
- return JsonResponse({'error': 'Choose a target language.'}, status=400)
76
- if model not in allowed_models:
77
- return JsonResponse({'error': 'Choose a valid translation model.'}, status=400)
78
-
79
- # MADLAD auto-detects and ignores any provided source; skip the source
80
- # side of this check for it so we don't fallback unnecessarily.
81
- check_source = None if model == 'madlad' else (source_code or None)
82
- model, fell_back = language_support.resolve_model(model, check_source, target_code)
83
- if model not in allowed_models:
84
- # Shouldn't happen (google_fallback is always registered), but stay safe.
85
- return JsonResponse({'error': 'No translation model supports this language pair.'}, status=422)
86
-
87
- service = TranslationService()
88
- result = service.translate(text, target_code, source_code or None, model=model)
89
- if result.get('error'):
90
- return JsonResponse(result, status=502)
91
-
92
- source_code = result.get('source_language', source_code)
93
- known_languages = {item['code']: item['name'] for item in service.get_supported_languages()}
94
-
95
- def language_for(code):
96
- if not code or code == 'auto-detected':
97
- return None
98
- language, _ = Language.objects.get_or_create(
99
- code=code,
100
- defaults={'name': known_languages.get(code, code.upper())},
101
- )
102
- return language
103
-
104
- source_language = language_for(source_code)
105
- target_language = language_for(target_code)
106
- TranslationHistory.objects.create(
107
- user=request.user if request.user.is_authenticated else None,
108
- source_text=text,
109
- translated_text=result['translated_text'],
110
- source_language=source_language,
111
- target_language=target_language,
112
- detected_source_language=source_code if source_code != 'auto-detected' else '',
113
- translation_model=model,
114
- )
115
- result['model_label'] = allowed_models[model]
116
- result['model_used'] = model
117
- result['fell_back_to_google'] = fell_back
118
- return JsonResponse(result)
119
-
120
-
121
- def get_elevenlabs_model(lang_code: str) -> str:
122
- """
123
- Returns the cheapest compatible ElevenLabs model ID for a given language code.
124
- Falls back to v3 if v2 does not support it.
125
- """
126
- clean_code = lang_code.lower().strip()
127
-
128
- if clean_code in language_support.ELEVENLABS_MULTILINGUAL_V2_LANGUAGES:
129
- return "eleven_multilingual_v2"
130
- elif clean_code in language_support.ELEVENLABS_V3_LANGUAGES:
131
- return "eleven_v3"
132
- else:
133
- raise ValueError(f"Language code '{lang_code}' is not supported by any ElevenLabs model.")
134
-
135
- @require_POST
136
- def text_to_speech(request):
137
- if not settings.ELEVENLABS_API_KEY:
138
- return JsonResponse(
139
- {'error': 'Text-to-speech is not configured on the server.'}, status=503
140
- )
141
-
142
- try:
143
- payload = json.loads(request.body)
144
- except (TypeError, json.JSONDecodeError):
145
- return JsonResponse({'error': 'Send a valid JSON request.'}, status=400)
146
-
147
- text = (payload.get('text') or '').strip()
148
- lang = (payload.get('language') or '').lower()
149
-
150
- if not text:
151
- return JsonResponse({'error': 'No text to read aloud.'}, status=400)
152
- text = text[:5000]
153
-
154
- # Fail fast client-side instead of burning an API call: ElevenLabs'
155
- # multilingual model has no voice for these languages yet.
156
- if lang and lang not in language_support.ELEVENLABS_V3_LANGUAGES:
157
- return JsonResponse(
158
- {
159
- 'error': 'ElevenLabs does not have a voice for this language yet.',
160
- 'unsupported_language': True,
161
- },
162
- status=422,
163
- )
164
-
165
- try:
166
- response = requests.post(
167
- settings.ELEVENLABS_TTS_URL.format(voice_id=settings.ELEVENLABS_VOICE_ID),
168
- headers={
169
- 'xi-api-key': settings.ELEVENLABS_API_KEY,
170
- 'Content-Type': 'application/json',
171
- 'Accept': 'audio/mpeg',
172
- },
173
- json={
174
- 'text': text,
175
- 'model_id': get_elevenlabs_model(lang),
176
- },
177
- timeout=20,
178
- )
179
- except requests.RequestException:
180
- return JsonResponse({'error': 'Speech service is unreachable.'}, status=502)
181
-
182
- if response.status_code != 200:
183
- logger_message = response.text[:300] if response.text else ''
184
- return JsonResponse(
185
- {'error': 'Speech generation failed.', 'detail': logger_message}, status=502
186
- )
187
-
188
- return HttpResponse(response.content, content_type='audio/mpeg')
189
-
190
-
191
- @require_POST
192
- def delete_history(request, pk):
193
- get_object_or_404(TranslationHistory, pk=pk).delete()
194
- return redirect('translator:history')
195
-
196
-
197
- @require_POST
198
- def clear_history(request):
199
- TranslationHistory.objects.all().delete()
200
- return redirect('translator:history')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator_project/__init__.py DELETED
File without changes
translator_app/translator_project/__pycache__/__init__.cpython-314.pyc DELETED
Binary file (179 Bytes)
 
translator_app/translator_project/__pycache__/settings.cpython-314.pyc DELETED
Binary file (3.66 kB)
 
translator_app/translator_project/__pycache__/urls.cpython-314.pyc DELETED
Binary file (1.1 kB)
 
translator_app/translator_project/__pycache__/wsgi.cpython-314.pyc DELETED
Binary file (684 Bytes)
 
translator_app/translator_project/asgi.py DELETED
@@ -1,16 +0,0 @@
1
- """
2
- ASGI config for translator_project project.
3
-
4
- It exposes the ASGI callable as a module-level variable named ``application``.
5
-
6
- For more information on this file, see
7
- https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
8
- """
9
-
10
- import os
11
-
12
- from django.core.asgi import get_asgi_application
13
-
14
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'translator_project.settings')
15
-
16
- application = get_asgi_application()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator_project/settings.py DELETED
@@ -1,134 +0,0 @@
1
- # translator_project/settings.py
2
-
3
- import os
4
- from pathlib import Path
5
- from dotenv import load_dotenv
6
-
7
- load_dotenv()
8
-
9
- BASE_DIR = Path(__file__).resolve().parent.parent
10
-
11
- SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "your-secret-key-here")
12
-
13
- DEBUG = True
14
-
15
- ALLOWED_HOSTS = [
16
- "localhost",
17
- "127.0.0.1",
18
- ]
19
-
20
- # Application definition
21
- INSTALLED_APPS = [
22
- "django.contrib.admin",
23
- "django.contrib.auth",
24
- "django.contrib.contenttypes",
25
- "django.contrib.sessions",
26
- "django.contrib.messages",
27
- "django.contrib.staticfiles",
28
- "rest_framework",
29
- "translator", # Our app
30
- ]
31
-
32
- MIDDLEWARE = [
33
- "django.middleware.security.SecurityMiddleware",
34
- "django.contrib.sessions.middleware.SessionMiddleware",
35
- "django.middleware.common.CommonMiddleware",
36
- "django.middleware.csrf.CsrfViewMiddleware",
37
- "django.contrib.auth.middleware.AuthenticationMiddleware",
38
- "django.contrib.messages.middleware.MessageMiddleware",
39
- "django.middleware.clickjacking.XFrameOptionsMiddleware",
40
- ]
41
-
42
- ROOT_URLCONF = "translator_project.urls"
43
-
44
- TEMPLATES = [
45
- {
46
- "BACKEND": "django.template.backends.django.DjangoTemplates",
47
- "DIRS": [],
48
- "APP_DIRS": True,
49
- "OPTIONS": {
50
- "context_processors": [
51
- "django.template.context_processors.debug",
52
- "django.template.context_processors.request",
53
- "django.contrib.auth.context_processors.auth",
54
- "django.contrib.messages.context_processors.messages",
55
- ],
56
- },
57
- },
58
- ]
59
-
60
- WSGI_APPLICATION = "translator_project.wsgi.application"
61
-
62
- # Database
63
- DATABASES = {
64
- "default": {
65
- "ENGINE": "django.db.backends.sqlite3",
66
- "NAME": BASE_DIR / "db.sqlite3",
67
- }
68
- }
69
-
70
- # Password validation
71
- AUTH_PASSWORD_VALIDATORS = [
72
- {
73
- "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
74
- },
75
- {
76
- "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
77
- },
78
- {
79
- "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
80
- },
81
- {
82
- "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
83
- },
84
- ]
85
-
86
- # Internationalization
87
- LANGUAGE_CODE = "en-us"
88
- TIME_ZONE = "UTC"
89
- USE_I18N = True
90
- USE_TZ = True
91
-
92
- # Static files
93
- STATIC_URL = "static/"
94
- STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
95
-
96
- # Media files
97
- MEDIA_URL = "/media/"
98
- MEDIA_ROOT = os.path.join(BASE_DIR, "media")
99
-
100
- # Default primary key field type
101
- DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
102
-
103
- # ML translation service
104
- MODEL_API_URL = os.getenv("MODEL_API_URL")
105
- TRANSLATION_DEFAULT_MODEL = os.getenv("TRANSLATION_DEFAULT_MODEL", "nllb")
106
- TRANSLATION_MODELS = (
107
- ("nllb", "NLLB-200 (600M · Fast)"),
108
- ("nllb_1_3b", "NLLB-200 (1.3B · Accurate)"),
109
- ("madlad", "MADLAD-400 (3B · Large)"),
110
- ("google", "Google Translate"),
111
- )
112
- TRANSLATION_MODEL_ENGINES = {
113
- "nllb": "nllb",
114
- "nllb_1_3b": "nllb_1_3b",
115
- "madlad": "madlad",
116
- }
117
-
118
- # Optional traditional-provider fallback. Add this option to TRANSLATION_MODELS
119
- # only when a DeepL-compatible endpoint and key are configured.
120
- TRANSLATION_API_KEY = os.getenv("TRANSLATION_API_KEY")
121
- TRANSLATION_API_URL = os.getenv(
122
- "TRANSLATION_API_URL", "https://api-free.deepl.com/v2/translate"
123
- )
124
- if TRANSLATION_API_KEY:
125
- TRANSLATION_MODELS += (("deepl_fallback", "DeepL"),)
126
- # Or for Google Translate:
127
- # TRANSLATION_API_URL = 'https://translation.googleapis.com/language/translate/v2'
128
-
129
- # ElevenLabs text-to-speech. Key must only ever live server-side (env var) -
130
- # never send it to the browser. Get a key at https://elevenlabs.io/app/settings/api-keys
131
- # and a voice ID from https://elevenlabs.io/app/voice-library
132
- ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
133
- ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID", "hpp4J3VqNfWAUOO0d1Us")
134
- ELEVENLABS_TTS_URL = "https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator_project/urls.py DELETED
@@ -1,23 +0,0 @@
1
- """
2
- URL configuration for translator_project project.
3
-
4
- The `urlpatterns` list routes URLs to views. For more information please see:
5
- https://docs.djangoproject.com/en/6.0/topics/http/urls/
6
- Examples:
7
- Function views
8
- 1. Add an import: from my_app import views
9
- 2. Add a URL to urlpatterns: path('', views.home, name='home')
10
- Class-based views
11
- 1. Add an import: from other_app.views import Home
12
- 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13
- Including another URLconf
14
- 1. Import the include() function: from django.urls import include, path
15
- 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16
- """
17
- from django.contrib import admin
18
- from django.urls import include, path
19
-
20
- urlpatterns = [
21
- path('admin/', admin.site.urls),
22
- path('', include('translator.urls')),
23
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator_app/translator_project/wsgi.py DELETED
@@ -1,16 +0,0 @@
1
- """
2
- WSGI config for translator_project project.
3
-
4
- It exposes the WSGI callable as a module-level variable named ``application``.
5
-
6
- For more information on this file, see
7
- https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
8
- """
9
-
10
- import os
11
-
12
- from django.core.wsgi import get_wsgi_application
13
-
14
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'translator_project.settings')
15
-
16
- application = get_wsgi_application()