nofater commited on
Commit
5641b64
·
verified ·
1 Parent(s): 2a0825b
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. README.md +21 -4
  3. app.py +45 -10
  4. assets/ankira.png +3 -0
  5. assets/ankira_blue.png +0 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/ankira.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: German Dictation Trainer
3
  emoji: ✍️
4
  colorFrom: indigo
5
  colorTo: green
@@ -9,12 +9,27 @@ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- # German Dictation Trainer
 
 
 
 
13
 
14
  A mobile-first dictation trainer. Enter the words you want to practice, get a
15
  short German dictation read aloud, write it by hand, then photograph your page
16
  to see a word-level diff against the original.
17
 
 
 
 
 
 
 
 
 
 
 
 
18
  No models run in this Space — every inference is an HTTP call to a Modal
19
  endpoint. Configure these as **Space secrets**:
20
 
@@ -24,5 +39,7 @@ endpoint. Configure these as **Space secrets**:
24
  | `MODAL_TTS_URL` | Higgs Audio v3 TTS (`/v1/audio/speech`; base URL also accepted) |
25
  | `MODAL_OCR_URL` | Qwen3-VL llama-server base URL (multimodal chat completions) |
26
 
27
- The recognized-text box is shown on purpose: handwriting OCR is imperfect, so
28
- seeing what was read keeps OCR errors distinguishable from your own.
 
 
 
1
  ---
2
+ title: Ankira - German Dictation Trainer
3
  emoji: ✍️
4
  colorFrom: indigo
5
  colorTo: green
 
9
  pinned: false
10
  ---
11
 
12
+ <p align="center">
13
+ <img src="assets/ankira.png" alt="Ankira" width="320">
14
+ </p>
15
+
16
+ # Ankira: German Dictation Trainer
17
 
18
  A mobile-first dictation trainer. Enter the words you want to practice, get a
19
  short German dictation read aloud, write it by hand, then photograph your page
20
  to see a word-level diff against the original.
21
 
22
+ Original plan included importing well-remembered words from Anki cards for practice, thus ANKIra.
23
+
24
+ # Models used
25
+
26
+ - https://huggingface.co/LiquidAI/LFM2.5-8B-A1B-GGUF
27
+ - https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct-GGUF
28
+ - https://huggingface.co/bosonai/higgs-audio-v3-tts-4b
29
+
30
+
31
+ # Running
32
+
33
  No models run in this Space — every inference is an HTTP call to a Modal
34
  endpoint. Configure these as **Space secrets**:
35
 
 
39
  | `MODAL_TTS_URL` | Higgs Audio v3 TTS (`/v1/audio/speech`; base URL also accepted) |
40
  | `MODAL_OCR_URL` | Qwen3-VL llama-server base URL (multimodal chat completions) |
41
 
42
+ # Our Team
43
+
44
+ - [@nofate](https://huggingface.co/nofater) Michael Gamov
45
+ - [@latticetower](https://huggingface.co/latticetower) Tatiana Malygina
app.py CHANGED
@@ -11,6 +11,7 @@ Run locally from this directory (the Space root):
11
  MODAL_LLM_URL=... MODAL_TTS_URL=... MODAL_OCR_URL=... uv run python app.py
12
  """
13
 
 
14
  import json
15
  import os
16
  import tempfile
@@ -101,11 +102,21 @@ MOBILE_CSS = """
101
  margin: 0 auto !important;
102
  padding: 12px 14px 28px !important;
103
  }
104
- .gradio-container h1 {
 
 
 
 
 
 
 
 
 
 
 
 
105
  font-size: 1.6rem;
106
  font-weight: 700;
107
- text-align: center;
108
- margin: 10px 0 2px;
109
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
110
  -webkit-background-clip: text;
111
  background-clip: text;
@@ -146,6 +157,29 @@ FA_HEAD = (
146
  SPINNER = '<i class="fa-solid fa-spinner fa-spin"></i>'
147
 
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  def _require_env(name: str) -> str:
150
  val = os.environ.get(name)
151
  if not val:
@@ -307,23 +341,24 @@ def build_ui() -> gr.Blocks:
307
  # localStorage-backed: survives a tab reload while the learner writes (spec §3).
308
  state = gr.BrowserState({"diktat": "", "created_at": 0})
309
 
310
- gr.Markdown("# ✍️ German Dictation Trainer")
311
 
312
  # Four stacked views; nav() keeps exactly one visible. Order must match
313
  # wizard.VIEWS.
314
  with gr.Group(visible=True) as view_input:
315
- gr.Markdown("### 1 · Words to practice", elem_classes="panel-title")
 
316
  words_in = gr.Textbox(
317
  label="Words to practice",
318
  placeholder="Comma- or newline-separated, e.g. angeblich, ablehnen, Apfel",
319
  lines=4,
320
  )
321
  level_in = gr.Dropdown(["A1", "A2", "B1", "B2"], value="A2", label="Level")
322
- start_btn = gr.Button("Start", variant="primary")
323
  input_status = gr.Markdown(visible=False, sanitize_html=False, elem_classes="status")
324
 
325
  with gr.Group(visible=False) as view_listen:
326
- gr.Markdown("### 2 · Listen", elem_classes="panel-title")
327
  audio_out = gr.Audio(
328
  type="filepath", interactive=False, label="Dictation audio"
329
  )
@@ -335,10 +370,10 @@ def build_ui() -> gr.Blocks:
335
  finished_btn = gr.Button("Finished", variant="primary")
336
 
337
  with gr.Group(visible=False) as view_upload:
338
- gr.Markdown("### 3 · Upload your handwriting", elem_classes="panel-title")
339
  image_in = gr.Image(
340
  type="filepath",
341
- sources=["upload", "webcam"],
342
  label="Photo of your handwriting",
343
  )
344
  with gr.Row():
@@ -347,7 +382,7 @@ def build_ui() -> gr.Blocks:
347
  upload_status = gr.Markdown(visible=False, sanitize_html=False, elem_classes="status")
348
 
349
  with gr.Group(visible=False) as view_results:
350
- gr.Markdown("### 4 · Results", elem_classes="panel-title")
351
  recognized_out = gr.Textbox(
352
  label="Recognized text (OCR)", interactive=False, lines=4
353
  )
 
11
  MODAL_LLM_URL=... MODAL_TTS_URL=... MODAL_OCR_URL=... uv run python app.py
12
  """
13
 
14
+ import base64
15
  import json
16
  import os
17
  import tempfile
 
102
  margin: 0 auto !important;
103
  padding: 12px 14px 28px !important;
104
  }
105
+ .app-header {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ gap: 12px;
110
+ margin: 12px 0 4px;
111
+ }
112
+ .app-logo {
113
+ height: 128px;
114
+ width: auto;
115
+ flex: 0 0 auto;
116
+ }
117
+ .app-title {
118
  font-size: 1.6rem;
119
  font-weight: 700;
 
 
120
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
121
  -webkit-background-clip: text;
122
  background-clip: text;
 
157
  SPINNER = '<i class="fa-solid fa-spinner fa-spin"></i>'
158
 
159
 
160
+ def _logo_data_uri() -> str:
161
+ """Embed the scaled-down Ankira logo as a base64 PNG data URI, so the header
162
+ needs no Gradio file-serving allowlist. Empty string if the file is missing
163
+ (the header then falls back to the ✍️ emoji)."""
164
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "ankira_blue.png")
165
+ try:
166
+ with open(path, "rb") as f:
167
+ b64 = base64.b64encode(f.read()).decode("ascii")
168
+ return f"data:image/png;base64,{b64}"
169
+ except OSError:
170
+ return ""
171
+
172
+
173
+ _LOGO_URI = _logo_data_uri()
174
+ _LOGO_TAG = (
175
+ f'<img class="app-logo" alt="Ankira" src="{_LOGO_URI}">' if _LOGO_URI else "✍️"
176
+ )
177
+ HEADER_HTML = (
178
+ f'<div class="app-header">{_LOGO_TAG}'
179
+ f'<span class="app-title">German Dictation Trainer</span></div>'
180
+ )
181
+
182
+
183
  def _require_env(name: str) -> str:
184
  val = os.environ.get(name)
185
  if not val:
 
341
  # localStorage-backed: survives a tab reload while the learner writes (spec §3).
342
  state = gr.BrowserState({"diktat": "", "created_at": 0})
343
 
344
+ gr.HTML(HEADER_HTML)
345
 
346
  # Four stacked views; nav() keeps exactly one visible. Order must match
347
  # wizard.VIEWS.
348
  with gr.Group(visible=True) as view_input:
349
+ gr.Markdown("### Let's start!", elem_classes="panel-title")
350
+
351
  words_in = gr.Textbox(
352
  label="Words to practice",
353
  placeholder="Comma- or newline-separated, e.g. angeblich, ablehnen, Apfel",
354
  lines=4,
355
  )
356
  level_in = gr.Dropdown(["A1", "A2", "B1", "B2"], value="A2", label="Level")
357
+ start_btn = gr.Button("Generate", variant="primary")
358
  input_status = gr.Markdown(visible=False, sanitize_html=False, elem_classes="status")
359
 
360
  with gr.Group(visible=False) as view_listen:
361
+ gr.Markdown("### Listen", elem_classes="panel-title")
362
  audio_out = gr.Audio(
363
  type="filepath", interactive=False, label="Dictation audio"
364
  )
 
370
  finished_btn = gr.Button("Finished", variant="primary")
371
 
372
  with gr.Group(visible=False) as view_upload:
373
+ gr.Markdown("### Upload", elem_classes="panel-title")
374
  image_in = gr.Image(
375
  type="filepath",
376
+ sources=["upload", "webcam", "clipboard"],
377
  label="Photo of your handwriting",
378
  )
379
  with gr.Row():
 
382
  upload_status = gr.Markdown(visible=False, sanitize_html=False, elem_classes="status")
383
 
384
  with gr.Group(visible=False) as view_results:
385
+ gr.Markdown("### Results", elem_classes="panel-title")
386
  recognized_out = gr.Textbox(
387
  label="Recognized text (OCR)", interactive=False, lines=4
388
  )
assets/ankira.png ADDED

Git LFS Details

  • SHA256: 98c4799f3778102e8b13bcad19c1ba1a5d85aca482cd5799b41821e8a02187f0
  • Pointer size: 131 Bytes
  • Size of remote file: 119 kB
assets/ankira_blue.png ADDED