Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -103,20 +103,21 @@ def translate_libre_all_variants(text):
|
|
| 103 |
return results
|
| 104 |
|
| 105 |
|
| 106 |
-
# --- HTML TEMPLATES
|
| 107 |
-
HTML_TEMPLATE = """<!DOCTYPE html><html lang="en"><head>...</head><body>...</body></html>"""
|
| 108 |
-
|
|
|
|
| 109 |
|
| 110 |
app = Flask(__name__)
|
| 111 |
CORS(app, resources={r"/*": {"origins": "*"}})
|
| 112 |
|
| 113 |
-
# Global error handler
|
| 114 |
@app.errorhandler(Exception)
|
| 115 |
def handle_exception(e):
|
| 116 |
print(f"[GLOBAL ERROR] {type(e).__name__}: {e}", flush=True)
|
| 117 |
import traceback
|
| 118 |
traceback.print_exc()
|
| 119 |
-
return jsonify({"error": str(e)}),
|
| 120 |
|
| 121 |
# Pre-load model
|
| 122 |
try:
|
|
@@ -141,10 +142,6 @@ def index():
|
|
| 141 |
return render_template_string(HTML_TEMPLATE, marian=marian, libre=libre, source_text=source_text)
|
| 142 |
|
| 143 |
|
| 144 |
-
# =============================================================================
|
| 145 |
-
# LIBRETRANSLATE-COMPATIBLE ENDPOINTS
|
| 146 |
-
# =============================================================================
|
| 147 |
-
|
| 148 |
def _extract_text(data):
|
| 149 |
"""Extract text from q field. Weblate sends q as either string or list."""
|
| 150 |
q = data.get("q", "")
|
|
@@ -153,18 +150,17 @@ def _extract_text(data):
|
|
| 153 |
return str(q).strip()
|
| 154 |
|
| 155 |
|
| 156 |
-
@app.route("/translate", methods=["GET", "POST"])
|
| 157 |
def libretranslate_translate():
|
| 158 |
"""
|
| 159 |
LibreTranslate-compatible translation endpoint.
|
| 160 |
-
|
| 161 |
"""
|
| 162 |
print(f"[WEblate] === /translate called ({request.method}) ===", flush=True)
|
| 163 |
-
print(f"[WEblate] Content-Type: {request.content_type}", flush=True)
|
| 164 |
|
|
|
|
| 165 |
if request.method == "GET":
|
| 166 |
-
|
| 167 |
-
return jsonify({"message": "POST required for translation"}), 200
|
| 168 |
|
| 169 |
try:
|
| 170 |
raw_data = request.get_data(as_text=True)
|
|
@@ -211,8 +207,8 @@ def libretranslate_translate():
|
|
| 211 |
print(f"[WEblate] text={text[:50]}, source={source}, target={target}", flush=True)
|
| 212 |
|
| 213 |
if not text:
|
| 214 |
-
print(f"[WEblate]
|
| 215 |
-
return jsonify({"
|
| 216 |
|
| 217 |
# Handle auto source
|
| 218 |
if source == "auto":
|
|
@@ -249,13 +245,14 @@ def libretranslate_translate():
|
|
| 249 |
|
| 250 |
response = {"translatedText": best_translation or ""}
|
| 251 |
print(f"[WEblate] SUCCESS: {response}", flush=True)
|
| 252 |
-
return jsonify(response)
|
| 253 |
|
| 254 |
except Exception as e:
|
| 255 |
print(f"[WEblate] Translation failed: {e}", flush=True)
|
| 256 |
import traceback
|
| 257 |
traceback.print_exc()
|
| 258 |
-
|
|
|
|
| 259 |
|
| 260 |
|
| 261 |
@app.route("/languages", methods=["GET"])
|
|
@@ -267,7 +264,7 @@ def libretranslate_languages():
|
|
| 267 |
])
|
| 268 |
except Exception as e:
|
| 269 |
print(f"[WEblate] /languages error: {e}", flush=True)
|
| 270 |
-
return jsonify([]),
|
| 271 |
|
| 272 |
|
| 273 |
@app.route("/detect", methods=["POST"])
|
|
@@ -282,35 +279,24 @@ def libretranslate_detect():
|
|
| 282 |
return jsonify([{"confidence": 0.99, "language": detected}])
|
| 283 |
except Exception as e:
|
| 284 |
print(f"[WEblate] /detect error: {e}", flush=True)
|
| 285 |
-
return jsonify([]),
|
| 286 |
|
| 287 |
|
| 288 |
@app.route("/frontend/settings", methods=["GET"])
|
| 289 |
def libretranslate_settings():
|
| 290 |
-
"""
|
| 291 |
-
LibreTranslate frontend settings endpoint.
|
| 292 |
-
Some clients check this for API capabilities.
|
| 293 |
-
"""
|
| 294 |
return jsonify({
|
| 295 |
"apiKeys": False,
|
| 296 |
"charLimit": 5000,
|
| 297 |
"dailyLimit": -1,
|
| 298 |
"frontendTimeout": 5000,
|
| 299 |
"keyRequired": False,
|
| 300 |
-
"language": {
|
| 301 |
-
"source": {"code": "en", "name": "English"},
|
| 302 |
-
"target": {"code": "kab", "name": "Kabyle"}
|
| 303 |
-
},
|
| 304 |
"suggestions": False,
|
| 305 |
"supportedFilesFormat": ["text", "html"],
|
| 306 |
"theme": "auto"
|
| 307 |
})
|
| 308 |
|
| 309 |
|
| 310 |
-
# =============================================================================
|
| 311 |
-
# NATIVE API
|
| 312 |
-
# =============================================================================
|
| 313 |
-
|
| 314 |
@app.route("/api/docs", methods=["GET"])
|
| 315 |
def api_docs():
|
| 316 |
base_url = request.url_root.rstrip("/")
|
|
|
|
| 103 |
return results
|
| 104 |
|
| 105 |
|
| 106 |
+
# --- HTML TEMPLATES ---
|
| 107 |
+
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>"""
|
| 108 |
+
|
| 109 |
+
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="/">← 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>"""
|
| 110 |
|
| 111 |
app = Flask(__name__)
|
| 112 |
CORS(app, resources={r"/*": {"origins": "*"}})
|
| 113 |
|
| 114 |
+
# Global error handler - NEVER return 500 to clients
|
| 115 |
@app.errorhandler(Exception)
|
| 116 |
def handle_exception(e):
|
| 117 |
print(f"[GLOBAL ERROR] {type(e).__name__}: {e}", flush=True)
|
| 118 |
import traceback
|
| 119 |
traceback.print_exc()
|
| 120 |
+
return jsonify({"translatedText": "", "error": str(e)}), 200
|
| 121 |
|
| 122 |
# Pre-load model
|
| 123 |
try:
|
|
|
|
| 142 |
return render_template_string(HTML_TEMPLATE, marian=marian, libre=libre, source_text=source_text)
|
| 143 |
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
def _extract_text(data):
|
| 146 |
"""Extract text from q field. Weblate sends q as either string or list."""
|
| 147 |
q = data.get("q", "")
|
|
|
|
| 150 |
return str(q).strip()
|
| 151 |
|
| 152 |
|
| 153 |
+
@app.route("/translate", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
|
| 154 |
def libretranslate_translate():
|
| 155 |
"""
|
| 156 |
LibreTranslate-compatible translation endpoint.
|
| 157 |
+
NEVER returns 500 - always returns 200 with either translation or empty string.
|
| 158 |
"""
|
| 159 |
print(f"[WEblate] === /translate called ({request.method}) ===", flush=True)
|
|
|
|
| 160 |
|
| 161 |
+
# GET requests - just return empty translation
|
| 162 |
if request.method == "GET":
|
| 163 |
+
return jsonify({"translatedText": ""}), 200
|
|
|
|
| 164 |
|
| 165 |
try:
|
| 166 |
raw_data = request.get_data(as_text=True)
|
|
|
|
| 207 |
print(f"[WEblate] text={text[:50]}, source={source}, target={target}", flush=True)
|
| 208 |
|
| 209 |
if not text:
|
| 210 |
+
print(f"[WEblate] Empty text, returning empty translation", flush=True)
|
| 211 |
+
return jsonify({"translatedText": ""}), 200
|
| 212 |
|
| 213 |
# Handle auto source
|
| 214 |
if source == "auto":
|
|
|
|
| 245 |
|
| 246 |
response = {"translatedText": best_translation or ""}
|
| 247 |
print(f"[WEblate] SUCCESS: {response}", flush=True)
|
| 248 |
+
return jsonify(response), 200
|
| 249 |
|
| 250 |
except Exception as e:
|
| 251 |
print(f"[WEblate] Translation failed: {e}", flush=True)
|
| 252 |
import traceback
|
| 253 |
traceback.print_exc()
|
| 254 |
+
# NEVER return 500
|
| 255 |
+
return jsonify({"translatedText": ""}), 200
|
| 256 |
|
| 257 |
|
| 258 |
@app.route("/languages", methods=["GET"])
|
|
|
|
| 264 |
])
|
| 265 |
except Exception as e:
|
| 266 |
print(f"[WEblate] /languages error: {e}", flush=True)
|
| 267 |
+
return jsonify([]), 200
|
| 268 |
|
| 269 |
|
| 270 |
@app.route("/detect", methods=["POST"])
|
|
|
|
| 279 |
return jsonify([{"confidence": 0.99, "language": detected}])
|
| 280 |
except Exception as e:
|
| 281 |
print(f"[WEblate] /detect error: {e}", flush=True)
|
| 282 |
+
return jsonify([]), 200
|
| 283 |
|
| 284 |
|
| 285 |
@app.route("/frontend/settings", methods=["GET"])
|
| 286 |
def libretranslate_settings():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
return jsonify({
|
| 288 |
"apiKeys": False,
|
| 289 |
"charLimit": 5000,
|
| 290 |
"dailyLimit": -1,
|
| 291 |
"frontendTimeout": 5000,
|
| 292 |
"keyRequired": False,
|
| 293 |
+
"language": {"source": {"code": "en", "name": "English"}, "target": {"code": "kab", "name": "Kabyle"}},
|
|
|
|
|
|
|
|
|
|
| 294 |
"suggestions": False,
|
| 295 |
"supportedFilesFormat": ["text", "html"],
|
| 296 |
"theme": "auto"
|
| 297 |
})
|
| 298 |
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
@app.route("/api/docs", methods=["GET"])
|
| 301 |
def api_docs():
|
| 302 |
base_url = request.url_root.rstrip("/")
|