usmannawaz commited on
Commit
cc460a9
·
verified ·
1 Parent(s): dfee144

Upload 2 files

Browse files
Files changed (3) hide show
  1. .gitattributes +1 -0
  2. app.py +561 -0
  3. fonts/Menaion.otf +3 -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
+ fonts/Menaion.otf filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,561 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ import os
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+
6
+ import gradio as gr
7
+ import stanza
8
+ from huggingface_hub import snapshot_download
9
+
10
+
11
+ # ---------------------------------------------------------------------
12
+ # Model configuration
13
+ # ---------------------------------------------------------------------
14
+
15
+ MODEL_REPO_ID = "usmannawaz/old-church-slavonic-tokenizer-lemmatizer"
16
+ MODEL_VARIANT = "combined"
17
+
18
+ LANG = "cu"
19
+ POS_PACKAGE = "proiel_nocharlm"
20
+ TOKENIZER_PACKAGE = "proiel"
21
+ LEMMATIZER_PACKAGE = "proiel_nocharlm"
22
+
23
+ HF_LOCAL_DIR = "hf_models/old-church-slavonic-tokenizer-lemmatizer"
24
+ STANZA_DIR = os.path.join(os.path.expanduser("~"), "stanza_resources")
25
+
26
+ os.environ["STANZA_RESOURCES_DIR"] = STANZA_DIR
27
+
28
+ EXAMPLE_TEXT = "тѣма пророчьскꙑи ликъ д͞ховьнꙑѧ благодѣти исплъни сѧ"
29
+
30
+
31
+ # ---------------------------------------------------------------------
32
+ # Legacy/private-use characters -> Unicode OCS
33
+ #
34
+ # Duplicate keys from the old app were removed. The values below preserve
35
+ # the replacements that Python actually used previously: the final value
36
+ # of each duplicate key.
37
+ # ---------------------------------------------------------------------
38
+
39
+ REPLACEMENTS = {
40
+ "": "и",
41
+ "": "ч",
42
+ "": "ѥ",
43
+ "": "н",
44
+ "": ":~",
45
+ "": "~",
46
+ "": "̅",
47
+ "": "҆̀",
48
+ "": "ⷮ",
49
+ "": "ᲈ",
50
+ "": "Ч",
51
+ "": "ꙋ",
52
+ "": "чᲈ",
53
+ "": "҅́",
54
+ "": "꙾",
55
+ "": "ⷮ",
56
+ "": "҆́",
57
+ "ⷭⷭ": "҇",
58
+ "": "҆́",
59
+ "": "н҇",
60
+ "": "ꙶ",
61
+ "": "оу",
62
+ "": "ꙁ",
63
+ "": "ꙑ",
64
+ "": "ⱑ",
65
+ "": "с",
66
+ "": "Ѱ",
67
+ "": "҇",
68
+ "": "ⱉ",
69
+ "": "͡",
70
+ "": "҃",
71
+ "ⷣ": "ⷣ҇",
72
+ "ⷮ": "ⷮ҇",
73
+ "ⷯ": "ⷯ҇",
74
+ "": "ꙩ́",
75
+ "ⷤ҆": "ⷤ҇",
76
+ "": "",
77
+ "": "҃",
78
+ "": "꙯",
79
+ "": "҃",
80
+ }
81
+
82
+
83
+ # ---------------------------------------------------------------------
84
+ # Font setup
85
+ #
86
+ # Add your licensed font file as:
87
+ # fonts/Menaion.otf
88
+ #
89
+ # The app still starts without it, but the browser uses fallback fonts.
90
+ # ---------------------------------------------------------------------
91
+
92
+ FONT_DIR = Path("fonts")
93
+ FONT_FILE = FONT_DIR / "Menaion.otf"
94
+
95
+ if FONT_DIR.exists():
96
+ gr.set_static_paths(paths=[FONT_DIR])
97
+
98
+ FONT_FACE_CSS = """
99
+ @font-face {
100
+ font-family: "Menaion";
101
+ src: url("/gradio_api/file=fonts/Menaion.otf") format("opentype");
102
+ font-style: normal;
103
+ font-weight: normal;
104
+ font-display: swap;
105
+ }
106
+ """
107
+
108
+
109
+ CUSTOM_CSS = FONT_FACE_CSS + """
110
+ body {
111
+ background: linear-gradient(
112
+ 135deg,
113
+ #f8fafc 0%,
114
+ #eef6ff 50%,
115
+ #f3f7fb 100%
116
+ ) !important;
117
+ }
118
+
119
+ .gradio-container {
120
+ max-width: 1050px !important;
121
+ margin: auto !important;
122
+ font-family: Inter, Arial, sans-serif !important;
123
+ }
124
+
125
+ #hero {
126
+ background: linear-gradient(135deg, #0f172a, #0f5f8f);
127
+ border-radius: 28px;
128
+ padding: 34px;
129
+ margin-bottom: 22px;
130
+ box-shadow: 0 22px 60px rgba(15, 23, 42, 0.22);
131
+ }
132
+
133
+ #hero h1 {
134
+ color: #ffffff;
135
+ font-size: 2.4rem;
136
+ margin: 0 0 10px 0;
137
+ font-weight: 900;
138
+ }
139
+
140
+ #hero p {
141
+ color: #e0f2fe;
142
+ font-size: 1.05rem;
143
+ line-height: 1.55;
144
+ margin: 0;
145
+ }
146
+
147
+ #main-card,
148
+ #output-card {
149
+ background: #ffffff;
150
+ border: 1px solid #cbd5e1;
151
+ border-radius: 24px;
152
+ padding: 24px;
153
+ box-shadow: 0 18px 45px rgba(15, 23, 42, 0.10);
154
+ }
155
+
156
+ #output-card {
157
+ margin-top: 22px;
158
+ }
159
+
160
+ textarea,
161
+ input {
162
+ border-radius: 16px !important;
163
+ border: 1.5px solid #94a3b8 !important;
164
+ background: #ffffff !important;
165
+ color: #0f172a !important;
166
+ }
167
+
168
+ textarea:focus,
169
+ input:focus {
170
+ border-color: #0f5f8f !important;
171
+ box-shadow: 0 0 0 3px rgba(14, 116, 144, 0.18) !important;
172
+ }
173
+
174
+ label {
175
+ color: #0f172a !important;
176
+ font-weight: 850 !important;
177
+ }
178
+
179
+ button {
180
+ border-radius: 16px !important;
181
+ font-weight: 900 !important;
182
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14) !important;
183
+ }
184
+
185
+ button.primary {
186
+ background: linear-gradient(90deg, #0f172a, #0f5f8f) !important;
187
+ color: #ffffff !important;
188
+ border: none !important;
189
+ }
190
+
191
+ button.primary:hover {
192
+ background: linear-gradient(90deg, #020617, #075985) !important;
193
+ transform: translateY(-1px);
194
+ }
195
+
196
+ /*
197
+ The input uses Menaion only for visual rendering.
198
+ The underlying string is unchanged until preprocess_text() runs.
199
+ */
200
+ #ocs-input textarea {
201
+ font-family: "Menaion", "Noto Sans Cyrillic", serif !important;
202
+ font-size: 23px !important;
203
+ font-weight: normal !important;
204
+ line-height: 1.75 !important;
205
+ }
206
+
207
+ /* All final Unicode OCS output is rendered with Menaion. */
208
+ .ocs-font {
209
+ font-family: "Menaion", "Noto Sans Cyrillic", serif !important;
210
+ font-weight: normal !important;
211
+ }
212
+
213
+ #sentence-box {
214
+ background: #f8fafc;
215
+ border: 1px solid #cbd5e1;
216
+ border-left: 6px solid #0f5f8f;
217
+ border-radius: 18px;
218
+ padding: 16px;
219
+ color: #0f172a;
220
+ margin-bottom: 20px;
221
+ }
222
+
223
+ #sentence-box .label {
224
+ font-family: Inter, Arial, sans-serif;
225
+ font-size: 1rem;
226
+ font-weight: 850;
227
+ }
228
+
229
+ #sentence-text {
230
+ color: #0f172a;
231
+ font-size: 25px;
232
+ line-height: 1.85;
233
+ margin-top: 10px;
234
+ overflow-wrap: anywhere;
235
+ }
236
+
237
+ #table-title {
238
+ color: #0f5f8f;
239
+ font-family: Inter, Arial, sans-serif;
240
+ font-weight: 900;
241
+ font-size: 1.05rem;
242
+ margin: 6px 0 12px 0;
243
+ }
244
+
245
+ .ocs-table-wrap {
246
+ border: 1px solid #cbd5e1;
247
+ border-radius: 16px;
248
+ overflow: auto;
249
+ }
250
+
251
+ .ocs-table {
252
+ width: 100%;
253
+ border-collapse: collapse;
254
+ table-layout: fixed;
255
+ }
256
+
257
+ .ocs-table th {
258
+ background: #eef6ff;
259
+ color: #0f172a;
260
+ font-family: Inter, Arial, sans-serif;
261
+ font-size: 0.95rem;
262
+ text-align: left;
263
+ padding: 12px;
264
+ border-bottom: 1px solid #cbd5e1;
265
+ }
266
+
267
+ .ocs-table td {
268
+ color: #0f172a;
269
+ font-size: 22px;
270
+ line-height: 1.6;
271
+ padding: 11px 12px;
272
+ border-bottom: 1px solid #e2e8f0;
273
+ vertical-align: top;
274
+ overflow-wrap: anywhere;
275
+ }
276
+
277
+ .ocs-table tr:last-child td {
278
+ border-bottom: none;
279
+ }
280
+
281
+ .ocs-table tbody tr:nth-child(even) {
282
+ background: #f8fafc;
283
+ }
284
+
285
+ .status-box {
286
+ border-radius: 16px;
287
+ padding: 14px 16px;
288
+ background: #fff7ed;
289
+ border: 1px solid #fdba74;
290
+ color: #7c2d12;
291
+ font-family: Inter, Arial, sans-serif;
292
+ }
293
+
294
+ footer,
295
+ .api,
296
+ .settings,
297
+ a[href*="gradio.app"],
298
+ button[aria-label="Settings"] {
299
+ display: none !important;
300
+ }
301
+ """
302
+
303
+
304
+ HERO_HTML = """
305
+ <div id="hero">
306
+ <h1>OCS Combined Lemmatizer</h1>
307
+ <p>
308
+ Pasted text is mapped from legacy/private-use characters to Unicode,
309
+ processed by Stanza, and rendered with the Menaion font.
310
+ </p>
311
+ </div>
312
+ """
313
+
314
+
315
+ # ---------------------------------------------------------------------
316
+ # NLP loading
317
+ # ---------------------------------------------------------------------
318
+
319
+ @lru_cache(maxsize=1)
320
+ def get_model_paths():
321
+ repo_dir = snapshot_download(
322
+ repo_id=MODEL_REPO_ID,
323
+ local_dir=HF_LOCAL_DIR,
324
+ )
325
+
326
+ model_dir = Path(repo_dir)
327
+ tokenizer_model = (
328
+ model_dir
329
+ / "models"
330
+ / MODEL_VARIANT
331
+ / "tokenize"
332
+ / "cu_proiel_tokenizer.pt"
333
+ )
334
+ lemma_model = (
335
+ model_dir
336
+ / "models"
337
+ / MODEL_VARIANT
338
+ / "lemma"
339
+ / "cu_proiel_nocharlm_lemmatizer.pt"
340
+ )
341
+
342
+ if not tokenizer_model.exists():
343
+ raise FileNotFoundError(f"Tokenizer model not found: {tokenizer_model}")
344
+
345
+ if not lemma_model.exists():
346
+ raise FileNotFoundError(f"Lemmatizer model not found: {lemma_model}")
347
+
348
+ return tokenizer_model, lemma_model
349
+
350
+
351
+ @lru_cache(maxsize=1)
352
+ def load_nlp_pipeline():
353
+ tokenizer_model, lemma_model = get_model_paths()
354
+
355
+ # Download only the Stanza resources not supplied by your model repo.
356
+ stanza.download(
357
+ lang=LANG,
358
+ model_dir=STANZA_DIR,
359
+ processors={"pos": POS_PACKAGE},
360
+ package=None,
361
+ verbose=False,
362
+ )
363
+
364
+ return stanza.Pipeline(
365
+ lang=LANG,
366
+ dir=STANZA_DIR,
367
+ package=None,
368
+ processors={
369
+ "tokenize": TOKENIZER_PACKAGE,
370
+ "pos": POS_PACKAGE,
371
+ "lemma": LEMMATIZER_PACKAGE,
372
+ },
373
+ tokenize_model_path=str(tokenizer_model),
374
+ lemma_model_path=str(lemma_model),
375
+ tokenize_pretokenized=False,
376
+ use_gpu=False,
377
+ verbose=False,
378
+ )
379
+
380
+
381
+ # ---------------------------------------------------------------------
382
+ # Text processing
383
+ # ---------------------------------------------------------------------
384
+
385
+ def clean_text(text):
386
+ return str(text or "").strip()
387
+
388
+
389
+ def apply_mapping(text, mapping):
390
+ # Longer sequences must be replaced before their individual characters.
391
+ ordered = sorted(mapping.items(), key=lambda item: len(item[0]), reverse=True)
392
+
393
+ for old_text, new_text in ordered:
394
+ text = text.replace(old_text, new_text)
395
+
396
+ return text
397
+
398
+
399
+ def preprocess_text(text):
400
+ """
401
+ Pipeline stage:
402
+ raw pasted text -> Unicode OCS
403
+
404
+ Do not apply a font here. Fonts belong only to browser rendering.
405
+ """
406
+ return apply_mapping(text, REPLACEMENTS)
407
+
408
+
409
+ def make_sentence_html(text):
410
+ return f"""
411
+ <div id="sentence-box">
412
+ <div class="label">Processed Unicode OCS input</div>
413
+ <div id="sentence-text" class="ocs-font">{html.escape(text)}</div>
414
+ </div>
415
+ """
416
+
417
+
418
+ def make_results_html(rows):
419
+ table_rows = "".join(
420
+ f"""
421
+ <tr>
422
+ <td class="ocs-font">{html.escape(token)}</td>
423
+ <td class="ocs-font">{html.escape(lemma)}</td>
424
+ </tr>
425
+ """
426
+ for token, lemma in rows
427
+ )
428
+
429
+ return f"""
430
+ <div id="table-title">Token and lemma output</div>
431
+ <div class="ocs-table-wrap">
432
+ <table class="ocs-table">
433
+ <thead>
434
+ <tr>
435
+ <th>Token</th>
436
+ <th>Lemma</th>
437
+ </tr>
438
+ </thead>
439
+ <tbody>{table_rows}</tbody>
440
+ </table>
441
+ </div>
442
+ """
443
+
444
+
445
+ def analyze_text(text):
446
+ raw_text = clean_text(text)
447
+
448
+ if not raw_text:
449
+ return (
450
+ gr.update(visible=True),
451
+ """
452
+ <div class="status-box">
453
+ Please paste a sentence or raw OCS text first.
454
+ </div>
455
+ """,
456
+ "",
457
+ )
458
+
459
+ try:
460
+ # 1. PUA/legacy mapping -> standard Unicode OCS.
461
+ processed_text = preprocess_text(raw_text)
462
+
463
+ # 2. Stanza receives only the processed Unicode string.
464
+ nlp = load_nlp_pipeline()
465
+ doc = nlp(processed_text)
466
+
467
+ # 3. Model results remain Unicode strings.
468
+ rows = [
469
+ (word.text or "_", word.lemma or "_")
470
+ for sentence in doc.sentences
471
+ for word in sentence.words
472
+ ]
473
+
474
+ # 4. Menaion is applied only by CSS during browser visualization.
475
+ return (
476
+ gr.update(visible=True),
477
+ make_sentence_html(processed_text),
478
+ make_results_html(rows),
479
+ )
480
+
481
+ except Exception as error:
482
+ return (
483
+ gr.update(visible=True),
484
+ f"""
485
+ <div class="status-box">
486
+ <strong>Error:</strong> {html.escape(str(error))}
487
+ </div>
488
+ """,
489
+ "",
490
+ )
491
+
492
+
493
+ def clear_all():
494
+ return "", gr.update(visible=False), "", ""
495
+
496
+
497
+ def load_example():
498
+ return EXAMPLE_TEXT
499
+
500
+
501
+ # ---------------------------------------------------------------------
502
+ # Gradio interface
503
+ # ---------------------------------------------------------------------
504
+
505
+ with gr.Blocks(
506
+ title="OCS Combined Lemmatizer",
507
+ css=CUSTOM_CSS,
508
+ theme=gr.themes.Soft(
509
+ primary_hue="cyan",
510
+ secondary_hue="blue",
511
+ neutral_hue="slate",
512
+ ),
513
+ ) as demo:
514
+ gr.HTML(HERO_HTML)
515
+
516
+ with gr.Column(elem_id="main-card"):
517
+ text_input = gr.Textbox(
518
+ label="Input text",
519
+ lines=8,
520
+ placeholder="Paste Old Church Slavonic text here...",
521
+ elem_id="ocs-input",
522
+ )
523
+
524
+ with gr.Row():
525
+ analyze_button = gr.Button("Lemmatize", variant="primary")
526
+ example_button = gr.Button("Load example")
527
+ clear_button = gr.Button("Clear")
528
+
529
+ with gr.Column(elem_id="output-card", visible=False) as output_panel:
530
+ sentence_output = gr.HTML()
531
+ results_output = gr.HTML()
532
+
533
+ analyze_button.click(
534
+ fn=analyze_text,
535
+ inputs=text_input,
536
+ outputs=[output_panel, sentence_output, results_output],
537
+ show_progress="hidden",
538
+ )
539
+
540
+ example_button.click(
541
+ fn=load_example,
542
+ inputs=None,
543
+ outputs=text_input,
544
+ show_progress="hidden",
545
+ )
546
+
547
+ clear_button.click(
548
+ fn=clear_all,
549
+ inputs=None,
550
+ outputs=[
551
+ text_input,
552
+ output_panel,
553
+ sentence_output,
554
+ results_output,
555
+ ],
556
+ show_progress="hidden",
557
+ )
558
+
559
+
560
+ if __name__ == "__main__":
561
+ demo.launch()
fonts/Menaion.otf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:68792e712d2af4bf333a23f24c0b59f3efa3499a58c31f082ecc8a4235cb6fa8
3
+ size 169188