boffire's picture
Update app.py
1c44377 verified
Raw
History Blame Contribute Delete
27.4 kB
#!/usr/bin/env python3
"""
Kabyle Translation Hub - Hugging Face Spaces Edition
"""
import warnings
warnings.filterwarnings("ignore", category=FutureWarning)
import os
import time
import requests
import torch
from flask import Flask, request, render_template_string, jsonify
from flask_cors import CORS
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
from concurrent.futures import ThreadPoolExecutor, as_completed
# Configuration
LIBRETRANSLATE_URL = os.environ.get("LIBRETRANSLATE_URL", "https://imsidag-community-libretranslate-kabyle.hf.space/translate")
MODEL_ID = "boffire/marianmt-en-kab"
KABYLE_VARIANTS = {
"Taqbaylit (Standard)": "kab",
"Taqbaylit (Latest)": "kab_kab",
"Taqbaylit (Tasenselkimt)": "kab_comp",
"Taqbaylit (51000)": "kab_comp2",
"Taqbaylit (OS)": "kab_os",
"Taqbaylit (Num)": "kab_num",
}
model = None
tokenizer = None
device = None
# Rate limiting protection
_last_libre_request = 0
_MIN_LIBRE_INTERVAL = 0.5 # seconds between LibreTranslate requests
def load_model():
global model, tokenizer, device
if model is None:
print("Loading MarianMT model...", flush=True)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=False)
model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_ID).to(device).eval()
print(f"Model loaded on {device}", flush=True)
return model, tokenizer, device
def translate_marian(text):
if not text or not text.strip():
return ["Please enter text to translate"]
try:
model, tokenizer, device = load_model()
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)
inputs = {k: v.to(device) for k, v in inputs.items()}
with torch.no_grad():
outputs = model.generate(
**inputs,
num_beams=4,
num_return_sequences=3,
max_length=128,
early_stopping=True,
do_sample=False,
)
translations = []
for output in outputs:
trans = tokenizer.decode(output, skip_special_tokens=True)
if trans and trans not in translations:
translations.append(trans)
return translations if translations else ["[Error: No translation generated]"]
except Exception as e:
print(f"MarianMT error: {e}", flush=True)
import traceback
traceback.print_exc()
return [f"[Error: {str(e)}]"]
def translate_libre_variant(text, variant_code):
global _last_libre_request
# Rate limit protection
elapsed = time.time() - _last_libre_request
if elapsed < _MIN_LIBRE_INTERVAL:
time.sleep(_MIN_LIBRE_INTERVAL - elapsed)
_last_libre_request = time.time()
try:
r = requests.post(
LIBRETRANSLATE_URL,
headers={"Content-Type": "application/json"},
json={"q": text, "source": "en", "target": variant_code},
timeout=10
)
r.raise_for_status()
result = r.json().get("translatedText", "[Error: No translation]")
return {"success": True, "text": result}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
return {"success": False, "text": "[Error: Rate limited by LibreTranslate]"}
return {"success": False, "text": f"[Error: {str(e)[:50]}]"}
except Exception as e:
return {"success": False, "text": f"[Error: {str(e)[:50]}]"}
def translate_libre_all_variants(text):
results = {}
# Sequential instead of parallel to avoid rate limiting
for name, code in KABYLE_VARIANTS.items():
results[name] = translate_libre_variant(text, code)
time.sleep(0.3) # Small delay between requests
return results
# --- HTML TEMPLATES ---
HTML_TEMPLATE = """<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Kabyle Translation Hub</title><style>:root{--bg-primary:#0f172a;--bg-secondary:#1e293b;--bg-tertiary:#334155;--accent-primary:#6366f1;--accent-secondary:#8b5cf6;--accent-success:#10b981;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--border:#475569;--radius:12px;--radius-sm:8px}*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:linear-gradient(135deg,var(--bg-primary) 0%,#1a1a2e 100%);color:var(--text-primary);min-height:100vh;padding:20px}.container{max-width:1000px;margin:0 auto}header{text-align:center;padding:40px 20px}header h1{font-size:2.5rem;font-weight:800;background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:10px}header p{color:var(--text-secondary);font-size:1.1rem}.input-card{background:var(--bg-secondary);border-radius:var(--radius);padding:24px;margin-bottom:24px;border:1px solid var(--border)}.input-group{display:flex;flex-direction:column;gap:16px}textarea{width:100%;padding:16px;background:var(--bg-tertiary);border:2px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-size:16px;resize:vertical;min-height:120px;font-family:inherit;transition:border-color .2s}textarea:focus{outline:none;border-color:var(--accent-primary)}textarea::placeholder{color:var(--text-secondary)}.btn-primary{background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));color:white;border:none;padding:16px 32px;border-radius:var(--radius-sm);font-size:16px;font-weight:600;cursor:pointer;transition:transform .2s,box-shadow .2s;display:flex;align-items:center;justify-content:center;gap:8px}.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 30px rgba(99,102,241,.3)}.btn-primary:disabled{opacity:.6;cursor:not-allowed;transform:none}.results-container{display:flex;flex-direction:column;gap:20px}.engine-card{background:var(--bg-secondary);border-radius:var(--radius);border:1px solid var(--border);overflow:hidden}.engine-header{padding:20px 24px;background:var(--bg-tertiary);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}.engine-title{display:flex;align-items:center;gap:12px;font-size:1.1rem;font-weight:600}.engine-badge{font-size:.75rem;padding:4px 12px;background:var(--bg-secondary);border-radius:20px;color:var(--text-secondary);border:1px solid var(--border)}.translation-list{list-style:none}.translation-item{padding:20px 24px;border-bottom:1px solid var(--border);cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:space-between;gap:16px}.translation-item:last-child{border-bottom:none}.translation-item:hover{background:rgba(99,102,241,.1)}.translation-item.selected{background:rgba(16,185,129,.15);border-left:4px solid var(--accent-success)}.translation-text{flex:1;font-size:1.05rem;line-height:1.6;color:var(--text-primary)}.copy-icon{opacity:0;color:var(--accent-primary);transition:opacity .2s;flex-shrink:0}.translation-item:hover .copy-icon{opacity:1}.variant-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1px;background:var(--border)}.variant-item{background:var(--bg-secondary);padding:16px 20px;cursor:pointer;transition:all .2s;display:flex;flex-direction:column;gap:8px;position:relative}.variant-item:hover{background:rgba(99,102,241,.1)}.variant-item.selected{background:rgba(16,185,129,.15);box-shadow:inset 4px 0 0 var(--accent-success)}.variant-header{display:flex;align-items:center;justify-content:space-between}.variant-name{font-size:.75rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;font-weight:600}.variant-code{font-size:.7rem;color:var(--text-secondary);font-family:monospace;background:var(--bg-tertiary);padding:2px 8px;border-radius:4px}.variant-text{font-size:1rem;color:var(--text-primary);line-height:1.5}.variant-item.error{opacity:.6;cursor:default}.variant-item.error .variant-text{color:#ef4444;font-family:monospace;font-size:.85rem}.source-display{background:var(--bg-tertiary);padding:20px 24px;border-radius:var(--radius);margin-bottom:20px;border:1px solid var(--border)}.source-label{font-size:.75rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}.source-text{font-size:1.1rem;color:var(--text-primary);line-height:1.6}.check-icon{color:var(--accent-success);opacity:0;transition:opacity .2s}.translation-item.selected .check-icon,.variant-item.selected .check-icon{opacity:1}.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(100px);background:var(--accent-success);color:white;padding:12px 24px;border-radius:100px;font-weight:600;opacity:0;transition:all .3s ease;z-index:1000;box-shadow:0 10px 30px rgba(0,0,0,.3)}.toast.show{transform:translateX(-50%) translateY(0);opacity:1}.toast.error{background:#ef4444}.loading{display:inline-block;width:18px;height:18px;border:2px solid rgba(255,255,255,.3);border-radius:50%;border-top-color:white;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:60px 20px;color:var(--text-secondary)}.empty-state svg{width:64px;height:64px;margin-bottom:16px;opacity:.5}.api-note{text-align:center;margin-top:32px;color:var(--text-secondary);font-size:.85rem}.api-note code{background:var(--bg-tertiary);padding:2px 8px;border-radius:4px;font-family:monospace}.api-note a{color:var(--accent-primary);text-decoration:none}.api-note a:hover{text-decoration:underline}@media(max-width:640px){header h1{font-size:1.75rem}.variant-grid{grid-template-columns:1fr}}</style></head><body><div class="container"><header><h1>English → Kabyle</h1><p>Choose the translation that suits you best</p></header><div class="input-card"><form method="POST" id="translateForm"><div class="input-group"><textarea name="text" id="inputText" placeholder="Enter English text to translate..." required autocomplete="off">{{ request.form.get('text', '') }}</textarea><button type="submit" class="btn-primary" id="submitBtn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg><span>Translate</span></button></div></form></div>{% if source_text %}<div class="source-display"><div class="source-label">Source Text</div><div class="source-text">{{ source_text }}</div></div><div class="results-container"><div class="engine-card"><div class="engine-header"><div class="engine-title"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent-primary)"><rect x="4" y="4" width="16" height="16" rx="2"></rect><rect x="9" y="9" width="6" height="6"></rect></svg>MarianMT (Neural)</div><span class="engine-badge">{{ marian|length }} options</span></div><ul class="translation-list">{% for trans in marian %}<li class="translation-item" data-text="{{ trans }}" data-kind="translation"><span class="translation-text">{{ trans }}</span><svg class="check-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"></polyline></svg><svg class="copy-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></li>{% endfor %}</ul></div><div class="engine-card"><div class="engine-header"><div class="engine-title"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent-success)"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>LibreTranslate Variants</div><span class="engine-badge">{{ libre|length }} variants</span></div><div class="variant-grid">{% for variant_name, variant_data in libre.items() %}{% if variant_data.success %}<div class="variant-item" data-text="{{ variant_data.text }}" data-kind="variant"><div class="variant-header"><span class="variant-name">{{ variant_name }}</span><span class="variant-code">{{ variant_data.code }}</span></div><div class="variant-text">{{ variant_data.text }}</div><svg class="check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" style="position:absolute;top:16px;right:16px"><polyline points="20 6 9 17 4 12"></polyline></svg></div>{% else %}<div class="variant-item error"><div class="variant-header"><span class="variant-name">{{ variant_name }}</span><span class="variant-code">{{ variant_data.code }}</span></div><div class="variant-text">{{ variant_data.text }}</div></div>{% endif %}{% endfor %}</div></div></div>{% else %}<div class="empty-state"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg><p>Enter text above and click Translate to see results</p></div>{% endif %}<div class="api-note">API available at <code>POST /api/translate</code> — see <a href="/api/docs">full API documentation</a>.<br>LibreTranslate-compatible endpoints: <code>/translate</code>, <code>/languages</code>, <code>/detect</code></div></div><div class="toast" id="toast">Copied to clipboard!</div><script>async function copyText(text){let copied=false;if(navigator.clipboard&&window.isSecureContext){try{await navigator.clipboard.writeText(text);copied=true}catch(err){copied=false}}if(!copied){try{const textArea=document.createElement('textarea');textArea.value=text;textArea.style.position='fixed';textArea.style.left='-9999px';document.body.appendChild(textArea);textArea.focus();textArea.select();copied=document.execCommand('copy');document.body.removeChild(textArea)}catch(err){copied=false}}return copied}function showToastMessage(message,isError=false){const toast=document.getElementById('toast');toast.textContent=message;toast.classList.toggle('error',isError);toast.classList.add('show');setTimeout(()=>toast.classList.remove('show'),2000)}function setupClickToCopy(){document.querySelectorAll('.translation-item[data-text]').forEach(item=>{item.addEventListener('click',async function(){document.querySelectorAll('.translation-item').forEach(el=>el.classList.remove('selected'));this.classList.add('selected');const copied=await copyText(this.dataset.text);showToastMessage(copied?'Copied to clipboard!':'Copy failed — please copy manually',!copied)})});document.querySelectorAll('.variant-item[data-text]').forEach(item=>{item.addEventListener('click',async function(){document.querySelectorAll('.variant-item').forEach(el=>el.classList.remove('selected'));this.classList.add('selected');const copied=await copyText(this.dataset.text);showToastMessage(copied?'Copied to clipboard!':'Copy failed — please copy manually',!copied)})})}document.addEventListener('DOMContentLoaded',setupClickToCopy);const translateForm=document.getElementById('translateForm');if(translateForm){translateForm.addEventListener('submit',function(){const btn=document.getElementById('submitBtn');btn.disabled=true;btn.innerHTML='<span class="loading"></span> Translating...'})}</script></body></html>"""
API_DOCS_TEMPLATE = """<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Kabyle Translation Hub - API Docs</title><style>:root{--bg-primary:#0f172a;--bg-secondary:#1e293b;--bg-tertiary:#334155;--accent-primary:#6366f1;--accent-secondary:#8b5cf6;--accent-success:#10b981;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--border:#475569}*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:linear-gradient(135deg,var(--bg-primary) 0%,#1a1a2e 100%);color:var(--text-primary);min-height:100vh;padding:20px}.container{max-width:860px;margin:0 auto;padding-bottom:60px}header{padding:40px 0 20px}header h1{font-size:2.2rem;font-weight:800;background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}header p{color:var(--text-secondary)}header a{color:var(--accent-primary);text-decoration:none}header a:hover{text-decoration:underline}.section{background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:24px;margin-bottom:20px}.section h2{font-size:1.2rem;margin-bottom:12px;color:var(--text-primary)}.section h2 .method{display:inline-block;font-size:.75rem;font-weight:700;padding:3px 10px;border-radius:6px;margin-right:10px;font-family:monospace}.method.get{background:rgba(16,185,129,.2);color:var(--accent-success)}.method.post{background:rgba(99,102,241,.2);color:var(--accent-primary)}p{color:var(--text-secondary);margin-bottom:12px;line-height:1.6}code.inline{background:var(--bg-tertiary);padding:2px 8px;border-radius:4px;font-family:monospace;color:var(--text-primary);font-size:.9em}pre{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:16px;overflow-x:auto;font-family:monospace;font-size:.85rem;line-height:1.6;color:#e2e8f0;margin-bottom:12px}table{width:100%;border-collapse:collapse;margin-bottom:12px}th,td{text-align:left;padding:8px 12px;border-bottom:1px solid var(--border);font-size:.9rem}th{color:var(--text-secondary);font-weight:600}td{color:var(--text-primary)}td code{font-family:monospace;color:var(--accent-primary)}.back-link{display:inline-block;margin-bottom:20px}</style></head><body><div class="container"><a class="back-link" href="/">&larr; Back to translator</a><header><h1>Kabyle Translation Hub — API Docs</h1><p>Base URL: <code class="inline">{{ base_url }}</code></p></header><div class="section"><h2><span class="method post">POST</span>/api/translate</h2><p>Native API for translation with multiple engines and variants.</p><table><tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr><tr><td><code>text</code></td><td>string</td><td>yes</td><td>English text to translate</td></tr><tr><td><code>engines</code></td><td>array</td><td>no</td><td>Subset of <code>["marian","libre"]</code>. Defaults to both.</td></tr><tr><td><code>variants</code></td><td>array</td><td>no</td><td>Subset of LibreTranslate variant codes. Defaults to all.</td></tr></table><pre>curl -X POST "{{ base_url }}/api/translate" \\\n -H "Content-Type: application/json" \\\n -d '{"text": "Good morning", "engines": ["marian", "libre"]}'</pre></div><div class="section"><h2><span class="method post">POST</span>/translate</h2><p><strong>LibreTranslate-compatible endpoint</strong> for Weblate and other tools.</p><table><tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr><tr><td><code>q</code></td><td>string or array</td><td>yes</td><td>Text to translate</td></tr><tr><td><code>source</code></td><td>string</td><td>yes</td><td>Source language code</td></tr><tr><td><code>target</code></td><td>string</td><td>yes</td><td>Target language code</td></tr><tr><td><code>api_key</code></td><td>string</td><td>no</td><td>Ignored</td></tr></table><pre>curl -X POST "{{ base_url }}/translate" \\\n -H "Content-Type: application/json" \\\n -d '{"q": "Good morning", "source": "en", "target": "kab"}'</pre><p><strong>Weblate config:</strong></p><pre>MT_LIBRETRANSLATE_API_URL = "{{ base_url }}"</pre></div><div class="section"><h2><span class="method get">GET</span>/languages</h2><p>Supported language pairs.</p><pre>curl "{{ base_url }}/languages"</pre></div><div class="section"><h2><span class="method post">POST</span>/detect</h2><p>Language detection stub.</p><pre>curl -X POST "{{ base_url }}/detect" -d "q=Hello world"</pre></div><div class="section"><h2><span class="method get">GET</span>/health</h2><p>Health check.</p><pre>curl "{{ base_url }}/health"</pre></div></div></body></html>"""
app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}})
# Global error handler - NEVER return 500
@app.errorhandler(Exception)
def handle_exception(e):
print(f"[GLOBAL ERROR] {type(e).__name__}: {e}", flush=True)
import traceback
traceback.print_exc()
return jsonify({"translatedText": "", "error": str(e)}), 200
# Pre-load model
try:
load_model()
except Exception as e:
print(f"Pre-load failed: {e}", flush=True)
@app.route("/", methods=["GET", "POST"])
def index():
marian = []
libre = {}
source_text = ""
if request.method == "POST":
source_text = request.form.get("text", "").strip()
if source_text:
marian = translate_marian(source_text)
libre_results = translate_libre_all_variants(source_text)
for name, data in libre_results.items():
data['code'] = KABYLE_VARIANTS[name]
libre[name] = data
return render_template_string(HTML_TEMPLATE, marian=marian, libre=libre, source_text=source_text)
def _extract_text(data):
q = data.get("q", "")
if isinstance(q, list):
return q[0] if q else ""
return str(q).strip()
@app.route("/translate", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
def libretranslate_translate():
"""
Weblate-compatible translation endpoint.
Uses ONLY MarianMT - no LibreTranslate fallback to avoid 429 errors.
NEVER returns 500.
"""
print(f"[WEblate] === /translate called ({request.method}) ===", flush=True)
if request.method == "GET":
return jsonify({"translatedText": ""}), 200
try:
raw_data = request.get_data(as_text=True)
print(f"[WEblate] Raw body: {raw_data[:500]}", flush=True)
except Exception as e:
print(f"[WEblate] Could not read raw body: {e}", flush=True)
raw_data = ""
data = {}
try:
json_data = request.get_json(silent=True, force=True)
if json_data:
data = json_data
except Exception as e:
print(f"[WEblate] JSON parse failed: {e}", flush=True)
if not data:
try:
form_data = request.form
if form_data:
data = dict(form_data)
except Exception as e:
print(f"[WEblate] Form parse failed: {e}", flush=True)
if not data and raw_data:
try:
import json
data = json.loads(raw_data)
except Exception as e:
print(f"[WEblate] Manual JSON parse failed: {e}", flush=True)
text = _extract_text(data)
source = str(data.get("source", "en")).strip() if data else "en"
target = str(data.get("target", "kab")).strip() if data else "kab"
print(f"[WEblate] text={text[:50]}, source={source}, target={target}", flush=True)
if not text:
return jsonify({"translatedText": ""}), 200
if source == "auto":
kab_chars = set("ɛƐɣƔṭḍčǧḥṣẓ")
source = "kab" if any(c in text.lower() for c in kab_chars) else "en"
if source != "en":
return jsonify({"translatedText": ""}), 200
# Only use MarianMT - no LibreTranslate fallback to avoid 429s
try:
marian_results = translate_marian(text)
if marian_results and not marian_results[0].startswith("[Error"):
return jsonify({"translatedText": marian_results[0]}), 200
except Exception as e:
print(f"[WEblate] MarianMT failed: {e}", flush=True)
return jsonify({"translatedText": ""}), 200
@app.route("/languages", methods=["GET"])
def libretranslate_languages():
try:
return jsonify([
{"code": "en", "name": "English", "targets": ["kab"]},
{"code": "kab", "name": "Kabyle", "targets": ["en"]}
])
except Exception as e:
print(f"[WEblate] /languages error: {e}", flush=True)
return jsonify([]), 200
@app.route("/detect", methods=["POST"])
def libretranslate_detect():
try:
data = request.get_json(silent=True) or {}
if not data:
data = dict(request.form)
text = _extract_text(data)
kab_chars = set("ɛƐɣƔṭḍčǧḥṣẓ")
detected = "kab" if any(c in text.lower() for c in kab_chars) else "en"
return jsonify([{"confidence": 0.99, "language": detected}])
except Exception as e:
print(f"[WEblate] /detect error: {e}", flush=True)
return jsonify([]), 200
@app.route("/frontend/settings", methods=["GET"])
def libretranslate_settings():
return jsonify({
"apiKeys": False,
"charLimit": 5000,
"dailyLimit": -1,
"frontendTimeout": 5000,
"keyRequired": False,
"language": {"source": {"code": "en", "name": "English"}, "target": {"code": "kab", "name": "Kabyle"}},
"suggestions": False,
"supportedFilesFormat": ["text", "html"],
"theme": "auto"
})
@app.route("/api/docs", methods=["GET"])
def api_docs():
base_url = request.url_root.rstrip("/")
return render_template_string(API_DOCS_TEMPLATE, base_url=base_url)
@app.route("/api/translate", methods=["GET", "POST"])
def api_translate():
if request.method == "POST":
payload = request.get_json(silent=True) or {}
text = payload.get("text") or request.form.get("text", "")
engines = payload.get("engines") or ["marian", "libre"]
variant_filter = payload.get("variants")
else:
text = request.args.get("text", "")
engines_param = request.args.get("engines", "marian,libre")
engines = [e.strip() for e in engines_param.split(",") if e.strip()]
variants_param = request.args.get("variants")
variant_filter = [v.strip() for v in variants_param.split(",")] if variants_param else None
text = (text or "").strip()
if not text:
return jsonify({"error": "Missing 'text' parameter"}), 400
engines = [e.lower() for e in engines]
result = {"source_text": text}
if "marian" in engines:
result["marian"] = translate_marian(text)
if "libre" in engines:
variants_to_use = KABYLE_VARIANTS
if variant_filter:
variants_to_use = {
name: code for name, code in KABYLE_VARIANTS.items()
if code in variant_filter or name in variant_filter
}
if not variants_to_use:
return jsonify({"error": f"No matching variants for: {variant_filter}"}), 400
libre_results = {}
for name, code in variants_to_use.items():
libre_results[name] = translate_libre_variant(text, code)
time.sleep(0.3)
for name, data in libre_results.items():
data["code"] = code
result["libre"] = libre_results
return jsonify(result)
@app.route("/api/variants", methods=["GET"])
def api_variants():
return jsonify(KABYLE_VARIANTS)
@app.route("/health")
def health():
return jsonify({
"status": "healthy",
"model_loaded": model is not None
})
if __name__ == "__main__":
port = int(os.environ.get("PORT", 7860))
app.run(host="0.0.0.0", port=port, debug=False)