Spaces:
Sleeping
Sleeping
unnecessary dir
Browse files- translator_app/manage.py +0 -22
- translator_app/translator/__init__.py +0 -0
- translator_app/translator/__pycache__/__init__.cpython-314.pyc +0 -0
- translator_app/translator/__pycache__/admin.cpython-314.pyc +0 -0
- translator_app/translator/__pycache__/apps.cpython-314.pyc +0 -0
- translator_app/translator/__pycache__/models.cpython-314.pyc +0 -0
- translator_app/translator/__pycache__/tests.cpython-314.pyc +0 -0
- translator_app/translator/__pycache__/urls.cpython-314.pyc +0 -0
- translator_app/translator/__pycache__/views.cpython-314.pyc +0 -0
- translator_app/translator/admin.py +0 -23
- translator_app/translator/apps.py +0 -5
- translator_app/translator/forms.py +0 -83
- translator_app/translator/migrations/0001_initial.py +0 -75
- translator_app/translator/migrations/0002_translationhistory_translation_model.py +0 -15
- translator_app/translator/migrations/__init__.py +0 -1
- translator_app/translator/migrations/__pycache__/0001_initial.cpython-314.pyc +0 -0
- translator_app/translator/migrations/__pycache__/0002_translationhistory_translation_model.cpython-314.pyc +0 -0
- translator_app/translator/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
- translator_app/translator/models.py +0 -91
- translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc +0 -0
- translator_app/translator/services/language_support.py +0 -104
- translator_app/translator/services/translation_service.py +0 -270
- translator_app/translator/templates/translator/base.html +0 -21
- translator_app/translator/templates/translator/history.html +0 -9
- translator_app/translator/templates/translator/translator.html +0 -458
- translator_app/translator/tests.py +0 -35
- translator_app/translator/urls.py +0 -15
- translator_app/translator/views.py +0 -200
- translator_app/translator_project/__init__.py +0 -0
- translator_app/translator_project/__pycache__/__init__.cpython-314.pyc +0 -0
- translator_app/translator_project/__pycache__/settings.cpython-314.pyc +0 -0
- translator_app/translator_project/__pycache__/urls.cpython-314.pyc +0 -0
- translator_app/translator_project/__pycache__/wsgi.cpython-314.pyc +0 -0
- translator_app/translator_project/asgi.py +0 -16
- translator_app/translator_project/settings.py +0 -134
- translator_app/translator_project/urls.py +0 -23
- 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|