usmannawaz commited on
Commit
5432d4d
·
verified ·
1 Parent(s): 6b65a51

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +554 -319
  2. inference.py +150 -0
  3. model.py +133 -0
  4. requirements.txt +2 -4
app.py CHANGED
@@ -1,207 +1,194 @@
1
- import os
 
 
 
2
  from functools import lru_cache
3
  from pathlib import Path
4
 
5
  import gradio as gr
6
- import pandas as pd
7
- import spaces
8
- import stanza
9
- from huggingface_hub import snapshot_download
10
-
11
-
12
- MODEL_REPO_ID = "usmannawaz/old-church-slavonic-tokenizer-lemmatizer"
13
- MODEL_VARIANT = "combined"
14
-
15
- LANG = "cu"
16
- POS_PACKAGE = "proiel_nocharlm"
17
- TOKENIZER_PACKAGE = "proiel"
18
- LEMMATIZER_PACKAGE = "proiel_nocharlm"
19
-
20
- HF_LOCAL_DIR = "hf_models/old-church-slavonic-tokenizer-lemmatizer"
21
- STANZA_DIR = os.path.join(os.path.expanduser("~"), "stanza_resources")
22
-
23
- os.environ["STANZA_RESOURCES_DIR"] = STANZA_DIR
24
-
25
- EXAMPLE_TEXT = "тѣма пророчьскꙑи ликъ д͞ховьнꙑѧ благодѣти исплъни сѧ"
26
-
27
- REPLACEMENTS1 = {
28
- "": "и",
29
- "": "ч",
30
- "": "ѥ",
31
- "": "н",
32
- "": ":~",
33
- "": "~",
34
- "ⷣ": "ⷣ҇",
35
- "": "̅",
36
- "": "҆̀",
37
- "": "ⷮ",
38
- "": "ᲈ",
39
- "": "Ч",
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
- FONT_DIR = Path("fonts")
71
 
72
- if FONT_DIR.exists():
73
- gr.set_static_paths(paths=[FONT_DIR])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  CUSTOM_CSS = """
76
- @font-face {
77
- font-family: "Menaion";
78
- src: url("/gradio_api/file=fonts/Menaion.otf") format("opentype");
79
- font-style: normal;
80
- font-weight: normal;
81
- font-display: swap;
82
- }
83
  body {
84
- background: linear-gradient(135deg, #f8fafc 0%, #eef6ff 50%, #f3f7fb 100%) !important;
85
  }
86
  .gradio-container {
87
  max-width: 980px !important;
88
  margin: auto !important;
89
- font-family: Inter, Arial, sans-serif !important;
90
  }
91
- #hero {
92
- background: linear-gradient(135deg, #0f172a, #0f5f8f);
93
- border-radius: 28px;
94
- padding: 34px;
95
- margin-bottom: 22px;
96
- box-shadow: 0 22px 60px rgba(15, 23, 42, 0.22);
97
  }
98
- #hero h1 {
99
- color: #ffffff;
100
- font-size: 2.4rem;
101
- margin: 0 0 10px 0;
102
  font-weight: 900;
 
103
  }
104
- #hero p {
105
- color: #e0f2fe;
 
106
  font-size: 1.05rem;
107
  line-height: 1.55;
108
- margin: 0;
109
  }
110
- #main-card {
111
- background: #ffffff;
112
- border: 1px solid #cbd5e1;
113
- border-radius: 24px;
114
- padding: 24px;
115
- box-shadow: 0 18px 45px rgba(15, 23, 42, 0.10);
116
  }
117
- #output-card {
118
- background: #ffffff;
119
- border: 1px solid #cbd5e1;
120
- border-radius: 24px;
121
- padding: 22px;
122
- margin-top: 22px;
123
- box-shadow: 0 18px 45px rgba(15, 23, 42, 0.10);
 
 
 
124
  }
125
- textarea, input {
126
  border-radius: 16px !important;
127
- border: 1.5px solid #94a3b8 !important;
128
  background: #ffffff !important;
129
- color: #0f172a !important;
 
130
  }
131
  textarea:focus, input:focus {
132
- border-color: #0f5f8f !important;
133
- box-shadow: 0 0 0 3px rgba(14, 116, 144, 0.18) !important;
134
  }
135
  label {
136
- color: #0f172a !important;
137
- font-weight: 850 !important;
138
  }
139
  button {
140
- border-radius: 16px !important;
141
- font-weight: 900 !important;
142
- box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14) !important;
143
- }
144
- button.primary {
145
- background: linear-gradient(90deg, #0f172a, #0f5f8f) !important;
146
  color: #ffffff !important;
147
  border: none !important;
 
 
 
 
 
148
  }
149
- button.primary:hover {
150
- background: linear-gradient(90deg, #020617, #075985) !important;
151
  transform: translateY(-1px);
152
  }
153
- #ocs-input textarea {
154
- font-family: "Menaion", serif !important;
155
- font-size: 24px !important;
156
- line-height: 1.8 !important;
157
- font-weight: normal !important;
158
  }
159
- #sentence-box {
160
- background: #f8fafc;
161
- border: 1px solid #cbd5e1;
162
- border-left: 6px solid #0f5f8f;
163
  border-radius: 18px;
164
- padding: 16px;
165
- color: #0f172a;
166
- font-size: 1.05rem;
167
- line-height: 1.7;
168
- margin-bottom: 18px;
169
  }
170
- #sentence-box b {
171
- color: #0f172a;
172
- font-family: Inter, Arial, sans-serif !important;
 
173
  font-size: 1rem;
174
  }
175
- #sentence-text {
176
- color: #0f172a;
177
- font-family: "Menaion", serif !important;
178
- font-size: 25px !important;
179
- font-weight: normal !important;
180
- line-height: 1.8 !important;
181
- margin-top: 8px;
182
- }
183
- #table-title {
184
- color: #0f5f8f;
185
  font-weight: 900;
186
- font-size: 1rem;
187
- margin: 6px 0 10px 0;
188
  }
189
- #lemma-table,
190
- #lemma-table table,
191
- #lemma-table td,
192
- #lemma-table th,
193
- #lemma-table input,
194
- #lemma-table textarea,
195
- #lemma-table .cell-wrap,
196
- #lemma-table .table-wrap {
197
- font-family: "Menaion", serif !important;
198
- font-size: 21px !important;
199
- font-weight: normal !important;
200
- line-height: 1.6 !important;
201
  }
202
- .dataframe {
203
- border-radius: 16px !important;
204
- overflow: hidden !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  }
206
  footer,
207
  .api,
@@ -212,216 +199,464 @@ button[aria-label="Settings"] {
212
  }
213
  """
214
 
215
- HERO_HTML = """
216
- <div id="hero">
217
- <h1>Combo Tool Demo</h1>
218
- <p>
219
- Paste Old Church Slavonic text. The app preprocesses the input, tokenizes it, and returns each token with its predicted lemma.
220
- </p>
221
- </div>
222
- """
223
 
 
 
 
224
 
225
- @lru_cache(maxsize=1)
226
- def get_model_paths():
227
- repo_dir = snapshot_download(
228
- repo_id=MODEL_REPO_ID,
229
- local_dir=HF_LOCAL_DIR,
230
- )
 
 
 
 
 
231
 
232
- model_dir = Path(repo_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
- tokenizer_model = model_dir / "models" / MODEL_VARIANT / "tokenize" / "cu_proiel_tokenizer.pt"
235
- lemma_model = model_dir / "models" / MODEL_VARIANT / "lemma" / "cu_proiel_nocharlm_lemmatizer.pt"
236
 
237
- if not tokenizer_model.exists():
238
- raise FileNotFoundError(f"Tokenizer model not found: {tokenizer_model}")
239
 
240
- if not lemma_model.exists():
241
- raise FileNotFoundError(f"Lemmatizer model not found: {lemma_model}")
 
 
242
 
243
- return tokenizer_model, lemma_model
244
 
 
 
 
245
 
246
- @lru_cache(maxsize=1)
247
- def load_nlp_pipeline():
248
- tokenizer_model, lemma_model = get_model_paths()
249
 
250
- stanza.download(
251
- lang=LANG,
252
- model_dir=STANZA_DIR,
253
- processors={"pos": POS_PACKAGE},
254
- package=None,
255
- verbose=False,
256
- )
257
 
258
- return stanza.Pipeline(
259
- lang=LANG,
260
- dir=STANZA_DIR,
261
- package=None,
262
- processors={
263
- "tokenize": TOKENIZER_PACKAGE,
264
- "pos": POS_PACKAGE,
265
- "lemma": LEMMATIZER_PACKAGE,
266
- },
267
- tokenize_model_path=str(tokenizer_model),
268
- lemma_model_path=str(lemma_model),
269
- tokenize_pretokenized=False,
270
- use_gpu=True,
271
- verbose=False,
272
  )
273
 
 
 
274
 
275
- def clean_text(text):
276
- return str(text or "").strip()
277
 
278
 
279
- def apply_mapping(text, mapping):
280
- for old_char, new_char in sorted(mapping.items(), key=lambda item: len(item[0]), reverse=True):
281
- text = text.replace(old_char, new_char)
282
- return text
283
 
 
 
 
284
 
285
- def preprocess_text(text):
286
- text = apply_mapping(text, REPLACEMENTS1)
287
- return text
288
 
 
289
 
290
- def make_sentence_html(text):
291
- safe_text = (
292
- text.replace("&", "&amp;")
293
- .replace("<", "&lt;")
294
- .replace(">", "&gt;")
295
- )
296
 
297
- return f"""
298
- <div id="sentence-box">
299
- <b>Input text</b>
300
- <div id="sentence-text">{safe_text}</div>
301
- </div>
302
- """
303
 
 
304
 
305
- @spaces.GPU(duration=120)
306
- def analyze_text(text):
307
- text = clean_text(text)
308
 
309
- if not text:
310
- empty_table = pd.DataFrame(columns=["Token", "Lemma"])
311
- return (
312
- gr.update(visible=True),
313
- gr.update(value="<div id='sentence-box'>Please paste a sentence or raw text first.</div>"),
314
- gr.update(value="", visible=False),
315
- gr.update(value=empty_table, visible=False),
316
- )
317
 
318
  try:
319
- processed_text = preprocess_text(text)
320
- nlp = load_nlp_pipeline()
321
- doc = nlp(processed_text)
322
-
323
- rows = []
324
-
325
- for sentence in doc.sentences:
326
- for word in sentence.words:
327
- rows.append({
328
- "Token": word.text or "_",
329
- "Lemma": word.lemma or "_",
330
- })
331
-
332
- table = pd.DataFrame(rows, columns=["Token", "Lemma"])
333
-
334
- return (
335
- gr.update(visible=True),
336
- gr.update(value=make_sentence_html(processed_text)),
337
- gr.update(value="<div id='table-title'>Token and lemma output</div>", visible=True),
338
- gr.update(value=table, visible=True),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  )
340
 
341
- except Exception as error:
342
- empty_table = pd.DataFrame(columns=["Token", "Lemma"])
 
 
 
343
 
344
- return (
345
- gr.update(visible=True),
346
- gr.update(value=f"<div id='sentence-box'>Error: {str(error)}</div>"),
347
- gr.update(value="", visible=False),
348
- gr.update(value=empty_table, visible=False),
349
  )
350
 
 
 
 
 
351
 
352
- def clear_all():
353
- empty_table = pd.DataFrame(columns=["Token", "Lemma"])
354
 
355
- return (
356
- "",
357
- gr.update(visible=False),
358
- gr.update(value=""),
359
- gr.update(value="", visible=False),
360
- gr.update(value=empty_table, visible=False),
361
  )
362
 
363
 
364
- def load_example():
365
- return EXAMPLE_TEXT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
 
368
  with gr.Blocks(
369
- title="Combo New Tool",
370
  css=CUSTOM_CSS,
371
  theme=gr.themes.Soft(
372
- primary_hue="cyan",
373
- secondary_hue="blue",
374
  neutral_hue="slate",
375
  ),
376
  ) as demo:
377
- gr.HTML(HERO_HTML)
378
-
379
  with gr.Column(elem_id="main-card"):
380
- text_input = gr.Textbox(
381
- label="Input text",
382
- lines=8,
383
- placeholder="Paste Old Church Slavonic text here...",
384
- elem_id="ocs-input",
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  )
386
 
387
  with gr.Row():
388
- analyze_button = gr.Button("Lemmatize", variant="primary")
389
- example_button = gr.Button("Load example")
390
- clear_button = gr.Button("Clear")
391
-
392
- with gr.Column(elem_id="output-card", visible=False) as output_panel:
393
- sentence_output = gr.HTML()
394
- table_title = gr.HTML(visible=False)
395
-
396
- lemma_table = gr.Dataframe(
397
- headers=["Token", "Lemma"],
398
- interactive=False,
399
- wrap=True,
400
- show_label=False,
401
- visible=False,
402
- elem_id="lemma-table",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  )
404
 
405
- analyze_button.click(
406
- fn=analyze_text,
407
- inputs=text_input,
408
- outputs=[output_panel, sentence_output, table_title, lemma_table],
409
- show_progress="hidden",
410
- )
411
 
412
- example_button.click(
413
- fn=load_example,
414
- inputs=None,
415
- outputs=text_input,
416
- show_progress="hidden",
417
- )
418
 
419
- clear_button.click(
420
- fn=clear_all,
421
- inputs=None,
422
- outputs=[text_input, output_panel, sentence_output, table_title, lemma_table],
423
- show_progress="hidden",
424
- )
425
 
426
 
427
- demo.launch()
 
 
1
+ import html
2
+ import json
3
+ import re
4
+ import tempfile
5
  from functools import lru_cache
6
  from pathlib import Path
7
 
8
  import gradio as gr
9
+ import torch
10
+ from huggingface_hub import hf_hub_download
11
+
12
+ from inference import load_lemmatizer, load_registry, MODEL_REPO_ID, MODEL_ROOT
13
+
14
+
15
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
16
+ REGISTRY = load_registry("models_registry.json")
17
+
18
+ TARGET_COL_IDX = 2
19
+ BATCH_SIZE = 32
20
+
21
+
22
+
23
+
24
+ def _display_name(item):
25
+ return f"{item['language']} - {item['treebank']}"
26
+
27
+
28
+ LANGUAGES = sorted({item["language"] for item in REGISTRY.values()})
29
+
30
+ DISPLAY_TO_ID = {
31
+ _display_name(item): model_id
32
+ for model_id, item in REGISTRY.items()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
 
 
35
 
36
+ def treebank_choices(language):
37
+ choices = []
38
+
39
+ for model_id, item in REGISTRY.items():
40
+ if item["language"] == language:
41
+ choices.append(_display_name(item))
42
+
43
+ return sorted(choices)
44
+
45
+
46
+ def default_language():
47
+ if "Old Church Slavonic" in LANGUAGES:
48
+ return "Old Church Slavonic"
49
+
50
+ return LANGUAGES[0] if LANGUAGES else None
51
+
52
+
53
+ def default_treebank(language):
54
+ choices = treebank_choices(language)
55
+ preferred = "Old Church Slavonic - PROIEL"
56
+
57
+ if preferred in choices:
58
+ return preferred
59
+
60
+ return choices[0] if choices else None
61
+
62
 
63
  CUSTOM_CSS = """
 
 
 
 
 
 
 
64
  body {
65
+ background: linear-gradient(135deg, #eaf3ff 0%, #ffffff 48%, #dbeafe 100%);
66
  }
67
  .gradio-container {
68
  max-width: 980px !important;
69
  margin: auto !important;
70
+ font-family: Arial, Helvetica, sans-serif !important;
71
  }
72
+ #main-card {
73
+ background: #ffffff;
74
+ border: 1px solid #bfdbfe;
75
+ border-radius: 26px;
76
+ padding: 30px;
77
+ box-shadow: 0 20px 50px rgba(15, 23, 42, 0.16);
78
  }
79
+ #title {
80
+ text-align: center;
81
+ color: #020617;
82
+ font-size: 2.5rem;
83
  font-weight: 900;
84
+ margin-bottom: 0.25rem;
85
  }
86
+ #subtitle {
87
+ text-align: center;
88
+ color: #1e40af;
89
  font-size: 1.05rem;
90
  line-height: 1.55;
91
+ margin-bottom: 1.6rem;
92
  }
93
+ #badge-row {
94
+ text-align: center;
95
+ margin-bottom: 1.2rem;
 
 
 
96
  }
97
+ #badge-row span {
98
+ display: inline-block;
99
+ background: #eff6ff;
100
+ color: #1e3a8a;
101
+ border: 1px solid #bfdbfe;
102
+ border-radius: 999px;
103
+ padding: 7px 13px;
104
+ margin: 4px;
105
+ font-size: 0.88rem;
106
+ font-weight: 700;
107
  }
108
+ textarea, input, select {
109
  border-radius: 16px !important;
110
+ border: 1.5px solid #2563eb !important;
111
  background: #ffffff !important;
112
+ color: #020617 !important;
113
+ box-shadow: 0 6px 16px rgba(37, 99, 235, 0.08) !important;
114
  }
115
  textarea:focus, input:focus {
116
+ border-color: #1d4ed8 !important;
117
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18) !important;
118
  }
119
  label {
120
+ color: #020617 !important;
121
+ font-weight: 800 !important;
122
  }
123
  button {
124
+ background: linear-gradient(90deg, #020617, #1d4ed8) !important;
 
 
 
 
 
125
  color: #ffffff !important;
126
  border: none !important;
127
+ border-radius: 16px !important;
128
+ padding: 13px 24px !important;
129
+ font-weight: 900 !important;
130
+ font-size: 1rem !important;
131
+ box-shadow: 0 10px 22px rgba(37, 99, 235, 0.30) !important;
132
  }
133
+ button:hover {
134
+ background: linear-gradient(90deg, #000000, #2563eb) !important;
135
  transform: translateY(-1px);
136
  }
137
+ #output-box textarea {
138
+ background: #f8fbff !important;
139
+ border: 1.5px solid #1d4ed8 !important;
140
+ color: #020617 !important;
141
+ font-family: Consolas, "Courier New", monospace !important;
142
  }
143
+ #token-card {
144
+ background: #f8fbff;
145
+ border: 1.5px solid #1d4ed8;
 
146
  border-radius: 18px;
147
+ padding: 18px;
148
+ box-shadow: 0 8px 20px rgba(37, 99, 235, 0.10);
 
 
 
149
  }
150
+ .lemma-table {
151
+ width: 100%;
152
+ border-collapse: separate;
153
+ border-spacing: 0 8px;
154
  font-size: 1rem;
155
  }
156
+ .lemma-table th {
157
+ background: linear-gradient(90deg, #020617, #1d4ed8);
158
+ color: white;
159
+ padding: 12px 14px;
160
+ text-align: left;
 
 
 
 
 
161
  font-weight: 900;
 
 
162
  }
163
+ .lemma-table th:first-child {
164
+ border-radius: 12px 0 0 12px;
 
 
 
 
 
 
 
 
 
 
165
  }
166
+ .lemma-table th:last-child {
167
+ border-radius: 0 12px 12px 0;
168
+ }
169
+ .lemma-table td {
170
+ background: #ffffff;
171
+ color: #020617;
172
+ padding: 12px 14px;
173
+ border-top: 1px solid #bfdbfe;
174
+ border-bottom: 1px solid #bfdbfe;
175
+ font-weight: 700;
176
+ }
177
+ .lemma-table td:first-child {
178
+ border-left: 1px solid #bfdbfe;
179
+ border-radius: 12px 0 0 12px;
180
+ }
181
+ .lemma-table td:last-child {
182
+ border-right: 1px solid #bfdbfe;
183
+ border-radius: 0 12px 12px 0;
184
+ color: #1d4ed8;
185
+ }
186
+ #note {
187
+ color: #1e3a8a;
188
+ font-size: 0.92rem;
189
+ text-align: center;
190
+ margin-top: 1rem;
191
+ font-weight: 600;
192
  }
193
  footer,
194
  .api,
 
199
  }
200
  """
201
 
 
 
 
 
 
 
 
 
202
 
203
+ def make_html_table(tokens, lemmas):
204
+ if not tokens:
205
+ return ""
206
 
207
+ rows = []
208
+
209
+ for token, lemma in zip(tokens, lemmas):
210
+ rows.append(
211
+ f"""
212
+ <tr>
213
+ <td>{html.escape(token)}</td>
214
+ <td>{html.escape(lemma)}</td>
215
+ </tr>
216
+ """
217
+ )
218
 
219
+ return f"""
220
+ <div id="token-card">
221
+ <table class="lemma-table">
222
+ <thead>
223
+ <tr>
224
+ <th>Word</th>
225
+ <th>Lemma</th>
226
+ </tr>
227
+ </thead>
228
+ <tbody>
229
+ {''.join(rows)}
230
+ </tbody>
231
+ </table>
232
+ </div>
233
+ """
234
 
 
 
235
 
236
+ def update_treebanks(language):
237
+ choices = treebank_choices(language)
238
 
239
+ return gr.Dropdown(
240
+ choices=choices,
241
+ value=default_treebank(language),
242
+ )
243
 
 
244
 
245
+ def selected_model_id(display_name):
246
+ if not display_name or display_name not in DISPLAY_TO_ID:
247
+ raise ValueError("Please select a valid language and treebank.")
248
 
249
+ return DISPLAY_TO_ID[display_name]
 
 
250
 
 
 
 
 
 
 
 
251
 
252
+ @lru_cache(maxsize=128)
253
+ def load_vocab_chars_for_model(model_id):
254
+
255
+ item = REGISTRY[model_id]
256
+
257
+ vocab_path = hf_hub_download(
258
+ repo_id=MODEL_REPO_ID,
259
+ repo_type="model",
260
+ filename=f"{MODEL_ROOT}/{item['folder']}/{item['vocab_file']}",
 
 
 
 
 
261
  )
262
 
263
+ with open(vocab_path, encoding="utf8") as f:
264
+ vocab_data = json.load(f)
265
 
266
+ return set(vocab_data["char2idx"]) - {"<pad>", "<sos>", "<eos>", "<unk>"}
 
267
 
268
 
269
+ def unsupported_input_for_model(text, allowed_chars, max_bad_ratio=0.60, min_checked_chars=4):
270
+
271
+ checked = []
272
+ bad = []
273
 
274
+ for ch in text:
275
+ if ch.isspace():
276
+ continue
277
 
278
+
279
+ if ch.isdigit() or ch in {".", ",", ";", ":", "!", "?", "-", "'", '"', "(", ")", "[", "]", "/"}:
280
+ continue
281
 
282
+ checked.append(ch)
283
 
284
+ if ch not in allowed_chars:
285
+ bad.append(ch)
 
 
 
 
286
 
287
+ if len(checked) < min_checked_chars:
288
+ return False, []
 
 
 
 
289
 
290
+ bad_ratio = len(bad) / len(checked)
291
 
292
+ return bad_ratio >= max_bad_ratio, sorted(set(bad))
 
 
293
 
294
+
295
+ def lemmatize_sentence(sentence, display_name):
296
+ sentence = str(sentence).strip()
297
+
298
+ if not sentence:
299
+ return "", ""
 
 
300
 
301
  try:
302
+ model_id = selected_model_id(display_name)
303
+ except ValueError as e:
304
+ return "", str(e)
305
+
306
+ allowed_chars = load_vocab_chars_for_model(model_id)
307
+
308
+ is_bad, bad = unsupported_input_for_model(sentence, allowed_chars)
309
+ if is_bad:
310
+ return "",f"This input does not seem to match the selected language/treebank. Please select your desired language and treebank, then try again. Unsupported characters: {' '.join(bad[:20])}"
311
+
312
+ lemmatizer = load_lemmatizer(model_id, DEVICE)
313
+
314
+ tokens = sentence.split()
315
+ lemmas = lemmatizer.lemmatize_sentence(tokens)
316
+
317
+ lemmatized_sentence = " ".join(lemmas)
318
+ token_html = make_html_table(tokens, lemmas)
319
+
320
+ return lemmatized_sentence, token_html
321
+
322
+
323
+ def parse_conllu_sentences_from_text(text):
324
+ text = text.strip()
325
+ sents = []
326
+
327
+ for block in re.split(r"\n\n+", text):
328
+ sent = []
329
+
330
+ for line in block.splitlines():
331
+ if not line or line.startswith("#"):
332
+ continue
333
+
334
+ cols = line.split("\t")
335
+
336
+ if len(cols) != 10:
337
+ continue
338
+
339
+ tok_id = cols[0]
340
+
341
+ if "-" in tok_id or "." in tok_id:
342
+ continue
343
+
344
+ form = cols[1]
345
+ sent.append(form)
346
+
347
+ if sent:
348
+ sents.append(sent)
349
+
350
+ return sents
351
+
352
+
353
+ def make_source_for_token(tokens, index, k_context, sep_char):
354
+ form = tokens[index]
355
+
356
+ left_context = tokens[max(0, index - k_context):index]
357
+ right_context = tokens[index + 1:index + 1 + k_context]
358
+
359
+ left = " ".join(left_context).strip()
360
+ right = " ".join(right_context).strip()
361
+
362
+ src_left = left + " " if left else ""
363
+ src_right = " " + right if right else ""
364
+
365
+ return f"{src_left}{sep_char}{form}{sep_char}{src_right}"
366
+
367
+
368
+ def make_all_sources_from_conllu(text, lemmatizer):
369
+ sents = parse_conllu_sentences_from_text(text)
370
+
371
+ sources = []
372
+
373
+ for tokens in sents:
374
+ for i in range(len(tokens)):
375
+ src_string = make_source_for_token(
376
+ tokens=tokens,
377
+ index=i,
378
+ k_context=lemmatizer.k_context,
379
+ sep_char=lemmatizer.sep_char,
380
+ )
381
+ sources.append(src_string)
382
+
383
+ return sources
384
+
385
+
386
+ def predict_sources_batched(sources, lemmatizer, batch_size=BATCH_SIZE):
387
+ preds_all = []
388
+
389
+ if not sources:
390
+ return preds_all
391
+
392
+ pad_id = lemmatizer.vocab.char2idx["<pad>"]
393
+ sos_id = lemmatizer.vocab.char2idx["<sos>"]
394
+ eos_id = lemmatizer.vocab.char2idx["<eos>"]
395
+
396
+ for start in range(0, len(sources), batch_size):
397
+ batch_sources = sources[start:start + batch_size]
398
+
399
+ src_ids_list = []
400
+ src_lens = []
401
+
402
+ for src_string in batch_sources:
403
+ src_ids = (
404
+ [sos_id]
405
+ + lemmatizer.vocab.encode(src_string)
406
+ + [eos_id]
407
+ )
408
+
409
+ src_ids_list.append(src_ids)
410
+ src_lens.append(len(src_ids))
411
+
412
+ max_len = max(src_lens)
413
+
414
+ padded = [
415
+ ids + [pad_id] * (max_len - len(ids))
416
+ for ids in src_ids_list
417
+ ]
418
+
419
+ src = torch.tensor(
420
+ padded,
421
+ dtype=torch.long,
422
+ device=lemmatizer.device,
423
  )
424
 
425
+ src_lens_tensor = torch.tensor(
426
+ src_lens,
427
+ dtype=torch.long,
428
+ device=lemmatizer.device,
429
+ )
430
 
431
+ batch_preds = lemmatizer.model.generate(
432
+ src,
433
+ src_lens_tensor,
434
+ lemmatizer.vocab,
435
+ max_len=lemmatizer.max_gen_len,
436
  )
437
 
438
+ preds_all.extend(batch_preds)
439
+
440
+ return preds_all
441
+
442
 
443
+ def predict_conllu_lemmas(text, lemmatizer):
444
+ sources = make_all_sources_from_conllu(text, lemmatizer)
445
 
446
+ return predict_sources_batched(
447
+ sources=sources,
448
+ lemmatizer=lemmatizer,
449
+ batch_size=BATCH_SIZE,
 
 
450
  )
451
 
452
 
453
+ def write_back_conllu(input_text, preds_all):
454
+ text = input_text.rstrip("\n")
455
+ blocks = re.split(r"\n\n+", text)
456
+
457
+ out_blocks = []
458
+ p = 0
459
+
460
+ for block in blocks:
461
+ lines = block.split("\n")
462
+ new_lines = []
463
+
464
+ for line in lines:
465
+ if not line or line.startswith("#"):
466
+ new_lines.append(line)
467
+ continue
468
+
469
+ cols = line.split("\t")
470
+
471
+ if len(cols) != 10:
472
+ new_lines.append(line)
473
+ continue
474
+
475
+ tok_id = cols[0]
476
+
477
+ if "-" in tok_id or "." in tok_id:
478
+ new_lines.append(line)
479
+ continue
480
+
481
+ pred = preds_all[p] if p < len(preds_all) else "_"
482
+ cols[TARGET_COL_IDX] = pred if pred else "_"
483
+
484
+ new_lines.append("\t".join(cols))
485
+ p += 1
486
+
487
+ out_blocks.append("\n".join(new_lines))
488
+
489
+ output_text = "\n\n".join(out_blocks).rstrip() + "\n\n"
490
+
491
+ return output_text, p
492
+
493
+
494
+ def lemmatize_conllu_file(file_obj, display_name):
495
+ if file_obj is None:
496
+ return gr.update(value=None, visible=False), "Please upload a CoNLL-U file."
497
+
498
+ try:
499
+ model_id = selected_model_id(display_name)
500
+ except ValueError as e:
501
+ return gr.update(value=None, visible=False), str(e)
502
+
503
+ input_path = Path(file_obj.name)
504
+
505
+ with open(input_path, encoding="utf8") as f:
506
+ text = f.read()
507
+
508
+ lemmatizer = load_lemmatizer(model_id, DEVICE)
509
+
510
+ preds = predict_conllu_lemmas(text, lemmatizer)
511
+ output_text, total = write_back_conllu(text, preds)
512
+
513
+ safe_model_name = display_name.replace(" ", "_").replace("-", "_")
514
+ safe_model_name = re.sub(r"[^A-Za-z0-9_]+", "", safe_model_name)
515
+
516
+ out_path = (
517
+ Path(tempfile.gettempdir())
518
+ / f"{input_path.stem}.{safe_model_name}.lemmatized.conllu"
519
+ )
520
+
521
+ with open(out_path, "w", encoding="utf8", newline="\n") as f:
522
+ f.write(output_text)
523
+
524
+ message = (
525
+ f"Done. Wrote {total:,} lemma predictions.\n"
526
+ f"Input used: FORM column only.\n"
527
+ f"Updated column: LEMMA only.\n"
528
+ f"All other CoNLL-U columns and comments were preserved."
529
+ )
530
+
531
+ return gr.update(value=str(out_path), visible=True), message
532
+
533
+
534
+ def reset_download_button(file_obj):
535
+ return gr.update(value=None, visible=False), ""
536
+
537
+
538
+ DEFAULT_LANGUAGE = default_language()
539
+ DEFAULT_TREEBANK = default_treebank(DEFAULT_LANGUAGE)
540
 
541
 
542
  with gr.Blocks(
543
+ title="oldslaviclemma",
544
  css=CUSTOM_CSS,
545
  theme=gr.themes.Soft(
546
+ primary_hue="blue",
547
+ secondary_hue="sky",
548
  neutral_hue="slate",
549
  ),
550
  ) as demo:
 
 
551
  with gr.Column(elem_id="main-card"):
552
+ gr.Markdown("# oldslaviclemma", elem_id="title")
553
+
554
+ gr.Markdown(
555
+ "Select a language and treebank. Paste one sentence or upload a tokenized CoNLL-U file. "
556
+ "The system returns lemma predictions while preserving the original tokenization.",
557
+ elem_id="subtitle",
558
+ )
559
+
560
+ gr.HTML(
561
+ """
562
+ <div id="badge-row">
563
+ <span>oldslaviclemma</span>
564
+ <span>60+ languages</span>
565
+ <span>110+ treebanks</span>
566
+ <span>UD v2.12</span>
567
+ <span>Lemmatization</span>
568
+ </div>
569
+ """
570
  )
571
 
572
  with gr.Row():
573
+ language_input = gr.Dropdown(
574
+ label="Language",
575
+ choices=LANGUAGES,
576
+ value=DEFAULT_LANGUAGE,
577
+ )
578
+
579
+ treebank_input = gr.Dropdown(
580
+ label="Treebank",
581
+ choices=treebank_choices(DEFAULT_LANGUAGE),
582
+ value=DEFAULT_TREEBANK,
583
+ )
584
+
585
+ with gr.Tab("Sentence input"):
586
+ sentence_input = gr.Textbox(
587
+ label="Input sentence",
588
+ lines=5,
589
+ value="",
590
+ placeholder="Paste a sentence with words separated by spaces...",
591
+ )
592
+
593
+ run_button = gr.Button("Lemmatize sentence")
594
+
595
+ sentence_output = gr.Textbox(
596
+ label="Lemmatized sentence",
597
+ lines=5,
598
+ elem_id="output-box",
599
+ )
600
+
601
+ token_output = gr.HTML(
602
+ label="Word-level output",
603
+ value="",
604
+ )
605
+
606
+ gr.Markdown(
607
+ "Please paste one sentence with whitespace tokenization.",
608
+ elem_id="note",
609
+ )
610
+
611
+ with gr.Tab("CoNLL-U file input"):
612
+ gr.Markdown(
613
+ "Upload an already-tokenized CoNLL-U file. "
614
+ "The app reads the FORM column, predicts the LEMMA column, "
615
+ "and preserves comments, token IDs, UPOS, XPOS, FEATS, HEAD, DEPREL, DEPS, and MISC."
616
+ )
617
+
618
+ conllu_input = gr.File(
619
+ label="Upload CoNLL-U file",
620
+ file_types=[".conllu", ".txt"],
621
+ )
622
+
623
+ conllu_button = gr.Button("Lemmatize CoNLL-U file")
624
+
625
+ conllu_output = gr.DownloadButton(
626
+ label="Download lemmatized CoNLL-U file",
627
+ value=None,
628
+ visible=False,
629
+ )
630
+
631
+ conllu_message = gr.Textbox(
632
+ label="Status",
633
+ lines=4,
634
+ )
635
+
636
+ language_input.change(
637
+ fn=update_treebanks,
638
+ inputs=language_input,
639
+ outputs=treebank_input,
640
  )
641
 
642
+ run_button.click(
643
+ fn=lemmatize_sentence,
644
+ inputs=[sentence_input, treebank_input],
645
+ outputs=[sentence_output, token_output],
646
+ )
 
647
 
648
+ conllu_input.change(
649
+ fn=reset_download_button,
650
+ inputs=conllu_input,
651
+ outputs=[conllu_output, conllu_message],
652
+ )
 
653
 
654
+ conllu_button.click(
655
+ fn=lemmatize_conllu_file,
656
+ inputs=[conllu_input, treebank_input],
657
+ outputs=[conllu_output, conllu_message],
658
+ )
 
659
 
660
 
661
+ demo.queue()
662
+ demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
inference.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from functools import lru_cache
3
+ from pathlib import Path
4
+
5
+ import torch
6
+ from huggingface_hub import hf_hub_download
7
+
8
+ try:
9
+ from model import LemmaModel, Vocab
10
+ except ImportError:
11
+ from .model import LemmaModel, Vocab
12
+
13
+
14
+ MODEL_REPO_ID = "usmannawaz/oldslaviclemma"
15
+ MODEL_ROOT = "oldslaviclemma212"
16
+
17
+
18
+ @lru_cache(maxsize=1)
19
+ def load_registry(registry_path="models_registry.json"):
20
+
21
+ registry_path = Path(registry_path)
22
+ with registry_path.open(encoding="utf8") as f:
23
+ return json.load(f)
24
+
25
+
26
+ class OldSlavicLemmatizer:
27
+ def __init__(self, model, vocab, config, device):
28
+ self.model = model
29
+ self.vocab = vocab
30
+ self.config = config
31
+ self.device = torch.device(device)
32
+ self.sep_char = config.get("sep_char", "⟂")
33
+ self.k_context = int(config.get("k_context", 2))
34
+ self.max_gen_len = int(config.get("max_gen_len", 30))
35
+
36
+ def make_source(self, form, left_context=None, right_context=None):
37
+ left_context = left_context or []
38
+ right_context = right_context or []
39
+
40
+ left = " ".join(left_context[-self.k_context:]).strip()
41
+ right = " ".join(right_context[:self.k_context]).strip()
42
+
43
+ src_left = left + " " if left else ""
44
+ src_right = " " + right if right else ""
45
+
46
+ return f"{src_left}{self.sep_char}{form}{self.sep_char}{src_right}"
47
+
48
+ def lemmatize(self, form, left_context=None, right_context=None):
49
+ src_string = self.make_source(
50
+ form=form,
51
+ left_context=left_context,
52
+ right_context=right_context,
53
+ )
54
+
55
+ src_ids = (
56
+ [self.vocab.char2idx["<sos>"]]
57
+ + self.vocab.encode(src_string)
58
+ + [self.vocab.char2idx["<eos>"]]
59
+ )
60
+
61
+ src = torch.tensor([src_ids], dtype=torch.long, device=self.device)
62
+ src_lens = torch.tensor([len(src_ids)], dtype=torch.long, device=self.device)
63
+
64
+ return self.model.generate(
65
+ src,
66
+ src_lens,
67
+ self.vocab,
68
+ max_len=self.max_gen_len,
69
+ )[0]
70
+
71
+ def lemmatize_sentence(self, tokens):
72
+ lemmas = []
73
+
74
+ for i, token in enumerate(tokens):
75
+ left_context = tokens[max(0, i - self.k_context):i]
76
+ right_context = tokens[i + 1:i + 1 + self.k_context]
77
+
78
+ lemma = self.lemmatize(
79
+ token,
80
+ left_context=left_context,
81
+ right_context=right_context,
82
+ )
83
+
84
+ lemmas.append(lemma)
85
+
86
+ return lemmas
87
+
88
+
89
+ @lru_cache(maxsize=3)
90
+ def load_lemmatizer(model_id, device=None):
91
+
92
+ if device is None:
93
+ device = "cuda" if torch.cuda.is_available() else "cpu"
94
+ device = torch.device(device)
95
+
96
+ registry = load_registry("models_registry.json")
97
+ if model_id not in registry:
98
+ raise KeyError(f"Model id not found in registry: {model_id}")
99
+
100
+ item = registry[model_id]
101
+ folder = item["folder"]
102
+
103
+ config_path = hf_hub_download(
104
+ repo_id=MODEL_REPO_ID,
105
+ repo_type="model",
106
+ filename=f"{MODEL_ROOT}/{folder}/{item['config_file']}",
107
+ )
108
+
109
+ vocab_path = hf_hub_download(
110
+ repo_id=MODEL_REPO_ID,
111
+ repo_type="model",
112
+ filename=f"{MODEL_ROOT}/{folder}/{item['vocab_file']}",
113
+ )
114
+
115
+ weights_path = hf_hub_download(
116
+ repo_id=MODEL_REPO_ID,
117
+ repo_type="model",
118
+ filename=f"{MODEL_ROOT}/{folder}/{item['model_file']}",
119
+ )
120
+
121
+ with open(config_path, encoding="utf8") as f:
122
+ config = json.load(f)
123
+
124
+ with open(vocab_path, encoding="utf8") as f:
125
+ vocab_data = json.load(f)
126
+
127
+ vocab = Vocab(
128
+ char2idx=vocab_data["char2idx"],
129
+ idx2char=vocab_data["idx2char"],
130
+ )
131
+
132
+ model = LemmaModel(
133
+ vocab_size=len(vocab.char2idx),
134
+ char_emb_dim=int(config["char_emb_dim"]),
135
+ hidden_size=int(config["hidden_size"]),
136
+ drop_prob=float(config["drop_prob"]),
137
+ num_heads=int(config["num_heads"]),
138
+ max_gen_len=int(config.get("max_gen_len", 30)),
139
+ ).to(device)
140
+
141
+ state = torch.load(weights_path, map_location=device)
142
+ model.load_state_dict(state)
143
+ model.eval()
144
+
145
+ return OldSlavicLemmatizer(
146
+ model=model,
147
+ vocab=vocab,
148
+ config=config,
149
+ device=device,
150
+ )
model.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ import torch
3
+ import torch.nn as nn
4
+
5
+
6
+ class Vocab:
7
+ def __init__(self, char2idx=None, idx2char=None):
8
+ if char2idx is None:
9
+ char2idx = {
10
+ "<pad>": 0,
11
+ "<sos>": 1,
12
+ "<eos>": 2,
13
+ "<unk>": 3,
14
+ }
15
+ self.char2idx = char2idx
16
+ if idx2char is None:
17
+ self.idx2char = {i: c for c, i in self.char2idx.items()}
18
+ else:
19
+ self.idx2char = {int(k): v for k, v in idx2char.items()}
20
+
21
+ def encode(self, s: str) -> List[int]:
22
+ unk = self.char2idx["<unk>"]
23
+ return [self.char2idx.get(ch, unk) for ch in s]
24
+
25
+ def decode(self, ids: List[int]) -> str:
26
+ out = []
27
+ eos_id = self.char2idx["<eos>"]
28
+ for i in ids:
29
+ if i == eos_id:
30
+ break
31
+ if i > eos_id:
32
+ out.append(self.idx2char.get(int(i), ""))
33
+ return "".join(out)
34
+
35
+
36
+ class LemmaModel(nn.Module):
37
+ def __init__(
38
+ self,
39
+ vocab_size: int,
40
+ char_emb_dim: int = 96,
41
+ hidden_size: int = 128,
42
+ drop_prob: float = 0.30,
43
+ num_heads: int = 16,
44
+ max_gen_len: int = 30,
45
+ ):
46
+ super().__init__()
47
+ self.max_gen_len = max_gen_len
48
+ self.emb = nn.Embedding(vocab_size, char_emb_dim, padding_idx=0)
49
+ self.dropout_enc = nn.Dropout(drop_prob)
50
+ self.dropout_dec = nn.Dropout(drop_prob)
51
+ self.dropout_att = nn.Dropout(drop_prob)
52
+ self.enc1 = nn.LSTM(char_emb_dim, hidden_size, bidirectional=True, batch_first=True)
53
+ self.enc2 = nn.LSTM(hidden_size * 2, hidden_size, bidirectional=True, batch_first=True)
54
+ self.attn = nn.MultiheadAttention(hidden_size * 2, num_heads, batch_first=True)
55
+ self.dec = nn.LSTM(char_emb_dim + hidden_size * 4, hidden_size * 2, batch_first=True)
56
+ self.dec_cross_attn = nn.MultiheadAttention(
57
+ embed_dim=hidden_size * 2,
58
+ num_heads=num_heads,
59
+ kdim=hidden_size * 4,
60
+ vdim=hidden_size * 4,
61
+ batch_first=True,
62
+ )
63
+ self.out = nn.Linear(hidden_size * 2, vocab_size, bias=True)
64
+
65
+ def encode(self, src, src_lens):
66
+ emb = self.emb(src)
67
+ packed1 = nn.utils.rnn.pack_padded_sequence(
68
+ emb, src_lens.cpu(), batch_first=True, enforce_sorted=False
69
+ )
70
+ enc1_o, _ = self.enc1(packed1)
71
+ enc1_o, _ = nn.utils.rnn.pad_packed_sequence(enc1_o, batch_first=True)
72
+ enc1_o = self.dropout_enc(enc1_o)
73
+ packed2 = nn.utils.rnn.pack_padded_sequence(
74
+ enc1_o, src_lens.cpu(), batch_first=True, enforce_sorted=False
75
+ )
76
+ enc2_o, _ = self.enc2(packed2)
77
+ enc2_o, _ = nn.utils.rnn.pad_packed_sequence(enc2_o, batch_first=True)
78
+ enc2_o = self.dropout_enc(enc2_o)
79
+ attn_o, _ = self.attn(enc1_o, enc2_o, enc2_o)
80
+ attn_o = self.dropout_att(attn_o)
81
+ return torch.cat([enc2_o, attn_o], dim=-1)
82
+
83
+ def forward(self, src, src_lens, tgt):
84
+ encoder_combined = self.encode(src, src_lens)
85
+ dt = self.emb(tgt[:, :-1])
86
+ target_len = dt.size(1)
87
+ if encoder_combined.size(1) >= target_len:
88
+ comb_trim = encoder_combined[:, :target_len, :]
89
+ else:
90
+ pad = encoder_combined.new_zeros(
91
+ encoder_combined.size(0),
92
+ target_len - encoder_combined.size(1),
93
+ encoder_combined.size(2),
94
+ )
95
+ comb_trim = torch.cat([encoder_combined, pad], dim=1)
96
+ dec_inp = torch.cat([dt, comb_trim], dim=-1)
97
+ dec_o, _ = self.dec(dec_inp)
98
+ dec_o = self.dropout_dec(dec_o)
99
+ cross_out, _ = self.dec_cross_attn(dec_o, encoder_combined, encoder_combined)
100
+ cross_out = self.dropout_att(cross_out)
101
+ return self.out(cross_out)
102
+
103
+ def generate(self, src, src_lens, vocab, max_len=None):
104
+ self.eval()
105
+ if max_len is None:
106
+ max_len = self.max_gen_len
107
+ batch_size = src.size(0)
108
+ with torch.no_grad():
109
+ encoder_combined = self.encode(src, src_lens)
110
+ source_len = encoder_combined.size(1)
111
+ cur = torch.full(
112
+ (batch_size, 1), vocab.char2idx["<sos>"], device=src.device, dtype=torch.long
113
+ )
114
+ hidden = None
115
+ hyps = [[] for _ in range(batch_size)]
116
+ for step in range(max_len):
117
+ emb_t = self.emb(cur).squeeze(1)
118
+ if source_len == 0:
119
+ comb_t = encoder_combined[:, 0, :]
120
+ else:
121
+ comb_t = encoder_combined[:, min(step, source_len - 1), :]
122
+ dec_inp_t = torch.cat([emb_t, comb_t], dim=-1).unsqueeze(1)
123
+ dec_o, hidden = self.dec(dec_inp_t, hidden)
124
+ dec_o = self.dropout_dec(dec_o)
125
+ cross_out, _ = self.dec_cross_attn(dec_o, encoder_combined, encoder_combined)
126
+ cross_out = self.dropout_att(cross_out)
127
+ logits = self.out(cross_out)
128
+ cur = logits.argmax(-1)
129
+ for i in range(batch_size):
130
+ hyps[i].append(int(cur[i, 0].item()))
131
+ if all(int(cur[i, 0].item()) == vocab.char2idx["<eos>"] for i in range(batch_size)):
132
+ break
133
+ return [vocab.decode(h) for h in hyps]
requirements.txt CHANGED
@@ -1,5 +1,3 @@
 
1
  gradio
2
- stanza
3
- huggingface_hub
4
- pandas
5
- torch
 
1
+ torch
2
  gradio
3
+ huggingface_hub