diyclassics Claude Opus 4.6 commited on
Commit
fe2f397
·
1 Parent(s): ad09e60

Add Greek diacritics restorer demo page

Browse files

New Streamlit page (10_diacritics_demo.py) with three tabs:
- Restore: input undiacriticized Greek, see polytonic output with colorized changes
- Evaluate: compare restored output against reference with mutable/overall/sentence accuracy
- About: CANINE-S architecture, mutable vs. immutable chars, 97.21% accuracy

Uses latincy-diacritics package (installed from local source, not yet on PyPI).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +1 -0
  2. pages/10_diacritics_demo.py +246 -0
app.py CHANGED
@@ -23,5 +23,6 @@ st.markdown(
23
  - [Analyze morphology](morphology_demo) — lemma, case, gender, tense, and more
24
  - [Normalize U/V spelling](uv_normalizer_demo) with rule-based [latincy-uv](https://github.com/diyclassics/latincy-uv)
25
  - [Correct long-s OCR artifacts](long_s_demo) with [latincy-long-s](https://github.com/diyclassics/latincy-long-s)
 
26
  """
27
  )
 
23
  - [Analyze morphology](morphology_demo) — lemma, case, gender, tense, and more
24
  - [Normalize U/V spelling](uv_normalizer_demo) with rule-based [latincy-uv](https://github.com/diyclassics/latincy-uv)
25
  - [Correct long-s OCR artifacts](long_s_demo) with [latincy-long-s](https://github.com/diyclassics/latincy-long-s)
26
+ - [Restore Greek diacritics](diacritics_demo) with [latincy-diacritics](https://github.com/diyclassics/latincy-diacritics)
27
  """
28
  )
pages/10_diacritics_demo.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ from latincy_diacritics import DiacriticRestorer, strip_diacritics, MUTABLE_CHARS, base_char
4
+
5
+ st.set_page_config(page_title="Greek Diacritics Restorer Demo", layout="wide")
6
+ st.sidebar.header("Greek Diacritics Restorer Demo")
7
+
8
+ # Opening of John 1:1, stripped of diacritics
9
+ SAMPLE_TEXT = (
10
+ "εν αρχη ην ο λογος και ο λογος ην προς τον θεον και θεος ην ο λογος"
11
+ )
12
+
13
+ # HTML styling
14
+ HTML_GREEN = '<span style="color: #28a745; font-weight: bold">'
15
+ HTML_RED = '<span style="color: #dc3545; font-weight: bold">'
16
+ HTML_GREY = '<span style="color: #6c757d">'
17
+ HTML_END = "</span>"
18
+
19
+
20
+ @st.cache_resource
21
+ def get_restorer() -> DiacriticRestorer:
22
+ """Get cached restorer instance."""
23
+ return DiacriticRestorer()
24
+
25
+
26
+ def colorize_changes(original: str, restored: str, reference: str = None) -> str:
27
+ """Create HTML with color-coded changes.
28
+
29
+ Without reference: green=changed, grey=unchanged.
30
+ With reference: green=correct, red=incorrect, grey=unchanged.
31
+ """
32
+ parts = []
33
+
34
+ for i, (orig, rest) in enumerate(zip(original, restored)):
35
+ if orig != rest:
36
+ if reference and i < len(reference):
37
+ if rest == reference[i]:
38
+ parts.append(f"{HTML_GREEN}{rest}{HTML_END}")
39
+ else:
40
+ parts.append(f"{HTML_RED}{rest}{HTML_END}")
41
+ else:
42
+ parts.append(f"{HTML_GREEN}{rest}{HTML_END}")
43
+ else:
44
+ parts.append(f"{HTML_GREY}{rest}{HTML_END}")
45
+
46
+ return "".join(parts)
47
+
48
+
49
+ restorer = get_restorer()
50
+
51
+ st.title("Greek Diacritics Restorer")
52
+ st.markdown(
53
+ "Restore polytonic diacritics to undiacriticized Ancient Greek text "
54
+ "using a character-level [CANINE-S](https://huggingface.co/google/canine-s) transformer, "
55
+ "powered by [latincy-diacritics](https://github.com/diyclassics/latincy-diacritics)."
56
+ )
57
+
58
+ tab1, tab2, tab3 = st.tabs(["Restore", "Evaluate", "About"])
59
+
60
+ # === RESTORE TAB ===
61
+ with tab1:
62
+ st.markdown(
63
+ "Enter undiacriticized Ancient Greek text and restore polytonic diacritics."
64
+ )
65
+
66
+ col1, col2 = st.columns([1, 1])
67
+
68
+ with col1:
69
+ text = st.text_area(
70
+ "Input text (without diacritics):",
71
+ value=SAMPLE_TEXT,
72
+ height=200,
73
+ help="Enter Ancient Greek text stripped of diacritics",
74
+ )
75
+
76
+ show_details = st.checkbox("Show character details", value=False)
77
+
78
+ if st.button("Restore", type="primary"):
79
+ if not text.strip():
80
+ st.warning("Please enter some text")
81
+ else:
82
+ result = restorer.restore_detailed(text)
83
+ input_text = result["input"]
84
+ output_text = result["output"]
85
+ predictions = result["predictions"]
86
+
87
+ with col2:
88
+ st.markdown("**Restored text:**")
89
+ colored = colorize_changes(input_text, output_text)
90
+ st.markdown(colored, unsafe_allow_html=True)
91
+
92
+ # Count changes and mutable positions
93
+ changes = []
94
+ mutable_count = 0
95
+ for in_ch, out_ch in predictions:
96
+ if base_char(in_ch) in MUTABLE_CHARS:
97
+ mutable_count += 1
98
+ if in_ch != out_ch:
99
+ changes.append((in_ch, out_ch))
100
+
101
+ n_changes = len(changes)
102
+ st.markdown(
103
+ f"*{n_changes} character{'s' if n_changes != 1 else ''} restored "
104
+ f"out of {mutable_count} mutable position{'s' if mutable_count != 1 else ''}*"
105
+ )
106
+
107
+ if show_details:
108
+ if changes:
109
+ with st.expander("Character details", expanded=True):
110
+ seen = set()
111
+ for in_ch, out_ch in changes:
112
+ key = (in_ch, out_ch)
113
+ if key not in seen:
114
+ seen.add(key)
115
+ st.markdown(f"- **{in_ch}** → **{out_ch}**")
116
+ else:
117
+ st.info("No characters changed")
118
+
119
+ # === EVALUATE TAB ===
120
+ with tab2:
121
+ st.markdown(
122
+ "Enter correctly diacriticized text to evaluate accuracy. "
123
+ "The system strips diacritics, restores them, and compares against your reference."
124
+ )
125
+
126
+ # Reference text: John 1:1 with correct polytonic diacritics
127
+ REFERENCE_SAMPLE = (
128
+ "ἐν ἀρχῇ ἦν ὁ λόγος καὶ ὁ λόγος ἦν πρὸς τὸν θεόν καὶ θεὸς ἦν ὁ λόγος"
129
+ )
130
+
131
+ reference = st.text_area(
132
+ "Reference text (with correct diacritics):",
133
+ value=REFERENCE_SAMPLE,
134
+ height=200,
135
+ help="Enter Ancient Greek text with correct polytonic diacritics",
136
+ )
137
+
138
+ if st.button("Evaluate", type="primary"):
139
+ if not reference.strip():
140
+ st.warning("Please enter reference text")
141
+ else:
142
+ stripped = strip_diacritics(reference)
143
+ restored = restorer.restore(stripped)
144
+
145
+ # Calculate metrics
146
+ total_chars = min(len(stripped), len(restored), len(reference))
147
+ total_mutable = 0
148
+ correct_mutable = 0
149
+ total_correct = 0
150
+
151
+ for i in range(total_chars):
152
+ s, r, ref = stripped[i], restored[i], reference[i]
153
+
154
+ if r == ref:
155
+ total_correct += 1
156
+
157
+ if base_char(s) in MUTABLE_CHARS:
158
+ total_mutable += 1
159
+ if r == ref:
160
+ correct_mutable += 1
161
+
162
+ overall_acc = total_correct / total_chars if total_chars > 0 else 1.0
163
+ mutable_acc = correct_mutable / total_mutable if total_mutable > 0 else 1.0
164
+
165
+ # Sentence-level accuracy
166
+ ref_sentences = [s.strip() for s in reference.split(".") if s.strip()]
167
+ strip_sentences = [s.strip() for s in stripped.split(".") if s.strip()]
168
+ sentences_correct = 0
169
+ total_sentences = len(ref_sentences)
170
+
171
+ for ref_sent, strip_sent in zip(ref_sentences, strip_sentences):
172
+ restored_sent = restorer.restore(strip_sent)
173
+ if restored_sent == ref_sent:
174
+ sentences_correct += 1
175
+
176
+ sentence_acc = sentences_correct / total_sentences if total_sentences > 0 else 1.0
177
+
178
+ st.subheader("Evaluation Results")
179
+ colored = colorize_changes(stripped, restored, reference)
180
+ st.markdown(colored, unsafe_allow_html=True)
181
+
182
+ st.markdown(
183
+ f"**Legend:** {HTML_GREEN}Correct{HTML_END} &middot; "
184
+ f"{HTML_RED}Incorrect{HTML_END} &middot; "
185
+ f"{HTML_GREY}Unchanged{HTML_END}",
186
+ unsafe_allow_html=True,
187
+ )
188
+
189
+ st.subheader("Metrics")
190
+ col1, col2, col3 = st.columns(3)
191
+ col1.metric("Mutable Accuracy", f"{mutable_acc:.1%}")
192
+ col2.metric("Overall Accuracy", f"{overall_acc:.1%}")
193
+ col3.metric("Sentence Accuracy", f"{sentence_acc:.1%}")
194
+
195
+ with st.expander("Detailed Statistics"):
196
+ st.markdown(f"""
197
+ - **Total characters:** {total_chars}
198
+ - **Mutable positions:** {total_mutable}
199
+ - **Mutable correct:** {correct_mutable}
200
+ - **Overall correct:** {total_correct}
201
+ - **Sentences:** {sentences_correct}/{total_sentences} perfect
202
+ """)
203
+
204
+ # === ABOUT TAB ===
205
+ with tab3:
206
+ st.markdown("""
207
+ ## About
208
+
209
+ ### The Diacritics Problem
210
+
211
+ Ancient Greek texts in their polytonic form use a rich system of diacritics:
212
+ **breathing marks** (smooth ψιλή, rough δασεία), **accents** (acute ὀξεία,
213
+ grave βαρεία, circumflex περισπωμένη), **iota subscript** (ὑπογεγραμμένη),
214
+ and **diaeresis** (διαλυτικά). Many digital Greek texts — especially those
215
+ from OCR, plain-text corpora, or keyboard input — lack these diacritics.
216
+
217
+ This tool automatically restores polytonic diacritics using a trained
218
+ character-level transformer.
219
+
220
+ ### CANINE-S Architecture
221
+
222
+ The model uses [CANINE-S](https://huggingface.co/google/canine-s), a
223
+ **character-level** transformer from Google that operates directly on Unicode
224
+ code points — no subword tokenization needed. This makes it ideal for
225
+ diacritics restoration, where the task is inherently character-level.
226
+
227
+ The model predicts, for each **mutable** character position, which polytonic
228
+ variant (from 2–25 options per base character) is correct in context.
229
+
230
+ ### Mutable vs. Immutable Characters
231
+
232
+ Only **vowels and rho** (α, ε, η, ι, ο, υ, ω, ρ) carry diacritics in Greek.
233
+ These are the "mutable" characters. Consonants, punctuation, and spaces pass
234
+ through unchanged — so **mutable accuracy** is the primary metric, since
235
+ overall accuracy is inflated by trivial identity predictions.
236
+
237
+ ### Accuracy
238
+
239
+ - **97.21% mutable accuracy** on held-out test set
240
+ - Trained on aligned pairs from UD Ancient Greek treebanks (Perseus, PROIEL)
241
+ - Replaces a v0 CNN that capped at 96.9% mutable accuracy
242
+
243
+ ### Source
244
+
245
+ [GitHub: latincy-diacritics](https://github.com/diyclassics/latincy-diacritics)
246
+ """)