Spaces:
Running
Running
Commit ·
fe2f397
1
Parent(s): ad09e60
Add Greek diacritics restorer demo page
Browse filesNew 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>
- app.py +1 -0
- 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} · "
|
| 184 |
+
f"{HTML_RED}Incorrect{HTML_END} · "
|
| 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 |
+
""")
|