aleks-gotsa commited on
Commit
0353da7
·
1 Parent(s): 21fafd4

feat: full-page language switch (UK/EN)

Browse files
Files changed (1) hide show
  1. app.py +65 -15
app.py CHANGED
@@ -4,8 +4,8 @@ Ukrainian official-letter decoder. Photo or pasted text in; plain-language
4
  summary, required actions, deadlines, and scam flags out. Fully local:
5
  Tesseract OCR + Qwen3-4B-Instruct-2507 (Q4_K_M) via llama.cpp. No cloud calls.
6
 
7
- Answer-language toggle (UK/EN): the product is Ukrainian-first for my parents;
8
- English answers exist so hackathon judges can evaluate output quality.
9
 
10
  Build Small Hackathon 2026 · Backyard AI track.
11
  """
@@ -54,6 +54,7 @@ EN = "English"
54
 
55
  T = {
56
  "uk": {
 
57
  "stamp_danger": "Схоже на шахрайство",
58
  "stamp_ok": "Виглядає як справжній лист",
59
  "h_summary": "Про що цей лист",
@@ -71,6 +72,20 @@ T = {
71
  "err_empty": "Спочатку додайте лист: сфотографуйте його або вставте текст у поле зліва.",
72
  "err_conn": "Помилка зв'язку з моделлю:",
73
  "err_json": "Модель повернула некоректну відповідь. Спробуйте ще раз.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  },
75
  "en": {
76
  "stamp_danger": "Looks like a scam",
@@ -90,6 +105,19 @@ T = {
90
  "err_empty": "Add a letter first: photograph it or paste the text on the left.",
91
  "err_conn": "Model connection error:",
92
  "err_json": "The model returned a malformed answer. Try again.",
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  },
94
  }
95
 
@@ -98,6 +126,15 @@ def lang_code(choice: str) -> str:
98
  return "en" if choice == EN else "uk"
99
 
100
 
 
 
 
 
 
 
 
 
 
101
  # ---------------------------------------------------------------- OCR path
102
 
103
 
@@ -318,6 +355,18 @@ def decode(letter_text: str, lang_choice: str):
318
  yield render_result(data, tok_s, t)
319
 
320
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  # --------------------------------------------------------------------- UI
322
 
323
  CSS = """
@@ -415,34 +464,30 @@ EXAMPLE_SCAM = """UA-BANK ПОВІДОМЛЕННЯ
415
  Служба безпеки банку."""
416
 
417
  with gr.Blocks(title="Paper Decoder — Розшифровувач паперів") as demo:
418
- gr.HTML(
419
- '<div class="pd-header">'
420
- '<div class="pd-wordmark">Розшифровувач <span class="accent">паперів</span></div>'
421
- '<div class="pd-tagline">Сфотографуйте офіційний лист — отримаєте просте пояснення, '
422
- "список дій, усі дати й суми та перевірку на шахрайство. "
423
- "Працює повністю на цьому сервері, лист нікуди не надсилається.</div></div>"
424
- )
425
  with gr.Row():
426
  with gr.Column(scale=5):
427
  image = gr.Image(
428
  type="filepath",
429
  sources=["upload"],
430
- label="Фото листа / Photo of the letter",
431
  height=260,
432
  )
433
  letter = gr.Textbox(
434
- label="Текст листа / Letter text",
435
- placeholder="…або вставте текст листа сюди",
436
  lines=10,
437
  )
438
  lang = gr.Radio(
439
  choices=[UK, EN],
440
  value=UK,
441
- label="Мова відповіді / Answer language",
442
  )
443
  with gr.Row():
444
- decode_btn = gr.Button("Розшифрувати лист", variant="primary", size="lg")
445
- clear_btn = gr.ClearButton([image, letter], value="Очистити")
 
 
446
  gr.Examples(
447
  examples=[[EXAMPLE_LEGIT], [EXAMPLE_SCAM]],
448
  inputs=[letter],
@@ -457,6 +502,11 @@ with gr.Blocks(title="Paper Decoder — Розшифровувач папері
457
  image.upload(do_ocr, inputs=[image], outputs=[letter])
458
  decode_btn.click(decode, inputs=[letter, lang], outputs=[result])
459
  letter.submit(decode, inputs=[letter, lang], outputs=[result])
 
 
 
 
 
460
 
461
  if __name__ == "__main__":
462
  demo.launch(css=CSS)
 
4
  summary, required actions, deadlines, and scam flags out. Fully local:
5
  Tesseract OCR + Qwen3-4B-Instruct-2507 (Q4_K_M) via llama.cpp. No cloud calls.
6
 
7
+ The language radio switches the whole page and the model's answer language.
8
+ Ukrainian-first for my parents; English exists so judges can evaluate output.
9
 
10
  Build Small Hackathon 2026 · Backyard AI track.
11
  """
 
54
 
55
  T = {
56
  "uk": {
57
+ # result rendering
58
  "stamp_danger": "Схоже на шахрайство",
59
  "stamp_ok": "Виглядає як справжній лист",
60
  "h_summary": "Про що цей лист",
 
72
  "err_empty": "Спочатку додайте лист: сфотографуйте його або вставте текст у поле зліва.",
73
  "err_conn": "Помилка зв'язку з моделлю:",
74
  "err_json": "Модель повернула некоректну відповідь. Спробуйте ще раз.",
75
+ # page UI
76
+ "wordmark": 'Розшифровувач <span class="accent">паперів</span>',
77
+ "tagline": (
78
+ "Сфотографуйте офіційний лист — отримаєте просте пояснення, "
79
+ "список дій, усі дати й суми та перевірку на шахрайство. "
80
+ "Працює повністю на цьому сервері, лист нікуди не надсилається."
81
+ ),
82
+ "lbl_image": "Фото листа",
83
+ "lbl_letter": "Текст листа",
84
+ "ph_letter": "…або вставте текст листа сюди",
85
+ "btn_decode": "Розшифрувати лист",
86
+ "btn_clear": "Очистити",
87
+ "lbl_examples": "Приклади",
88
+ "placeholder_result": "Тут з'явиться розшифровка листа.",
89
  },
90
  "en": {
91
  "stamp_danger": "Looks like a scam",
 
105
  "err_empty": "Add a letter first: photograph it or paste the text on the left.",
106
  "err_conn": "Model connection error:",
107
  "err_json": "The model returned a malformed answer. Try again.",
108
+ "wordmark": 'Paper <span class="accent">Decoder</span>',
109
+ "tagline": (
110
+ "Photograph an official Ukrainian letter — get a plain-language "
111
+ "explanation, an action list, every date and amount, and a scam "
112
+ "check. Runs entirely on this server; the letter is sent nowhere."
113
+ ),
114
+ "lbl_image": "Photo of the letter",
115
+ "lbl_letter": "Letter text",
116
+ "ph_letter": "…or paste the letter text here",
117
+ "btn_decode": "Decode the letter",
118
+ "btn_clear": "Clear",
119
+ "lbl_examples": "Examples",
120
+ "placeholder_result": "The decoded letter will appear here.",
121
  },
122
  }
123
 
 
126
  return "en" if choice == EN else "uk"
127
 
128
 
129
+ def header_html(lang: str) -> str:
130
+ t = T[lang]
131
+ return (
132
+ '<div class="pd-header">'
133
+ f'<div class="pd-wordmark">{t["wordmark"]}</div>'
134
+ f'<div class="pd-tagline">{html.escape(t["tagline"])}</div></div>'
135
+ )
136
+
137
+
138
  # ---------------------------------------------------------------- OCR path
139
 
140
 
 
355
  yield render_result(data, tok_s, t)
356
 
357
 
358
+ def switch_ui(lang_choice: str):
359
+ """Swap every static UI string when the language radio changes."""
360
+ t = T[lang_code(lang_choice)]
361
+ return (
362
+ header_html(lang_code(lang_choice)),
363
+ gr.update(label=t["lbl_image"]),
364
+ gr.update(label=t["lbl_letter"], placeholder=t["ph_letter"]),
365
+ gr.update(value=t["btn_decode"]),
366
+ gr.update(value=t["btn_clear"]),
367
+ )
368
+
369
+
370
  # --------------------------------------------------------------------- UI
371
 
372
  CSS = """
 
464
  Служба безпеки банку."""
465
 
466
  with gr.Blocks(title="Paper Decoder — Розшифровувач паперів") as demo:
467
+ header = gr.HTML(header_html("uk"))
 
 
 
 
 
 
468
  with gr.Row():
469
  with gr.Column(scale=5):
470
  image = gr.Image(
471
  type="filepath",
472
  sources=["upload"],
473
+ label=T["uk"]["lbl_image"],
474
  height=260,
475
  )
476
  letter = gr.Textbox(
477
+ label=T["uk"]["lbl_letter"],
478
+ placeholder=T["uk"]["ph_letter"],
479
  lines=10,
480
  )
481
  lang = gr.Radio(
482
  choices=[UK, EN],
483
  value=UK,
484
+ label="Мова / Language",
485
  )
486
  with gr.Row():
487
+ decode_btn = gr.Button(
488
+ T["uk"]["btn_decode"], variant="primary", size="lg"
489
+ )
490
+ clear_btn = gr.ClearButton([image, letter], value=T["uk"]["btn_clear"])
491
  gr.Examples(
492
  examples=[[EXAMPLE_LEGIT], [EXAMPLE_SCAM]],
493
  inputs=[letter],
 
502
  image.upload(do_ocr, inputs=[image], outputs=[letter])
503
  decode_btn.click(decode, inputs=[letter, lang], outputs=[result])
504
  letter.submit(decode, inputs=[letter, lang], outputs=[result])
505
+ lang.change(
506
+ switch_ui,
507
+ inputs=[lang],
508
+ outputs=[header, image, letter, decode_btn, clear_btn],
509
+ )
510
 
511
  if __name__ == "__main__":
512
  demo.launch(css=CSS)