boffire commited on
Commit
bc9c55d
·
verified ·
1 Parent(s): 5d2113e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +700 -0
app.py ADDED
@@ -0,0 +1,700 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Kabyle Translation Hub - Hugging Face Spaces Edition
3
+ Simple translation interface: MarianMT vs LibreTranslate
4
+ Users choose their preferred translation - no metrics, no scores.
5
+ """
6
+
7
+ import os
8
+ import requests
9
+ import torch
10
+ from flask import Flask, request, render_template_string, jsonify
11
+ from transformers import MarianMTModel, MarianTokenizer
12
+ from concurrent.futures import ThreadPoolExecutor, as_completed
13
+
14
+ # Configuration
15
+ LIBRETRANSLATE_URL = os.environ.get("LIBRETRANSLATE_URL", "https://imsidag-community-libretranslate-kabyle.hf.space/translate")
16
+ MODEL_REPO = os.environ.get("MODEL_REPO", "bofenghuang/marianmt-en-kab")
17
+
18
+ # LibreTranslate Kabyle variants
19
+ KABYLE_VARIANTS = {
20
+ "Taqbaylit (Standard)": "kab",
21
+ "Taqbaylit (Latest)": "kab_kab",
22
+ "Taqbaylit (Tasenselkimt)": "kab_comp",
23
+ "Taqbaylit (51000)": "kab_comp2",
24
+ "Taqbaylit (OS)": "kab_os",
25
+ "Taqbaylit (Num)": "kab_num",
26
+ }
27
+
28
+ # Global variables for model
29
+ model = None
30
+ tokenizer = None
31
+ device = None
32
+
33
+ def load_model():
34
+ """Load MarianMT model from Hugging Face Hub"""
35
+ global model, tokenizer, device
36
+
37
+ if model is not None:
38
+ return True
39
+
40
+ try:
41
+ print("Loading MarianMT model from Hugging Face Hub...")
42
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
43
+
44
+ tokenizer = MarianTokenizer.from_pretrained(MODEL_REPO)
45
+ model = MarianMTModel.from_pretrained(MODEL_REPO).to(device).eval()
46
+
47
+ print(f"Model loaded successfully on {device}!")
48
+ return True
49
+ except Exception as e:
50
+ print(f"Error loading model: {e}")
51
+ return False
52
+
53
+ # Load model on startup (with error handling)
54
+ model_loaded = load_model()
55
+
56
+ def translate_marian(text):
57
+ """Translate using MarianMT with multiple alternatives"""
58
+ if not model_loaded or model is None:
59
+ return ["[Error: Model not loaded. Please check logs.]"]
60
+
61
+ try:
62
+ inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
63
+ inputs = {k: v.to(device) for k, v in inputs.items()}
64
+
65
+ with torch.no_grad():
66
+ outputs = model.generate(
67
+ **inputs,
68
+ num_beams=6,
69
+ num_beam_groups=3,
70
+ diversity_penalty=1.2,
71
+ num_return_sequences=3,
72
+ max_length=128,
73
+ early_stopping=True,
74
+ )
75
+
76
+ translations = []
77
+ for output in outputs:
78
+ trans = tokenizer.decode(output, skip_special_tokens=True)
79
+ if trans not in translations: # Avoid duplicates
80
+ translations.append(trans)
81
+
82
+ return translations if translations else ["[Error: No translation generated]"]
83
+
84
+ except Exception as e:
85
+ print(f"MarianMT translation error: {e}")
86
+ return [f"[Error: {str(e)}"]
87
+
88
+ def translate_libre_variant(text, variant_code):
89
+ """Translate using a specific LibreTranslate variant"""
90
+ try:
91
+ r = requests.post(
92
+ LIBRETRANSLATE_URL,
93
+ headers={"Content-Type": "application/json"},
94
+ json={"q": text, "source": "en", "target": variant_code},
95
+ timeout=10
96
+ )
97
+ r.raise_for_status()
98
+ result = r.json().get("translatedText", "[Error: No translation]")
99
+ return {"success": True, "text": result}
100
+ except Exception as e:
101
+ return {"success": False, "text": f"[Error: {str(e)[:50]}]"}
102
+
103
+ def translate_libre_all_variants(text):
104
+ """Translate using all LibreTranslate variants in parallel"""
105
+ results = {}
106
+ with ThreadPoolExecutor(max_workers=4) as executor:
107
+ future_to_name = {
108
+ executor.submit(translate_libre_variant, text, code): name
109
+ for name, code in KABYLE_VARIANTS.items()
110
+ }
111
+ for future in as_completed(future_to_name, timeout=15):
112
+ name = future_to_name[future]
113
+ try:
114
+ results[name] = future.result()
115
+ except Exception as e:
116
+ results[name] = {"success": False, "text": f"[Error: {e}]"}
117
+ return results
118
+
119
+ HTML_TEMPLATE = """
120
+ <!DOCTYPE html>
121
+ <html lang="en">
122
+ <head>
123
+ <meta charset="UTF-8">
124
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
125
+ <title>Kabyle Translation Hub</title>
126
+ <style>
127
+ :root {
128
+ --bg-primary: #0f172a;
129
+ --bg-secondary: #1e293b;
130
+ --bg-tertiary: #334155;
131
+ --accent-primary: #6366f1;
132
+ --accent-secondary: #8b5cf6;
133
+ --accent-success: #10b981;
134
+ --text-primary: #f1f5f9;
135
+ --text-secondary: #94a3b8;
136
+ --border: #475569;
137
+ --radius: 12px;
138
+ --radius-sm: 8px;
139
+ }
140
+
141
+ * { margin: 0; padding: 0; box-sizing: border-box; }
142
+
143
+ body {
144
+ font-family: system-ui, -apple-system, sans-serif;
145
+ background: linear-gradient(135deg, var(--bg-primary) 0%, #1a1a2e 100%);
146
+ color: var(--text-primary);
147
+ min-height: 100vh;
148
+ padding: 20px;
149
+ }
150
+
151
+ .container {
152
+ max-width: 1000px;
153
+ margin: 0 auto;
154
+ }
155
+
156
+ header {
157
+ text-align: center;
158
+ padding: 40px 20px;
159
+ }
160
+
161
+ header h1 {
162
+ font-size: 2.5rem;
163
+ font-weight: 800;
164
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
165
+ -webkit-background-clip: text;
166
+ -webkit-text-fill-color: transparent;
167
+ margin-bottom: 10px;
168
+ }
169
+
170
+ header p {
171
+ color: var(--text-secondary);
172
+ font-size: 1.1rem;
173
+ }
174
+
175
+ .input-card {
176
+ background: var(--bg-secondary);
177
+ border-radius: var(--radius);
178
+ padding: 24px;
179
+ margin-bottom: 24px;
180
+ border: 1px solid var(--border);
181
+ }
182
+
183
+ .input-group {
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: 16px;
187
+ }
188
+
189
+ textarea {
190
+ width: 100%;
191
+ padding: 16px;
192
+ background: var(--bg-tertiary);
193
+ border: 2px solid var(--border);
194
+ border-radius: var(--radius-sm);
195
+ color: var(--text-primary);
196
+ font-size: 16px;
197
+ resize: vertical;
198
+ min-height: 120px;
199
+ font-family: inherit;
200
+ transition: border-color 0.2s;
201
+ }
202
+
203
+ textarea:focus {
204
+ outline: none;
205
+ border-color: var(--accent-primary);
206
+ }
207
+
208
+ textarea::placeholder {
209
+ color: var(--text-secondary);
210
+ }
211
+
212
+ .btn-primary {
213
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
214
+ color: white;
215
+ border: none;
216
+ padding: 16px 32px;
217
+ border-radius: var(--radius-sm);
218
+ font-size: 16px;
219
+ font-weight: 600;
220
+ cursor: pointer;
221
+ transition: transform 0.2s, box-shadow 0.2s;
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ gap: 8px;
226
+ }
227
+
228
+ .btn-primary:hover {
229
+ transform: translateY(-2px);
230
+ box-shadow: 0 10px 30px rgba(99, 102, 241, 0.3);
231
+ }
232
+
233
+ .btn-primary:disabled {
234
+ opacity: 0.6;
235
+ cursor: not-allowed;
236
+ transform: none;
237
+ }
238
+
239
+ .results-container {
240
+ display: flex;
241
+ flex-direction: column;
242
+ gap: 20px;
243
+ }
244
+
245
+ .engine-card {
246
+ background: var(--bg-secondary);
247
+ border-radius: var(--radius);
248
+ border: 1px solid var(--border);
249
+ overflow: hidden;
250
+ }
251
+
252
+ .engine-header {
253
+ padding: 20px 24px;
254
+ background: var(--bg-tertiary);
255
+ border-bottom: 1px solid var(--border);
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: space-between;
259
+ }
260
+
261
+ .engine-title {
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 12px;
265
+ font-size: 1.1rem;
266
+ font-weight: 600;
267
+ }
268
+
269
+ .engine-badge {
270
+ font-size: 0.75rem;
271
+ padding: 4px 12px;
272
+ background: var(--bg-secondary);
273
+ border-radius: 20px;
274
+ color: var(--text-secondary);
275
+ border: 1px solid var(--border);
276
+ }
277
+
278
+ .translation-list {
279
+ list-style: none;
280
+ }
281
+
282
+ .translation-item {
283
+ padding: 20px 24px;
284
+ border-bottom: 1px solid var(--border);
285
+ cursor: pointer;
286
+ transition: all 0.2s;
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: space-between;
290
+ gap: 16px;
291
+ }
292
+
293
+ .translation-item:last-child {
294
+ border-bottom: none;
295
+ }
296
+
297
+ .translation-item:hover {
298
+ background: rgba(99, 102, 241, 0.1);
299
+ }
300
+
301
+ .translation-item.selected {
302
+ background: rgba(16, 185, 129, 0.15);
303
+ border-left: 4px solid var(--accent-success);
304
+ }
305
+
306
+ .translation-text {
307
+ flex: 1;
308
+ font-size: 1.05rem;
309
+ line-height: 1.6;
310
+ color: var(--text-primary);
311
+ }
312
+
313
+ .copy-icon {
314
+ opacity: 0;
315
+ color: var(--accent-primary);
316
+ transition: opacity 0.2s;
317
+ flex-shrink: 0;
318
+ }
319
+
320
+ .translation-item:hover .copy-icon {
321
+ opacity: 1;
322
+ }
323
+
324
+ .variant-grid {
325
+ display: grid;
326
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
327
+ gap: 1px;
328
+ background: var(--border);
329
+ }
330
+
331
+ .variant-item {
332
+ background: var(--bg-secondary);
333
+ padding: 16px 20px;
334
+ cursor: pointer;
335
+ transition: all 0.2s;
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: 8px;
339
+ }
340
+
341
+ .variant-item:hover {
342
+ background: rgba(99, 102, 241, 0.1);
343
+ }
344
+
345
+ .variant-item.selected {
346
+ background: rgba(16, 185, 129, 0.15);
347
+ box-shadow: inset 4px 0 0 var(--accent-success);
348
+ }
349
+
350
+ .variant-header {
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: space-between;
354
+ }
355
+
356
+ .variant-name {
357
+ font-size: 0.75rem;
358
+ color: var(--text-secondary);
359
+ text-transform: uppercase;
360
+ letter-spacing: 0.5px;
361
+ font-weight: 600;
362
+ }
363
+
364
+ .variant-code {
365
+ font-size: 0.7rem;
366
+ color: var(--text-secondary);
367
+ font-family: monospace;
368
+ background: var(--bg-tertiary);
369
+ padding: 2px 8px;
370
+ border-radius: 4px;
371
+ }
372
+
373
+ .variant-text {
374
+ font-size: 1rem;
375
+ color: var(--text-primary);
376
+ line-height: 1.5;
377
+ }
378
+
379
+ .variant-item.error {
380
+ opacity: 0.6;
381
+ }
382
+
383
+ .variant-item.error .variant-text {
384
+ color: #ef4444;
385
+ font-family: monospace;
386
+ font-size: 0.85rem;
387
+ }
388
+
389
+ .source-display {
390
+ background: var(--bg-tertiary);
391
+ padding: 20px 24px;
392
+ border-radius: var(--radius);
393
+ margin-bottom: 20px;
394
+ border: 1px solid var(--border);
395
+ }
396
+
397
+ .source-label {
398
+ font-size: 0.75rem;
399
+ color: var(--text-secondary);
400
+ text-transform: uppercase;
401
+ letter-spacing: 0.5px;
402
+ margin-bottom: 8px;
403
+ }
404
+
405
+ .source-text {
406
+ font-size: 1.1rem;
407
+ color: var(--text-primary);
408
+ line-height: 1.6;
409
+ }
410
+
411
+ .check-icon {
412
+ color: var(--accent-success);
413
+ opacity: 0;
414
+ transition: opacity 0.2s;
415
+ }
416
+
417
+ .translation-item.selected .check-icon,
418
+ .variant-item.selected .check-icon {
419
+ opacity: 1;
420
+ }
421
+
422
+ .toast {
423
+ position: fixed;
424
+ bottom: 24px;
425
+ left: 50%;
426
+ transform: translateX(-50%) translateY(100px);
427
+ background: var(--accent-success);
428
+ color: white;
429
+ padding: 12px 24px;
430
+ border-radius: 100px;
431
+ font-weight: 600;
432
+ opacity: 0;
433
+ transition: all 0.3s ease;
434
+ z-index: 1000;
435
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
436
+ }
437
+
438
+ .toast.show {
439
+ transform: translateX(-50%) translateY(0);
440
+ opacity: 1;
441
+ }
442
+
443
+ .loading {
444
+ display: inline-block;
445
+ width: 18px;
446
+ height: 18px;
447
+ border: 2px solid rgba(255,255,255,0.3);
448
+ border-radius: 50%;
449
+ border-top-color: white;
450
+ animation: spin 0.8s linear infinite;
451
+ }
452
+
453
+ @keyframes spin {
454
+ to { transform: rotate(360deg); }
455
+ }
456
+
457
+ .empty-state {
458
+ text-align: center;
459
+ padding: 60px 20px;
460
+ color: var(--text-secondary);
461
+ }
462
+
463
+ .empty-state svg {
464
+ width: 64px;
465
+ height: 64px;
466
+ margin-bottom: 16px;
467
+ opacity: 0.5;
468
+ }
469
+
470
+ @media (max-width: 640px) {
471
+ header h1 {
472
+ font-size: 1.75rem;
473
+ }
474
+
475
+ .variant-grid {
476
+ grid-template-columns: 1fr;
477
+ }
478
+ }
479
+ </style>
480
+ </head>
481
+ <body>
482
+ <div class="container">
483
+ <header>
484
+ <h1>English → Kabyle</h1>
485
+ <p>Choose the translation that suits you best</p>
486
+ </header>
487
+
488
+ <div class="input-card">
489
+ <form method="POST" id="translateForm">
490
+ <div class="input-group">
491
+ <textarea
492
+ name="text"
493
+ id="inputText"
494
+ placeholder="Enter English text to translate..."
495
+ required
496
+ autocomplete="off"
497
+ >{{ request.form.get('text', '') }}</textarea>
498
+ <button type="submit" class="btn-primary" id="submitBtn">
499
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
500
+ <line x1="22" y1="2" x2="11" y2="13"></line>
501
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
502
+ </svg>
503
+ <span>Translate</span>
504
+ </button>
505
+ </div>
506
+ </form>
507
+ </div>
508
+
509
+ {% if source_text %}
510
+ <div class="source-display">
511
+ <div class="source-label">Source Text</div>
512
+ <div class="source-text">{{ source_text }}</div>
513
+ </div>
514
+
515
+ <div class="results-container">
516
+ <!-- MarianMT Results -->
517
+ <div class="engine-card">
518
+ <div class="engine-header">
519
+ <div class="engine-title">
520
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--accent-primary)">
521
+ <rect x="4" y="4" width="16" height="16" rx="2"></rect>
522
+ <rect x="9" y="9" width="6" height="6"></rect>
523
+ </svg>
524
+ MarianMT (Neural)
525
+ </div>
526
+ <span class="engine-badge">{{ marian|length }} options</span>
527
+ </div>
528
+ <ul class="translation-list">
529
+ {% for trans in marian %}
530
+ <li class="translation-item" onclick="selectTranslation(this, {{ trans | tojson }})" ondblclick="copyText({{ trans | tojson }})">
531
+ <span class="translation-text">{{ trans }}</span>
532
+ <svg class="check-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
533
+ <polyline points="20 6 9 17 4 12"></polyline>
534
+ </svg>
535
+ <svg class="copy-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
536
+ <rect x="9" y="9" width="13" height="13" rx="2"></rect>
537
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
538
+ </svg>
539
+ </li>
540
+ {% endfor %}
541
+ </ul>
542
+ </div>
543
+
544
+ <!-- LibreTranslate Results -->
545
+ <div class="engine-card">
546
+ <div class="engine-header">
547
+ <div class="engine-title">
548
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--accent-success)">
549
+ <circle cx="12" cy="12" r="10"></circle>
550
+ <line x1="2" y1="12" x2="22" y2="12"></line>
551
+ <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>
552
+ </svg>
553
+ LibreTranslate Variants
554
+ </div>
555
+ <span class="engine-badge">{{ libre|length }} variants</span>
556
+ </div>
557
+ <div class="variant-grid">
558
+ {% for variant_name, variant_data in libre.items() %}
559
+ {% if variant_data.success %}
560
+ <div class="variant-item" onclick="selectVariant(this, {{ variant_data.text | tojson }})" ondblclick="copyText({{ variant_data.text | tojson }})">
561
+ <div class="variant-header">
562
+ <span class="variant-name">{{ variant_name }}</span>
563
+ <span class="variant-code">{{ variant_data.code }}</span>
564
+ </div>
565
+ <div class="variant-text">{{ variant_data.text }}</div>
566
+ <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;">
567
+ <polyline points="20 6 9 17 4 12"></polyline>
568
+ </svg>
569
+ </div>
570
+ {% else %}
571
+ <div class="variant-item error">
572
+ <div class="variant-header">
573
+ <span class="variant-name">{{ variant_name }}</span>
574
+ <span class="variant-code">{{ variant_data.code }}</span>
575
+ </div>
576
+ <div class="variant-text">{{ variant_data.text }}</div>
577
+ </div>
578
+ {% endif %}
579
+ {% endfor %}
580
+ </div>
581
+ </div>
582
+ </div>
583
+ {% else %}
584
+ <div class="empty-state">
585
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
586
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
587
+ </svg>
588
+ <p>Enter text above and click Translate to see results</p>
589
+ </div>
590
+ {% endif %}
591
+ </div>
592
+
593
+ <div class="toast" id="toast">Copied to clipboard!</div>
594
+
595
+ <script>
596
+ let selectedText = '';
597
+
598
+ function selectTranslation(element, text) {
599
+ // Remove selected class from all items in this list
600
+ element.parentElement.querySelectorAll('.translation-item').forEach(item => {
601
+ item.classList.remove('selected');
602
+ });
603
+ // Add selected class to clicked item
604
+ element.classList.add('selected');
605
+ selectedText = text;
606
+
607
+ // Copy automatically on selection
608
+ copyText(text, false);
609
+ }
610
+
611
+ function selectVariant(element, text) {
612
+ // Remove selected class from all variants
613
+ document.querySelectorAll('.variant-item').forEach(item => {
614
+ item.classList.remove('selected');
615
+ });
616
+ // Add selected class to clicked item
617
+ element.classList.add('selected');
618
+ selectedText = text;
619
+
620
+ // Copy automatically on selection
621
+ copyText(text, false);
622
+ }
623
+
624
+ async function copyText(text, showToast = true) {
625
+ try {
626
+ await navigator.clipboard.writeText(text);
627
+ if (showToast) {
628
+ showToastMessage('Copied to clipboard!');
629
+ }
630
+ } catch (err) {
631
+ // Fallback
632
+ const textArea = document.createElement('textarea');
633
+ textArea.value = text;
634
+ document.body.appendChild(textArea);
635
+ textArea.select();
636
+ document.execCommand('copy');
637
+ document.body.removeChild(textArea);
638
+ if (showToast) {
639
+ showToastMessage('Copied to clipboard!');
640
+ }
641
+ }
642
+ }
643
+
644
+ function showToastMessage(message) {
645
+ const toast = document.getElementById('toast');
646
+ toast.textContent = message;
647
+ toast.classList.add('show');
648
+ setTimeout(() => toast.classList.remove('show'), 2000);
649
+ }
650
+
651
+ document.getElementById('translateForm').addEventListener('submit', function() {
652
+ const btn = document.getElementById('submitBtn');
653
+ btn.disabled = true;
654
+ btn.innerHTML = '<span class="loading"></span> Translating...';
655
+ });
656
+ </script>
657
+ </body>
658
+ </html>
659
+ """
660
+
661
+ app = Flask(__name__)
662
+
663
+ @app.route("/", methods=["GET", "POST"])
664
+ def index():
665
+ marian = []
666
+ libre = {}
667
+ source_text = ""
668
+
669
+ if request.method == "POST":
670
+ source_text = request.form.get("text", "").strip()
671
+ if source_text:
672
+ # Get Marian translations
673
+ marian = translate_marian(source_text)
674
+
675
+ # Get Libre translations
676
+ libre_results = translate_libre_all_variants(source_text)
677
+ for name, data in libre_results.items():
678
+ data['code'] = KABYLE_VARIANTS[name]
679
+ libre[name] = data
680
+
681
+ return render_template_string(
682
+ HTML_TEMPLATE,
683
+ marian=marian,
684
+ libre=libre,
685
+ source_text=source_text
686
+ )
687
+
688
+ @app.route("/health")
689
+ def health():
690
+ """Health check endpoint"""
691
+ return jsonify({
692
+ "status": "healthy",
693
+ "model_loaded": model_loaded,
694
+ "device": str(device) if device else "none"
695
+ })
696
+
697
+ if __name__ == "__main__":
698
+ # Hugging Face Spaces expects port 7860
699
+ port = int(os.environ.get("PORT", 7860))
700
+ app.run(host="0.0.0.0", port=port, debug=False)