Spaces:
Running
Running
| import streamlit as st | |
| from typing import Dict, Any | |
| from latincy_preprocess.uv import UVNormalizerRules, NormalizationResult | |
| st.set_page_config(page_title="U/V Normalizer Demo", layout="wide") | |
| st.sidebar.header("U/V Normalizer Demo") | |
| # Sample text (Seneca, Epistulae Morales 1) | |
| SAMPLE_TEXT = """Ita fac, mi Lucili: vindica te tibi, et tempus quod adhuc aut auferebatur aut subripiebatur aut excidebat collige et serva. Persuade tibi hoc sic esse ut scribo: quaedam tempora eripiuntur nobis, quaedam subducuntur, quaedam effluunt.""" | |
| SAMPLE_TEXT_UONLY = SAMPLE_TEXT.replace("v", "u").replace("V", "U") | |
| # HTML styling | |
| HTML_GREEN = '<span style="color: #28a745; font-weight: bold">' | |
| HTML_RED = '<span style="color: #dc3545; font-weight: bold">' | |
| HTML_GREY = '<span style="color: #6c757d">' | |
| HTML_END = "</span>" | |
| def get_normalizer() -> UVNormalizerRules: | |
| """Get cached normalizer instance.""" | |
| return UVNormalizerRules() | |
| def to_uonly(text: str) -> str: | |
| """Convert text to u-only spelling (all v -> u).""" | |
| return text.replace("v", "u").replace("V", "U") | |
| def colorize_changes(original: str, normalized: str, reference: str = None) -> str: | |
| """Create HTML with color-coded changes.""" | |
| result = [HTML_GREY] | |
| for i, (orig, norm) in enumerate(zip(original, normalized)): | |
| if orig != norm: | |
| if reference and i < len(reference): | |
| if norm == reference[i]: | |
| result.append(f"{HTML_END}{HTML_GREEN}{norm}{HTML_END}{HTML_GREY}") | |
| else: | |
| result.append(f"{HTML_END}{HTML_RED}{norm}{HTML_END}{HTML_GREY}") | |
| else: | |
| result.append(f"{HTML_END}{HTML_GREEN}{norm}{HTML_END}{HTML_GREY}") | |
| else: | |
| result.append(orig) | |
| result.append(HTML_END) | |
| return "".join(result) | |
| def calculate_metrics(source: str, normalized: str, reference: str) -> Dict[str, Any]: | |
| """Calculate accuracy metrics.""" | |
| min_len = min(len(source), len(normalized), len(reference)) | |
| total_uv = 0 | |
| correct = 0 | |
| true_positives = 0 | |
| false_positives = 0 | |
| false_negatives = 0 | |
| changes_needed = 0 | |
| changes_made = 0 | |
| for i in range(min_len): | |
| src, norm, ref = source[i], normalized[i], reference[i] | |
| if src.lower() in ("u", "v"): | |
| total_uv += 1 | |
| needed = src != ref | |
| made = src != norm | |
| if needed: | |
| changes_needed += 1 | |
| if made: | |
| changes_made += 1 | |
| if norm == ref: | |
| correct += 1 | |
| if needed and made: | |
| true_positives += 1 | |
| else: | |
| if not needed and made: | |
| false_positives += 1 | |
| elif needed and not made: | |
| false_negatives += 1 | |
| accuracy = correct / total_uv if total_uv > 0 else 1.0 | |
| precision = true_positives / changes_made if changes_made > 0 else 1.0 | |
| recall = true_positives / changes_needed if changes_needed > 0 else 1.0 | |
| f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0 | |
| return { | |
| "total_uv": total_uv, | |
| "correct": correct, | |
| "accuracy": accuracy, | |
| "precision": precision, | |
| "recall": recall, | |
| "f1": f1, | |
| "true_positives": true_positives, | |
| "false_positives": false_positives, | |
| "false_negatives": false_negatives, | |
| "changes_needed": changes_needed, | |
| "changes_made": changes_made, | |
| } | |
| def show_rule_details(result: NormalizationResult): | |
| """Show detailed rule application information.""" | |
| if not result.changes: | |
| st.info("No changes made - text already normalized") | |
| return | |
| st.subheader(f"Rule Applications ({len(result.changes)} changes)") | |
| by_rule: Dict[str, list] = {} | |
| for change in result.changes: | |
| if change.rule not in by_rule: | |
| by_rule[change.rule] = [] | |
| by_rule[change.rule].append(change) | |
| for rule, changes in sorted(by_rule.items()): | |
| with st.expander(f"**{rule}** ({len(changes)} changes)"): | |
| for change in changes[:10]: | |
| st.markdown( | |
| f"- `{change.context}` -> {change.original} -> **{change.normalized}**" | |
| ) | |
| if len(changes) > 10: | |
| st.markdown(f"*...and {len(changes) - 10} more*") | |
| normalizer = get_normalizer() | |
| st.title("Latin U/V Normalizer") | |
| st.markdown( | |
| "Rule-based U/V normalization for Latin using " | |
| "[latincy-preprocess](https://pypi.org/project/latincy-preprocess/). " | |
| "Converts consonantal 'u' to 'v' and vocalic 'v' to 'u'." | |
| ) | |
| tab1, tab2, tab3 = st.tabs(["Normalize", "Evaluate", "About"]) | |
| # === NORMALIZE TAB === | |
| with tab1: | |
| st.markdown( | |
| "Enter Latin text with u-only spelling and normalize it " | |
| "to proper u/v distinction." | |
| ) | |
| col1, col2 = st.columns([1, 1]) | |
| with col1: | |
| text = st.text_area( | |
| "Input text (u-only spelling):", | |
| value=SAMPLE_TEXT_UONLY, | |
| height=200, | |
| help="Enter Latin text with 'u' for both vowels and consonants", | |
| ) | |
| show_details = st.checkbox("Show rule details", value=False) | |
| if st.button("Normalize", type="primary"): | |
| if not text.strip(): | |
| st.warning("Please enter some text") | |
| else: | |
| result = normalizer.normalize_detailed(text) | |
| with col2: | |
| st.markdown("**Normalized text:**") | |
| colored = colorize_changes(text, result.normalized) | |
| st.markdown(colored, unsafe_allow_html=True) | |
| n_changes = len(result.changes) | |
| st.markdown(f"*{n_changes} change{'s' if n_changes != 1 else ''} made*") | |
| if show_details: | |
| show_rule_details(result) | |
| # === EVALUATE TAB === | |
| with tab2: | |
| st.markdown( | |
| "Enter correctly normalized text to evaluate accuracy. " | |
| "The system will convert it to u-only form, normalize it, " | |
| "and compare against your reference." | |
| ) | |
| reference = st.text_area( | |
| "Reference text (correctly normalized):", | |
| value=SAMPLE_TEXT, | |
| height=200, | |
| help="Enter Latin text with correct U/V distinction", | |
| ) | |
| if st.button("Evaluate", type="primary"): | |
| if not reference.strip(): | |
| st.warning("Please enter reference text") | |
| else: | |
| source = to_uonly(reference) | |
| normalized = normalizer.normalize(source) | |
| metrics = calculate_metrics(source, normalized, reference) | |
| st.subheader("Evaluation Results") | |
| colored = colorize_changes(source, normalized, reference) | |
| st.markdown(colored, unsafe_allow_html=True) | |
| st.markdown( | |
| f"**Legend:** {HTML_GREEN}Correct{HTML_END} · " | |
| f"{HTML_RED}Incorrect{HTML_END} · " | |
| f"{HTML_GREY}Unchanged{HTML_END}", | |
| unsafe_allow_html=True, | |
| ) | |
| st.subheader("Metrics") | |
| col1, col2, col3, col4 = st.columns(4) | |
| col1.metric("Accuracy", f"{metrics['accuracy']:.1%}") | |
| col2.metric("Precision", f"{metrics['precision']:.1%}") | |
| col3.metric("Recall", f"{metrics['recall']:.1%}") | |
| col4.metric("F1 Score", f"{metrics['f1']:.1%}") | |
| with st.expander("Detailed Statistics"): | |
| st.markdown(f""" | |
| - **Total U/V characters:** {metrics['total_uv']} | |
| - **Correct:** {metrics['correct']} | |
| - **Changes needed:** {metrics['changes_needed']} | |
| - **Changes made:** {metrics['changes_made']} | |
| - **True positives:** {metrics['true_positives']} | |
| - **False positives:** {metrics['false_positives']} | |
| - **False negatives:** {metrics['false_negatives']} | |
| """) | |
| # === ABOUT TAB === | |
| with tab3: | |
| st.markdown(""" | |
| ## About | |
| This tool normalizes Latin U/V spelling: | |
| - **Consonantal 'u' -> 'v'**: e.g., *ueni* -> *veni*, *uia* -> *via* | |
| - **Vocalic 'v' -> 'u'**: e.g., *lvna* -> *luna* | |
| ### Rule Categories | |
| The normalizer applies rules in priority order: | |
| 1. **QU digraph**: `qu` always stays as `qu` (*quod*, *aqua*) | |
| 2. **NGU/GU digraph**: `ngu`/`gu` before vowel stays `u` (*lingua*, *sanguis*) | |
| 3. **Word exceptions**: Morphological exceptions (*cui*, *sua*, *fuit*) | |
| 4. **Perfect tense**: U-perfect verb forms (*fuit*, *potuit*, *fuisse*) | |
| 5. **Double-u patterns**: Context-dependent (*servus*, *fluvius*, *iuvat*) | |
| 6. **Initial position**: Before vowel -> `v`, before consonant -> `u` | |
| 7. **Intervocalic**: Between vowels -> `v` (*novus*, *brevis*) | |
| 8. **Before consonant**: -> `u` (*scriptum*, *causa*) | |
| 9. **Word-final**: -> `u` (*tu*, *cum*) | |
| 10. **Post-consonant**: Before vowel -> `v` (*silva*, *servo*) | |
| ### Accuracy | |
| - **Curated test set (100 sentences):** 100% | |
| - **UD Latin treebanks (~1800 sentences):** ~98% | |
| ### Source | |
| [PyPI: latincy-preprocess](https://pypi.org/project/latincy-preprocess/) | |
| """) | |