unijoh commited on
Commit
78d4b6c
·
verified ·
1 Parent(s): 738c41b

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +378 -212
  2. tag_labels.json +25 -25
app.py CHANGED
@@ -1,10 +1,12 @@
1
- import os, re, string, json
 
 
 
2
  from collections import defaultdict
3
 
4
  import gradio as gr
5
  import torch
6
  import numpy as np
7
- import pandas as pd
8
  from transformers import AutoTokenizer, AutoModelForTokenClassification
9
 
10
  # ----------------------------
@@ -13,7 +15,7 @@ from transformers import AutoTokenizer, AutoModelForTokenClassification
13
  MODEL_ID = "Setur/BRAGD"
14
  TAGS_FILEPATH = "Sosialurin-BRAGD_tags.csv" # must match model labels
15
  LABELS_FILEPATH = "tag_labels.json" # add to repo root (FO+EN labels)
16
- HF_TOKEN = os.getenv("BRAGD") # Space secret
17
 
18
  if not HF_TOKEN:
19
  raise RuntimeError("Missing BRAGD token secret (Space → Settings → Secrets → BRAGD).")
@@ -26,144 +28,267 @@ INTERVALS = (
26
  (51, 53), (54, 60), (61, 63), (64, 66), (67, 70), (71, 72)
27
  )
28
 
29
- GROUP_ORDER = ["subcategory","gender","number","case","article","proper","degree","declension","mood","voice","tense","person","definiteness"]
 
 
 
30
 
31
- # You said Subcategory B doesn't exist and will be deleted from the CSV:
32
  HIDE_CODES = {"subcategory": {"B"}}
33
 
34
  UI = {
35
- "fo": {"w":"Orð", "t":"Mark", "s":"Útgreining", "m":"Merking"},
36
- "en": {"w":"Word","t":"Tag", "s":"Analysis", "m":"Meaning"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  }
38
 
39
  # Theme color: #89AFA9 (+ close shades)
40
- CSS = """
41
  :root{
42
  --primary-500:#89AFA9; --primary-600:#6F9992; --primary-700:#5B7F79;
43
  --primary-100:#E1ECEA; --primary-200:#C6DAD6;
44
  }
45
- .gr-button-primary, button.primary, .primary{
46
- background:var(--primary-500)!important; border-color:var(--primary-600)!important; color:#0b1b19!important;
 
 
 
 
47
  }
48
- .gr-button-primary:hover, button.primary:hover, .primary:hover{ background:var(--primary-600)!important; }
49
  a{ color:var(--primary-700)!important; }
50
 
51
- /* Dataframe column sizing:
52
- - word + tag stay on one line
53
- - analysis can wrap only if needed
54
- */
55
- .gr-dataframe table { table-layout: auto !important; width: 100% !important; }
56
- .gr-dataframe th, .gr-dataframe td { vertical-align: top; }
57
- .gr-dataframe th:nth-child(1), .gr-dataframe td:nth-child(1) { white-space: nowrap; width: 1%; }
58
- .gr-dataframe th:nth-child(2), .gr-dataframe td:nth-child(2) { white-space: nowrap; min-width: 8.5rem; }
59
- .gr-dataframe th:nth-child(3), .gr-dataframe td:nth-child(3) { white-space: normal; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  """
61
 
62
- def simp_tok(s: str):
63
- return re.findall(r"\w+|[" + re.escape(string.punctuation) + "]", s)
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- def load_tag_mappings(path: str):
66
- df = pd.read_csv(path)
67
- feature_cols = list(df.columns[1:])
68
- tag_to_features = {row["Original Tag"]: row[1:].values.astype(int) for _, row in df.iterrows()}
69
- features_to_tag = {tuple(row[1:].values.astype(int)): row["Original Tag"] for _, row in df.iterrows()}
70
  return tag_to_features, features_to_tag, len(feature_cols), feature_cols
71
 
72
  def group_from_col(col: str):
73
- if col == "Article": return ("article","A")
74
- if col.startswith("No-Article "): return ("article", col.split()[-1])
75
- if col == "Proper Noun": return ("proper","P")
76
- if col.startswith("Not-Proper-Noun "): return ("proper", col.split()[-1])
 
 
 
 
77
 
78
  prefixes = [
79
- ("Word Class ","word_class"),
80
- ("Subcategory ","subcategory"), ("No-Subcategory ","subcategory"),
81
- ("Gender ","gender"), ("No-Gender ","gender"),
82
- ("Number ","number"), ("No-Number ","number"),
83
- ("Case ","case"), ("No-Case ","case"),
84
- ("Degree ","degree"), ("No-Degree ","degree"),
85
- ("Declension ","declension"), ("No-Declension ","declension"),
86
- ("Mood ","mood"),
87
- ("Voice ","voice"), ("No-Voice ","voice"),
88
- ("Tense ","tense"), ("No-Tense ","tense"),
89
- ("Person ","person"), ("No-Person ","person"),
90
- ("Definite ","definiteness"), ("Indefinite ","definiteness"),
91
  ]
92
- for p,g in prefixes:
93
  if col.startswith(p):
94
  return (g, col.split()[-1])
95
- return (None,None)
 
96
 
97
  def process_tag_features(tag_to_features: dict, intervals):
98
- arrs = [np.array(tpl) for tpl in set(tuple(a) for a in tag_to_features.values())]
99
- wt_masks = {wt:[a for a in arrs if a[wt]==1] for wt in range(15)}
100
- out = {}
101
- for wt,labels in wt_masks.items():
 
 
 
 
 
102
  if not labels:
103
- out[wt]=[]
104
  continue
105
  sum_labels = np.sum(np.array(labels), axis=0)
106
- out[wt] = [iv for iv in intervals if np.sum(sum_labels[iv[0]:iv[1]+1]) != 0]
107
- return out
 
 
108
 
109
- def predict_vectors(logits, attention_mask, begin_tokens, dict_intervals, vec_len):
110
  softmax = torch.nn.Softmax(dim=0)
111
  vectors = []
 
112
  for idx in range(len(logits)):
113
- if attention_mask[idx].item()!=1 or begin_tokens[idx]!=1:
 
 
114
  continue
115
 
116
- pred = logits[idx]
117
  vec = torch.zeros(vec_len, device=logits.device)
118
 
119
- wt = torch.argmax(softmax(pred[0:15])).item()
120
- vec[wt]=1
 
 
121
 
122
- for (a,b) in dict_intervals.get(wt, []):
123
- seg = pred[a:b+1]
124
- k = torch.argmax(softmax(seg)).item()
125
- vec[a+k]=1
 
 
126
 
127
  vectors.append(vec)
 
128
  return vectors
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  # ----------------------------
131
- # Load labels (extracted from your XLSX)
132
  # ----------------------------
133
  with open(LABELS_FILEPATH, "r", encoding="utf-8") as f:
134
  LABELS = json.load(f)
135
 
136
- def label_for(lang: str, group: str, wc: str, code: str) -> str:
137
- lang = "fo" if lang=="fo" else "en"
138
  by_wc = LABELS.get(lang, {}).get("by_word_class", {})
139
  glob = LABELS.get(lang, {}).get("global", {})
140
- if wc and wc in by_wc and code in by_wc[wc].get(group, {}):
141
- return by_wc[wc][group][code]
 
142
  return glob.get(group, {}).get(code, "")
143
 
144
  # ----------------------------
145
- # Load CSV mappings (authoritative)
146
  # ----------------------------
147
  tag_to_features, features_to_tag, VEC_LEN, FEATURE_COLS = load_tag_mappings(TAGS_FILEPATH)
148
 
149
- # ----------------------------
150
- # Load model
151
- # ----------------------------
152
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN)
153
  model = AutoModelForTokenClassification.from_pretrained(MODEL_ID, token=HF_TOKEN)
 
154
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
155
- model.to(device); model.eval()
 
156
 
157
  if hasattr(model, "config") and hasattr(model.config, "num_labels"):
158
  if model.config.num_labels != VEC_LEN:
159
- raise RuntimeError(f"Label size mismatch: model={model.config.num_labels}, csv={VEC_LEN}. Wrong CSV?")
 
 
 
160
 
161
  DICT_INTERVALS = process_tag_features(tag_to_features, INTERVALS)
162
 
163
- # Build GROUPS from CSV headers
164
- GROUPS = defaultdict(list) # group -> [(idx, code, colname)]
165
- for i,col in enumerate(FEATURE_COLS):
166
- g,code = group_from_col(col)
167
  if g and code not in HIDE_CODES.get(g, set()):
168
  GROUPS[g].append((i, code, col))
169
 
@@ -171,86 +296,99 @@ def vector_to_tag(vec: torch.Tensor) -> str:
171
  return features_to_tag.get(tuple(vec.int().tolist()), "Unknown Tag")
172
 
173
  def wc_code(vec: torch.Tensor) -> str:
174
- for idx,code,_ in GROUPS["word_class"]:
175
- if int(vec[idx].item())==1:
176
  return code
177
  return ""
178
 
179
  def group_code(vec: torch.Tensor, group: str) -> str:
180
  hidden = HIDE_CODES.get(group, set())
181
- for idx,code,_ in GROUPS.get(group, []):
182
  if code in hidden:
183
  continue
184
- if int(vec[idx].item())==1:
185
  return code
186
  return ""
187
 
188
- def clean_label(s: str) -> str:
189
- s = (s or "").strip()
190
- s = re.sub(r"\s+", " ", s)
191
- # remove leading punctuation/hyphen artifacts
192
- s = s.strip(" -;,:")
193
- return s
194
 
195
- def visible_summary(vec: torch.Tensor, lang: str) -> str:
196
  """
197
  Útgreining / Analysis:
198
- - ONLY words/labels (no letters, no hyphens like "X –")
199
- - word + tag columns stay single-line; analysis wraps only if needed (CSS)
 
 
200
  """
201
- lang = "fo" if lang=="fo" else "en"
202
  raw_tag = vector_to_tag(vec)
203
  wc = wc_code(vec)
204
 
205
- # Special-case: DGd should show ONLY "fyriseting"/"preposition"
206
  if raw_tag == "DGd":
207
  return "fyriseting" if lang == "fo" else "preposition"
208
 
209
- wc_lbl = clean_label(label_for(lang, "word_class", wc, wc) or wc)
 
 
 
 
 
210
 
211
  labels = []
212
 
213
- # For pronouns: don't start with the main word-class label (subcategories already include it)
214
- if wc != "P":
215
  if wc_lbl:
216
  labels.append(wc_lbl)
217
 
 
218
  for g in GROUP_ORDER:
219
  c = group_code(vec, g)
220
  if not c:
221
  continue
222
-
223
- # Hide "stýrir falli" / "stýrir ikki falli" in Útgreining (but keep them in expanded tags)
224
- if wc == "D" and g == "subcategory" and c in {"G", "N"}:
225
- continue
226
-
227
- lbl = label_for(lang, g, wc, c) or label_for(lang, g, "", c) or ""
228
- lbl = clean_label(lbl)
229
  if not lbl:
230
  continue
231
 
232
- # Extra safety in case the exact phrases come from labels
233
- if lang == "fo" and lbl in {"stýrir falli", "stýrir ikki falli"}:
 
234
  continue
235
- if lang == "en" and lbl.lower() in {"governs case", "does not govern case"}:
 
 
 
236
  continue
237
 
238
- if lbl not in labels:
239
- labels.append(lbl)
240
 
241
- # If pronoun ended up empty (shouldn't), fall back to word-class label
242
- if not labels and wc_lbl:
243
- labels = [wc_lbl]
 
 
244
 
245
- return ", ".join(labels)
 
 
 
 
 
 
246
 
 
247
 
248
- def meaning_detail(vec: torch.Tensor, lang: str) -> str:
249
  """
250
- Merking / Meaning (more explicit):
251
- keeps codes + labels (useful for debugging and linguists)
252
  """
253
- lang = "fo" if lang=="fo" else "en"
254
  wc = wc_code(vec)
255
  parts = []
256
 
@@ -266,85 +404,79 @@ def meaning_detail(vec: torch.Tensor, lang: str) -> str:
266
 
267
  return "; ".join([p for p in parts if p])
268
 
269
- def compute_codes_by_wc():
 
 
 
 
 
 
 
270
  codes = defaultdict(lambda: defaultdict(set)) # wc -> group -> set(code)
271
  for arr in tag_to_features.values():
272
  arr = np.array(arr)
273
 
274
  wc = None
275
- for idx,code,_ in GROUPS["word_class"]:
276
- if arr[idx]==1:
277
  wc = code
278
  break
279
  if not wc:
280
  continue
281
 
282
  for g in GROUP_ORDER:
283
- hidden = HIDE_CODES.get(g, set())
284
- for idx,code,_ in GROUPS.get(g, []):
285
- if code in hidden:
286
  continue
287
- if arr[idx]==1:
288
  codes[wc][g].add(code)
289
 
290
- return codes
291
-
292
- CODES_BY_WC = compute_codes_by_wc()
293
-
294
- def build_legend(lang: str) -> str:
295
- """
296
- Elaborate overview:
297
- Under each orðaflokkur / word class, show the letter codes actually used in the CURRENT CSV,
298
- with labels from tag_labels.json (fallback to code if missing).
299
- """
300
- lang = "fo" if lang=="fo" else "en"
301
- title = "### Markingaryvirlit" if lang=="fo" else "### Tag legend"
302
  lines = [title, ""]
303
 
304
- for wc in sorted(CODES_BY_WC.keys()):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  wcl = label_for(lang, "word_class", wc, wc) or ""
306
  lines.append(f"#### {wc} — {wcl}" if wcl else f"#### {wc}")
307
 
308
  for g in GROUP_ORDER:
309
- cs = sorted(CODES_BY_WC[wc].get(g, set()))
310
  if not cs:
311
  continue
312
 
313
- # group header
314
- if lang=="fo":
315
- group_name = {
316
- "subcategory":"Undirflokkur",
317
- "gender":"Kyn",
318
- "number":"Tal",
319
- "case":"Fall",
320
- "article":"Bundni/óbundni",
321
- "proper":"Sernavn",
322
- "degree":"Stig",
323
- "declension":"Bending",
324
- "mood":"Háttur",
325
- "voice":"Søgn",
326
- "tense":"Tíð",
327
- "person":"Persónur",
328
- "definiteness":"Bundni/óbundni",
329
- }.get(g, g)
330
- else:
331
- group_name = {
332
- "subcategory":"Subcategory",
333
- "gender":"Gender",
334
- "number":"Number",
335
- "case":"Case",
336
- "article":"Definite suffix",
337
- "proper":"Proper noun",
338
- "degree":"Degree",
339
- "declension":"Declension",
340
- "mood":"Mood",
341
- "voice":"Voice",
342
- "tense":"Tense",
343
- "person":"Person",
344
- "definiteness":"Definiteness",
345
- }.get(g, g)
346
-
347
- lines.append(f"**{group_name}**")
348
  for c in cs:
349
  lbl = label_for(lang, g, wc, c) or label_for(lang, g, "", c)
350
  lines.append(f"- `{c}` — {lbl}" if lbl else f"- `{c}`")
@@ -354,10 +486,14 @@ def build_legend(lang: str) -> str:
354
 
355
  return "\n".join(lines).strip()
356
 
 
 
 
357
  def run_model(sentence: str):
358
  s = (sentence or "").strip()
359
  if not s:
360
  return []
 
361
  tokens = simp_tok(s)
362
  if not tokens:
363
  return []
@@ -377,88 +513,118 @@ def run_model(sentence: str):
377
  attention_mask = enc["attention_mask"].to(device)
378
  word_ids = enc.word_ids(batch_index=0)
379
 
380
- begin = []
 
381
  last = None
382
  for wid in word_ids:
383
  if wid is None:
384
- begin.append(0)
385
  elif wid != last:
386
- begin.append(1)
387
  else:
388
- begin.append(0)
389
  last = wid
390
 
391
  with torch.no_grad():
392
- logits = model(input_ids=input_ids, attention_mask=attention_mask).logits[0]
 
393
 
394
- vectors = predict_vectors(logits, attention_mask[0], begin, DICT_INTERVALS, VEC_LEN)
395
 
396
  rows = []
397
  vec_i = 0
398
- seen = set()
399
- for i,wid in enumerate(word_ids):
400
- if wid is None or begin[i]!=1 or wid in seen:
 
 
 
 
 
401
  continue
402
- seen.add(wid)
 
403
  word = tokens[wid] if wid < len(tokens) else "<UNK>"
404
  vec = vectors[vec_i] if vec_i < len(vectors) else torch.zeros(VEC_LEN, device=device)
405
  rows.append({"word": word, "vec": vec.int().tolist()})
406
  vec_i += 1
 
407
  return rows
408
 
409
- def render_main(rows_state, lang: str):
410
- lang = "fo" if lang=="fo" else "en"
411
- cols = [UI[lang]["w"], UI[lang]["t"], UI[lang]["s"]]
412
- if not rows_state:
413
- return pd.DataFrame(columns=cols), build_legend(lang), pd.DataFrame(columns=[UI[lang]["w"], UI[lang]["t"], UI[lang]["m"]])
 
 
 
414
 
415
- out_main = []
416
- out_mean = []
417
- for r in rows_state:
418
  vec = torch.tensor(r["vec"])
419
  tag = vector_to_tag(vec)
420
- out_main.append([r["word"], tag, visible_summary(vec, lang)])
421
- out_mean.append([r["word"], tag, meaning_detail(vec, lang)])
422
 
423
- df_main = pd.DataFrame(out_main, columns=cols)
424
- df_mean = pd.DataFrame(out_mean, columns=[UI[lang]["w"], UI[lang]["t"], UI[lang]["m"]])
425
- return df_main, build_legend(lang), df_mean
 
 
426
 
427
  # ----------------------------
428
- # Gradio UI
429
  # ----------------------------
430
  theme = gr.themes.Soft()
431
 
432
  with gr.Blocks(theme=theme, css=CSS, title="BRAGD-markarin") as demo:
433
- gr.Markdown("## BRAGD-markarin\nSkriv ein setning og fá hann markaðan.\n\n**Model:** `Setur/BRAGD`")
434
-
435
- inp = gr.Textbox(lines=3, label="Setningur / Sentence", placeholder="Skriv her… / Type here…")
436
- btn = gr.Button("Marka / Tag", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
  state = gr.State([])
439
 
440
- out_df = gr.Dataframe(wrap=True, interactive=False, label="Úrslit / Results")
441
-
442
- # Under results + can be changed AFTER tagging (no rerun; just re-render)
443
- lang = gr.Dropdown(choices=[("Føroyskt","fo"), ("English","en")], value="fo", label="Mál / Language")
444
-
445
  with gr.Accordion("Útgreinað marking / Expanded tags", open=False):
446
- out_mean_df = gr.Dataframe(wrap=True, interactive=False, label="")
447
 
448
- with gr.Accordion("Markingaryvirlit / Legend", open=False):
449
- legend_md = gr.Markdown(build_legend("fo"))
450
 
451
  def on_tag(sentence, lang_choice):
452
  rows = run_model(sentence)
453
- df_main, legend, df_mean = render_main(rows, lang_choice)
454
- return rows, df_main, legend, df_mean
455
 
456
  def on_lang(rows, lang_choice):
457
- df_main, legend, df_mean = render_main(rows, lang_choice)
458
- return df_main, legend, df_mean
459
 
460
- btn.click(on_tag, inputs=[inp, lang], outputs=[state, out_df, legend_md, out_mean_df])
461
- lang.change(on_lang, inputs=[state, lang], outputs=[out_df, legend_md, out_mean_df])
462
 
463
  if __name__ == "__main__":
464
  demo.launch()
 
1
+ import os
2
+ import re
3
+ import string
4
+ import json
5
  from collections import defaultdict
6
 
7
  import gradio as gr
8
  import torch
9
  import numpy as np
 
10
  from transformers import AutoTokenizer, AutoModelForTokenClassification
11
 
12
  # ----------------------------
 
15
  MODEL_ID = "Setur/BRAGD"
16
  TAGS_FILEPATH = "Sosialurin-BRAGD_tags.csv" # must match model labels
17
  LABELS_FILEPATH = "tag_labels.json" # add to repo root (FO+EN labels)
18
+ HF_TOKEN = os.getenv("BRAGD") # Space secret name
19
 
20
  if not HF_TOKEN:
21
  raise RuntimeError("Missing BRAGD token secret (Space → Settings → Secrets → BRAGD).")
 
28
  (51, 53), (54, 60), (61, 63), (64, 66), (67, 70), (71, 72)
29
  )
30
 
31
+ GROUP_ORDER = [
32
+ "subcategory", "gender", "number", "case", "article", "proper",
33
+ "degree", "declension", "mood", "voice", "tense", "person", "definiteness"
34
+ ]
35
 
36
+ # You said subcategory B doesn't exist and will be deleted from the CSV
37
  HIDE_CODES = {"subcategory": {"B"}}
38
 
39
  UI = {
40
+ "fo": {
41
+ "title": "BRAGD-markarin",
42
+ "inst": "Skriv ein setning og fá hann markaðan.",
43
+ "model": "Model:",
44
+ "word": "Orð",
45
+ "tag": "Mark",
46
+ "analysis": "Útgreining",
47
+ "results": "Úrslit",
48
+ "expanded": "Útgreinað marking",
49
+ "legend": "Markingaryvirlit",
50
+ "lang": "Mál",
51
+ },
52
+ "en": {
53
+ "title": "BRAGD tagger",
54
+ "inst": "Type a sentence and get it tagged.",
55
+ "model": "Model:",
56
+ "word": "Word",
57
+ "tag": "Tag",
58
+ "analysis": "Analysis",
59
+ "results": "Results",
60
+ "expanded": "Expanded tags",
61
+ "legend": "Tag legend",
62
+ "lang": "Language",
63
+ },
64
  }
65
 
66
  # Theme color: #89AFA9 (+ close shades)
67
+ CSS = r"""
68
  :root{
69
  --primary-500:#89AFA9; --primary-600:#6F9992; --primary-700:#5B7F79;
70
  --primary-100:#E1ECEA; --primary-200:#C6DAD6;
71
  }
72
+ .gr-button-primary{
73
+ background:var(--primary-500)!important;
74
+ border-color:var(--primary-600)!important;
75
+ color:#0b1b19!important;
76
+ padding: 8px 14px !important;
77
+ font-size: 14px !important;
78
  }
79
+ .gr-button-primary:hover{ background:var(--primary-600)!important; }
80
  a{ color:var(--primary-700)!important; }
81
 
82
+ /* tighten overall vertical spacing a bit */
83
+ .gradio-container .prose{ margin: 0 !important; }
84
+ #header_md h2, #header_md p { margin: 0.2rem 0 !important; }
85
+
86
+ /* language dropdown: small, no big box */
87
+ #lang_dd { max-width: 160px; }
88
+ #lang_dd .wrap { padding-top: 0 !important; }
89
+
90
+ /* results table */
91
+ table.bragd {
92
+ width: 100%;
93
+ border-collapse: separate;
94
+ border-spacing: 0;
95
+ border: 1px solid rgba(0,0,0,0.08);
96
+ border-radius: 12px;
97
+ overflow: hidden;
98
+ }
99
+ table.bragd thead th{
100
+ text-align: left;
101
+ font-weight: 600;
102
+ background: rgba(137,175,169,0.20);
103
+ padding: 10px 12px;
104
+ border-bottom: 1px solid rgba(0,0,0,0.08);
105
+ font-size: 13px;
106
+ }
107
+ table.bragd tbody td{
108
+ padding: 10px 12px;
109
+ border-bottom: 1px solid rgba(0,0,0,0.06);
110
+ vertical-align: top;
111
+ font-size: 14px;
112
+ }
113
+ table.bragd tbody tr:last-child td{ border-bottom: none; }
114
+ td.wordcol, td.tagcol { white-space: nowrap; }
115
+ td.tagcol { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
116
+ td.analysiscol { white-space: normal; }
117
+
118
+ /* Make Orð/Word column fit content */
119
+ td.wordcol { width: 1%; }
120
+ td.tagcol { min-width: 8ch; width: 1%; }
121
+
122
+ /* Expanded tags table a touch smaller */
123
+ table.bragd.small tbody td, table.bragd.small thead th { font-size: 13px; }
124
+
125
+ /* header row for results + language picker */
126
+ #results_header .prose h3 { margin: 0.2rem 0 !important; }
127
  """
128
 
129
+ # ----------------------------
130
+ # Utilities
131
+ # ----------------------------
132
+ def simp_tok(sentence: str):
133
+ return re.findall(r"\w+|[" + re.escape(string.punctuation) + "]", sentence)
134
+
135
+ def load_tag_mappings(tags_filepath: str):
136
+ import pandas as pd # local import keeps cold-start slightly lighter
137
+ tags_df = pd.read_csv(tags_filepath)
138
+
139
+ feature_cols = list(tags_df.columns[1:])
140
+ tag_to_features = {row["Original Tag"]: row[1:].values.astype(int) for _, row in tags_df.iterrows()}
141
+ features_to_tag = {tuple(row[1:].values.astype(int)): row["Original Tag"] for _, row in tags_df.iterrows()}
142
 
 
 
 
 
 
143
  return tag_to_features, features_to_tag, len(feature_cols), feature_cols
144
 
145
  def group_from_col(col: str):
146
+ if col == "Article":
147
+ return ("article", "A")
148
+ if col.startswith("No-Article "):
149
+ return ("article", col.split()[-1])
150
+ if col == "Proper Noun":
151
+ return ("proper", "P")
152
+ if col.startswith("Not-Proper-Noun "):
153
+ return ("proper", col.split()[-1])
154
 
155
  prefixes = [
156
+ ("Word Class ", "word_class"),
157
+ ("Subcategory ", "subcategory"), ("No-Subcategory ", "subcategory"),
158
+ ("Gender ", "gender"), ("No-Gender ", "gender"),
159
+ ("Number ", "number"), ("No-Number ", "number"),
160
+ ("Case ", "case"), ("No-Case ", "case"),
161
+ ("Degree ", "degree"), ("No-Degree ", "degree"),
162
+ ("Declension ", "declension"), ("No-Declension ", "declension"),
163
+ ("Mood ", "mood"),
164
+ ("Voice ", "voice"), ("No-Voice ", "voice"),
165
+ ("Tense ", "tense"), ("No-Tense ", "tense"),
166
+ ("Person ", "person"), ("No-Person ", "person"),
167
+ ("Definite ", "definiteness"), ("Indefinite ", "definiteness"),
168
  ]
169
+ for p, g in prefixes:
170
  if col.startswith(p):
171
  return (g, col.split()[-1])
172
+
173
+ return (None, None)
174
 
175
  def process_tag_features(tag_to_features: dict, intervals):
176
+ # Compute allowed intervals per POS (like demo.py)
177
+ list_of_tags = list(tag_to_features.values())
178
+ unique_arrays = [np.array(tpl) for tpl in set(tuple(arr) for arr in list_of_tags)]
179
+
180
+ word_type_masks = {wt: [arr for arr in unique_arrays if arr[wt] == 1] for wt in range(15)}
181
+ dict_intervals = {}
182
+
183
+ for wt in range(15):
184
+ labels = word_type_masks[wt]
185
  if not labels:
186
+ dict_intervals[wt] = []
187
  continue
188
  sum_labels = np.sum(np.array(labels), axis=0)
189
+ allowed = [interval for interval in intervals if np.sum(sum_labels[interval[0]:interval[1] + 1]) != 0]
190
+ dict_intervals[wt] = allowed
191
+
192
+ return dict_intervals
193
 
194
+ def predict_vectors(logits: torch.Tensor, attention_mask: torch.Tensor, begin_tokens, dict_intervals, vec_len: int):
195
  softmax = torch.nn.Softmax(dim=0)
196
  vectors = []
197
+
198
  for idx in range(len(logits)):
199
+ if attention_mask[idx].item() != 1:
200
+ continue
201
+ if begin_tokens[idx] != 1:
202
  continue
203
 
204
+ pred_logits = logits[idx]
205
  vec = torch.zeros(vec_len, device=logits.device)
206
 
207
+ # POS
208
+ probs = softmax(pred_logits[0:15])
209
+ wt = torch.argmax(probs).item()
210
+ vec[wt] = 1
211
 
212
+ # feature groups
213
+ for (a, b) in dict_intervals.get(wt, []):
214
+ seg = pred_logits[a:b + 1]
215
+ probs = softmax(seg)
216
+ k = torch.argmax(probs).item()
217
+ vec[a + k] = 1
218
 
219
  vectors.append(vec)
220
+
221
  return vectors
222
 
223
+ def clean_label(s: str) -> str:
224
+ s = (s or "").strip()
225
+ s = re.sub(r"\s+", " ", s)
226
+ return s.strip(" -;:,")
227
+
228
+ def html_escape(s: str) -> str:
229
+ return (
230
+ (s or "")
231
+ .replace("&", "&amp;")
232
+ .replace("<", "&lt;")
233
+ .replace(">", "&gt;")
234
+ .replace('"', "&quot;")
235
+ )
236
+
237
+ def rows_to_table_html(headers, rows, small=False):
238
+ cls = "bragd small" if small else "bragd"
239
+ thead = "".join(f"<th>{html_escape(h)}</th>" for h in headers)
240
+ body = []
241
+ for r in rows:
242
+ body.append(
243
+ "<tr>"
244
+ f"<td class='wordcol'>{html_escape(r[0])}</td>"
245
+ f"<td class='tagcol'>{html_escape(r[1])}</td>"
246
+ f"<td class='analysiscol'>{html_escape(r[2])}</td>"
247
+ "</tr>"
248
+ )
249
+ tbody = "".join(body) if body else "<tr><td class='wordcol'></td><td class='tagcol'></td><td class='analysiscol'></td></tr>"
250
+ return f"<table class='{cls}'><thead><tr>{thead}</tr></thead><tbody>{tbody}</tbody></table>"
251
+
252
  # ----------------------------
253
+ # Load labels (FO+EN)
254
  # ----------------------------
255
  with open(LABELS_FILEPATH, "r", encoding="utf-8") as f:
256
  LABELS = json.load(f)
257
 
258
+ def label_for(lang: str, group: str, wc_code: str, code: str) -> str:
259
+ lang = "fo" if lang == "fo" else "en"
260
  by_wc = LABELS.get(lang, {}).get("by_word_class", {})
261
  glob = LABELS.get(lang, {}).get("global", {})
262
+
263
+ if wc_code and wc_code in by_wc and code in by_wc[wc_code].get(group, {}):
264
+ return by_wc[wc_code][group][code]
265
  return glob.get(group, {}).get(code, "")
266
 
267
  # ----------------------------
268
+ # Load mapping CSV + model
269
  # ----------------------------
270
  tag_to_features, features_to_tag, VEC_LEN, FEATURE_COLS = load_tag_mappings(TAGS_FILEPATH)
271
 
 
 
 
272
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN)
273
  model = AutoModelForTokenClassification.from_pretrained(MODEL_ID, token=HF_TOKEN)
274
+
275
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
276
+ model.to(device)
277
+ model.eval()
278
 
279
  if hasattr(model, "config") and hasattr(model.config, "num_labels"):
280
  if model.config.num_labels != VEC_LEN:
281
+ raise RuntimeError(
282
+ f"Label size mismatch: model has num_labels={model.config.num_labels}, "
283
+ f"but {TAGS_FILEPATH} implies {VEC_LEN}. You likely uploaded the wrong CSV."
284
+ )
285
 
286
  DICT_INTERVALS = process_tag_features(tag_to_features, INTERVALS)
287
 
288
+ # Build group lookup from CSV feature columns
289
+ GROUPS = defaultdict(list) # group -> list[(idx, code, colname)]
290
+ for i, col in enumerate(FEATURE_COLS):
291
+ g, code = group_from_col(col)
292
  if g and code not in HIDE_CODES.get(g, set()):
293
  GROUPS[g].append((i, code, col))
294
 
 
296
  return features_to_tag.get(tuple(vec.int().tolist()), "Unknown Tag")
297
 
298
  def wc_code(vec: torch.Tensor) -> str:
299
+ for idx, code, _ in GROUPS["word_class"]:
300
+ if int(vec[idx].item()) == 1:
301
  return code
302
  return ""
303
 
304
  def group_code(vec: torch.Tensor, group: str) -> str:
305
  hidden = HIDE_CODES.get(group, set())
306
+ for idx, code, _ in GROUPS.get(group, []):
307
  if code in hidden:
308
  continue
309
+ if int(vec[idx].item()) == 1:
310
  return code
311
  return ""
312
 
313
+ # ----------------------------
314
+ # Presentation logic
315
+ # ----------------------------
316
+ HIDE_IN_ANALYSIS_FO = {"stýrir falli", "stýrir ikki falli"}
317
+ HIDE_IN_ANALYSIS_EN = {"governs case", "does not govern case"}
 
318
 
319
+ def analysis_text(vec: torch.Tensor, lang: str) -> str:
320
  """
321
  Útgreining / Analysis:
322
+ - only human text (no codes)
323
+ - skip "stýrir falli" / "stýrir ikki falli"
324
+ - DGd becomes ONLY "fyriseting"/"preposition"
325
+ - pronouns and conjunctions start from subcategory (no duplicated base label)
326
  """
327
+ lang = "fo" if lang == "fo" else "en"
328
  raw_tag = vector_to_tag(vec)
329
  wc = wc_code(vec)
330
 
331
+ # DGd override: ONLY fyriseting / preposition
332
  if raw_tag == "DGd":
333
  return "fyriseting" if lang == "fo" else "preposition"
334
 
335
+ # Determine whether to include base word-class label first
336
+ include_wc = True
337
+ if wc == "P": # pronouns: start from subcategory label
338
+ include_wc = False
339
+ if wc == "C": # conjunctions: prefer the subcategory phrase
340
+ include_wc = False
341
 
342
  labels = []
343
 
344
+ if include_wc:
345
+ wc_lbl = clean_label(label_for(lang, "word_class", wc, wc) or wc)
346
  if wc_lbl:
347
  labels.append(wc_lbl)
348
 
349
+ # Add groups in stable order
350
  for g in GROUP_ORDER:
351
  c = group_code(vec, g)
352
  if not c:
353
  continue
354
+ lbl = clean_label(label_for(lang, g, wc, c) or label_for(lang, g, "", c) or "")
 
 
 
 
 
 
355
  if not lbl:
356
  continue
357
 
358
+ if lang == "fo" and lbl in HIDE_IN_ANALYSIS_FO:
359
+ continue
360
+ if lang == "en" and lbl.lower() in HIDE_IN_ANALYSIS_EN:
361
  continue
362
+
363
+ # for conjunctions: ensure the first visible label is the subcategory phrase
364
+ if wc == "C" and g == "subcategory":
365
+ labels.insert(0, lbl)
366
  continue
367
 
368
+ labels.append(lbl)
 
369
 
370
+ # Fallback if we removed wc label for pronouns/conjunctions and subcategory missing
371
+ if not labels:
372
+ wc_lbl = clean_label(label_for(lang, "word_class", wc, wc) or wc)
373
+ if wc_lbl:
374
+ labels = [wc_lbl]
375
 
376
+ # Deduplicate while preserving order
377
+ dedup = []
378
+ seen = set()
379
+ for x in labels:
380
+ if x not in seen:
381
+ dedup.append(x)
382
+ seen.add(x)
383
 
384
+ return ", ".join(dedup)
385
 
386
+ def expanded_text(vec: torch.Tensor, lang: str) -> str:
387
  """
388
+ Útgreinað marking / Expanded tags:
389
+ includes code + label per group (useful for debugging).
390
  """
391
+ lang = "fo" if lang == "fo" else "en"
392
  wc = wc_code(vec)
393
  parts = []
394
 
 
404
 
405
  return "; ".join([p for p in parts if p])
406
 
407
+ def build_legend(lang: str) -> str:
408
+ """
409
+ Elaborate legend:
410
+ Under each word class, show all letter codes that appear in the CURRENT CSV.
411
+ """
412
+ lang = "fo" if lang == "fo" else "en"
413
+
414
+ # Build codes-by-wc from the CSV mapping vectors
415
  codes = defaultdict(lambda: defaultdict(set)) # wc -> group -> set(code)
416
  for arr in tag_to_features.values():
417
  arr = np.array(arr)
418
 
419
  wc = None
420
+ for idx, code, _ in GROUPS["word_class"]:
421
+ if arr[idx] == 1:
422
  wc = code
423
  break
424
  if not wc:
425
  continue
426
 
427
  for g in GROUP_ORDER:
428
+ for idx, code, _ in GROUPS.get(g, []):
429
+ if code in HIDE_CODES.get(g, set()):
 
430
  continue
431
+ if arr[idx] == 1:
432
  codes[wc][g].add(code)
433
 
434
+ title = f"### {UI[lang]['legend']}"
 
 
 
 
 
 
 
 
 
 
 
435
  lines = [title, ""]
436
 
437
+ group_names = {
438
+ "fo": {
439
+ "subcategory": "Undirflokkur",
440
+ "gender": "Kyn",
441
+ "number": "Tal",
442
+ "case": "Fall",
443
+ "article": "Bundni/óbundni",
444
+ "proper": "Sernavn",
445
+ "degree": "Stig",
446
+ "declension": "Bending",
447
+ "mood": "Háttur",
448
+ "voice": "Søgn",
449
+ "tense": "Tíð",
450
+ "person": "Persónur",
451
+ "definiteness": "Bundni/óbundni",
452
+ },
453
+ "en": {
454
+ "subcategory": "Subcategory",
455
+ "gender": "Gender",
456
+ "number": "Number",
457
+ "case": "Case",
458
+ "article": "Definiteness (suffix)",
459
+ "proper": "Proper noun",
460
+ "degree": "Degree",
461
+ "declension": "Declension",
462
+ "mood": "Mood",
463
+ "voice": "Voice",
464
+ "tense": "Tense",
465
+ "person": "Person",
466
+ "definiteness": "Definiteness",
467
+ },
468
+ }[lang]
469
+
470
+ for wc in sorted(codes.keys()):
471
  wcl = label_for(lang, "word_class", wc, wc) or ""
472
  lines.append(f"#### {wc} — {wcl}" if wcl else f"#### {wc}")
473
 
474
  for g in GROUP_ORDER:
475
+ cs = sorted(codes[wc].get(g, set()))
476
  if not cs:
477
  continue
478
 
479
+ lines.append(f"**{group_names.get(g, g)}**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  for c in cs:
481
  lbl = label_for(lang, g, wc, c) or label_for(lang, g, "", c)
482
  lines.append(f"- `{c}` — {lbl}" if lbl else f"- `{c}`")
 
486
 
487
  return "\n".join(lines).strip()
488
 
489
+ # ----------------------------
490
+ # Model run + state
491
+ # ----------------------------
492
  def run_model(sentence: str):
493
  s = (sentence or "").strip()
494
  if not s:
495
  return []
496
+
497
  tokens = simp_tok(s)
498
  if not tokens:
499
  return []
 
513
  attention_mask = enc["attention_mask"].to(device)
514
  word_ids = enc.word_ids(batch_index=0)
515
 
516
+ # begin token mask: first subtoken per word
517
+ begin_tokens = []
518
  last = None
519
  for wid in word_ids:
520
  if wid is None:
521
+ begin_tokens.append(0)
522
  elif wid != last:
523
+ begin_tokens.append(1)
524
  else:
525
+ begin_tokens.append(0)
526
  last = wid
527
 
528
  with torch.no_grad():
529
+ out = model(input_ids=input_ids, attention_mask=attention_mask)
530
+ logits = out.logits[0]
531
 
532
+ vectors = predict_vectors(logits, attention_mask[0], begin_tokens, DICT_INTERVALS, VEC_LEN)
533
 
534
  rows = []
535
  vec_i = 0
536
+ seen_word_ids = set()
537
+
538
+ for i, wid in enumerate(word_ids):
539
+ if wid is None:
540
+ continue
541
+ if begin_tokens[i] != 1:
542
+ continue
543
+ if wid in seen_word_ids:
544
  continue
545
+
546
+ seen_word_ids.add(wid)
547
  word = tokens[wid] if wid < len(tokens) else "<UNK>"
548
  vec = vectors[vec_i] if vec_i < len(vectors) else torch.zeros(VEC_LEN, device=device)
549
  rows.append({"word": word, "vec": vec.int().tolist()})
550
  vec_i += 1
551
+
552
  return rows
553
 
554
+ def render(rows_state, lang_choice: str):
555
+ lang = "fo" if lang_choice == "fo" else "en"
556
+
557
+ headers_main = [f"{UI[lang]['word']}", f"{UI[lang]['tag']}", f"{UI[lang]['analysis']}"]
558
+ headers_exp = [f"{UI[lang]['word']}", f"{UI[lang]['tag']}", f"{UI[lang]['expanded']}"]
559
+
560
+ main_rows = []
561
+ exp_rows = []
562
 
563
+ for r in (rows_state or []):
 
 
564
  vec = torch.tensor(r["vec"])
565
  tag = vector_to_tag(vec)
566
+ main_rows.append([r["word"], tag, analysis_text(vec, lang)])
567
+ exp_rows.append([r["word"], tag, expanded_text(vec, lang)])
568
 
569
+ main_html = rows_to_table_html(headers_main, main_rows, small=False)
570
+ exp_html = rows_to_table_html(headers_exp, exp_rows, small=True)
571
+ legend_md = build_legend(lang)
572
+
573
+ return main_html, exp_html, legend_md
574
 
575
  # ----------------------------
576
+ # Gradio UI (compact + user-friendly)
577
  # ----------------------------
578
  theme = gr.themes.Soft()
579
 
580
  with gr.Blocks(theme=theme, css=CSS, title="BRAGD-markarin") as demo:
581
+ with gr.Row(equal_height=True):
582
+ with gr.Column(scale=2, min_width=240):
583
+ gr.Markdown(
584
+ f"## {UI['fo']['title']}\n"
585
+ f"{UI['fo']['inst']}\n\n"
586
+ f"**{UI['fo']['model']}** `{MODEL_ID}`",
587
+ elem_id="header_md"
588
+ )
589
+ with gr.Column(scale=5, min_width=420):
590
+ inp = gr.Textbox(lines=5, label=None, placeholder="Skriv her… / Type here…")
591
+ btn = gr.Button("Marka / Tag", variant="primary")
592
+
593
+ # Results header row with language picker on the far right
594
+ with gr.Row(equal_height=True, elem_id="results_header"):
595
+ with gr.Column(scale=5):
596
+ res_title = gr.Markdown(f"### {UI['fo']['results']} / {UI['en']['results']}")
597
+ with gr.Column(scale=1, min_width=170):
598
+ lang = gr.Dropdown(
599
+ choices=[("Føroyskt", "fo"), ("English", "en")],
600
+ value="fo",
601
+ label=None,
602
+ interactive=True,
603
+ filterable=False,
604
+ container=False,
605
+ elem_id="lang_dd",
606
+ )
607
 
608
  state = gr.State([])
609
 
610
+ out_main = gr.HTML()
 
 
 
 
611
  with gr.Accordion("Útgreinað marking / Expanded tags", open=False):
612
+ out_expanded = gr.HTML()
613
 
614
+ with gr.Accordion("Markingaryvirlit / Tag legend", open=False):
615
+ out_legend = gr.Markdown(build_legend("fo"))
616
 
617
  def on_tag(sentence, lang_choice):
618
  rows = run_model(sentence)
619
+ main_html, exp_html, legend_md = render(rows, lang_choice)
620
+ return rows, main_html, exp_html, legend_md
621
 
622
  def on_lang(rows, lang_choice):
623
+ main_html, exp_html, legend_md = render(rows, lang_choice)
624
+ return main_html, exp_html, legend_md
625
 
626
+ btn.click(on_tag, inputs=[inp, lang], outputs=[state, out_main, out_expanded, out_legend])
627
+ lang.change(on_lang, inputs=[state, lang], outputs=[out_main, out_expanded, out_legend])
628
 
629
  if __name__ == "__main__":
630
  demo.launch()
tag_labels.json CHANGED
@@ -7,8 +7,8 @@
7
  "A": "adjective",
8
  "P": "pronoun",
9
  "N": "numeral",
10
- "V": "verb (except for participle)",
11
- "L": "participle",
12
  "D": "adverb",
13
  "C": "conjunction",
14
  "F": "Foreign word",
@@ -82,7 +82,7 @@
82
  "G": "genitive"
83
  },
84
  "article": {
85
- "A": "with suffixed definite article"
86
  },
87
  "proper": {
88
  "P": "Proper Noun"
@@ -123,9 +123,9 @@
123
  "A": "absolute superlative"
124
  },
125
  "declension": {
126
- "S": "strong",
127
- "W": "weak",
128
- "e": "no-declension"
129
  },
130
  "gender": {
131
  "M": "masculine",
@@ -204,7 +204,7 @@
204
  },
205
  "V": {
206
  "word_class": {
207
- "V": "verb (except for participle)"
208
  },
209
  "mood": {
210
  "I": "infinitive",
@@ -233,16 +233,16 @@
233
  },
234
  "L": {
235
  "word_class": {
236
- "L": "participle"
237
  },
238
  "voice": {
239
  "A": "active",
240
  "M": "mediopassive"
241
  },
242
  "declension": {
243
- "S": "strong",
244
- "W": "weak",
245
- "e": "no-declension"
246
  },
247
  "gender": {
248
  "M": "masculine",
@@ -315,7 +315,7 @@
315
  "K": "punctuation"
316
  },
317
  "subcategory": {
318
- "E": "End of sentence",
319
  "C": "comma",
320
  "Q": "quotes",
321
  "O": "other"
@@ -452,8 +452,8 @@
452
  "A": "absolutt hástig"
453
  },
454
  "declension": {
455
- "S": "sterk",
456
- "W": "veik",
457
  "e": "eingin sterk/veik bending"
458
  },
459
  "gender": {
@@ -480,7 +480,7 @@
480
  "D": "ávísingarfornavn",
481
  "E": "ognarfornavn",
482
  "I": "óbundið fornavn",
483
- "P": "perónsfornavn",
484
  "Q": "spurnarfornavn",
485
  "X": "afturbent fornavn"
486
  },
@@ -490,9 +490,9 @@
490
  "N": "hvørkikyn"
491
  },
492
  "person": {
493
- "1": "fyrsti persónur",
494
- "2": "annar persónur",
495
- "3": "triði persónur"
496
  },
497
  "number": {
498
  "S": "eintal",
@@ -555,9 +555,9 @@
555
  "P": "fleirtal"
556
  },
557
  "person": {
558
- "1": "fyrsti persónur",
559
- "2": "annar persónur",
560
- "3": "triði persónur"
561
  }
562
  },
563
  "L": {
@@ -573,8 +573,8 @@
573
  "M": "miðalsøgn"
574
  },
575
  "declension": {
576
- "S": "sterk",
577
- "W": "veik",
578
  "e": "eingin sterk/veik bending"
579
  },
580
  "gender": {
@@ -613,8 +613,8 @@
613
  "C": "sambindingarorð"
614
  },
615
  "subcategory": {
616
- "C": "javnskipandi",
617
- "S": "innskipandi",
618
  "I": "navnháttarmerki (bara \"at\")",
619
  "R": "afturbeint fornavn"
620
  }
 
7
  "A": "adjective",
8
  "P": "pronoun",
9
  "N": "numeral",
10
+ "V": "verb",
11
+ "L": "past participle",
12
  "D": "adverb",
13
  "C": "conjunction",
14
  "F": "Foreign word",
 
82
  "G": "genitive"
83
  },
84
  "article": {
85
+ "A": "definite"
86
  },
87
  "proper": {
88
  "P": "Proper Noun"
 
123
  "A": "absolute superlative"
124
  },
125
  "declension": {
126
+ "S": "strong declension",
127
+ "W": "weak declension",
128
+ "e": "no declension"
129
  },
130
  "gender": {
131
  "M": "masculine",
 
204
  },
205
  "V": {
206
  "word_class": {
207
+ "V": "verb"
208
  },
209
  "mood": {
210
  "I": "infinitive",
 
233
  },
234
  "L": {
235
  "word_class": {
236
+ "L": "past participle"
237
  },
238
  "voice": {
239
  "A": "active",
240
  "M": "mediopassive"
241
  },
242
  "declension": {
243
+ "S": "strong declension",
244
+ "W": "weak declension",
245
+ "e": "no declension"
246
  },
247
  "gender": {
248
  "M": "masculine",
 
315
  "K": "punctuation"
316
  },
317
  "subcategory": {
318
+ "E": "end of sentence",
319
  "C": "comma",
320
  "Q": "quotes",
321
  "O": "other"
 
452
  "A": "absolutt hástig"
453
  },
454
  "declension": {
455
+ "S": "sterk bending",
456
+ "W": "veik bending",
457
  "e": "eingin sterk/veik bending"
458
  },
459
  "gender": {
 
480
  "D": "ávísingarfornavn",
481
  "E": "ognarfornavn",
482
  "I": "óbundið fornavn",
483
+ "P": "persónsfornavn",
484
  "Q": "spurnarfornavn",
485
  "X": "afturbent fornavn"
486
  },
 
490
  "N": "hvørkikyn"
491
  },
492
  "person": {
493
+ "1": "1. persónur",
494
+ "2": "2. persónur",
495
+ "3": "3. persónur"
496
  },
497
  "number": {
498
  "S": "eintal",
 
555
  "P": "fleirtal"
556
  },
557
  "person": {
558
+ "1": "1. persónur",
559
+ "2": "2. persónur",
560
+ "3": "3. persónur"
561
  }
562
  },
563
  "L": {
 
573
  "M": "miðalsøgn"
574
  },
575
  "declension": {
576
+ "S": "sterk bending",
577
+ "W": "veik bending",
578
  "e": "eingin sterk/veik bending"
579
  },
580
  "gender": {
 
613
  "C": "sambindingarorð"
614
  },
615
  "subcategory": {
616
+ "C": "javnskipandi sambindingarorð",
617
+ "S": "innskipandi sambindingarorð",
618
  "I": "navnháttarmerki (bara \"at\")",
619
  "R": "afturbeint fornavn"
620
  }