| """Label-projection correctness: alignment round-trips on real corpus lines, and the |
| ambiguity mask checked against the old project's `markable()` (ported verbatim).""" |
| import json |
| import os |
| import unicodedata |
| from pathlib import Path |
|
|
| import numpy as np |
| import pytest |
|
|
| from meter.dataset import (concat_verses, encode_macron_line, encode_plain, |
| encode_scan_line) |
| from meter.marks import (MAC_LONG, MAC_SHORT, SCAN_HEAVY, SCAN_LIGHT, SCAN_VERSE, |
| ambiguous_mask, bracketize, enforce_circumflex_heavy, |
| insert_marks, merge_vowelless_syllables, |
| parse_macron_line, parse_scan_line) |
|
|
| SRC = Path(os.path.expandvars(os.environ.get("MACRONIZER_SRC", |
| "$MACRONIZER_SRC"))) |
|
|
| |
| |
| needs_src = pytest.mark.skipif( |
| not (SRC / "data").is_dir(), |
| reason="MACRONIZER_SRC not set to a checkout of the meter-silver dataset") |
|
|
| |
| DICHRONA = set("αιυ") |
| DIPHTHONGS = {"αι", "αυ", "ει", "ευ", "ηυ", "οι", "ου", "υι", "ωυ"} |
| PERISPOMENI, YPOGEGRAMMENI, DIAERESIS = "͂", "ͅ", "̈" |
|
|
|
|
| def _base(ch): |
| return unicodedata.normalize("NFD", ch)[0].lower() |
|
|
|
|
| def _has(ch, mark): |
| return mark in unicodedata.normalize("NFD", ch) |
|
|
|
|
| def markable_ref(chars, i): |
| """Verbatim port of the macron data tree/scripts/macronize_corpus.py::markable.""" |
| ch = chars[i] |
| b = _base(ch) |
| if b not in DICHRONA: |
| return False |
| if _has(ch, PERISPOMENI) or _has(ch, YPOGEGRAMMENI): |
| return False |
| if i > 0 and not _has(ch, DIAERESIS) and _base(chars[i - 1]) + b in DIPHTHONGS: |
| return False |
| if (i + 1 < len(chars) and not _has(chars[i + 1], DIAERESIS) |
| and b + _base(chars[i + 1]) in DIPHTHONGS): |
| return False |
| return True |
|
|
|
|
| |
|
|
| def test_parse_macron_simple(): |
| plain, labels = parse_macron_line("ὦ παῖ τέλος μὲν Ζεὺς ἔχει βα^ρύκτυ^πος") |
| assert plain == "ὦ παῖ τέλος μὲν Ζεὺς ἔχει βαρύκτυπος" |
| |
| assert labels == {21: MAC_SHORT, 26: MAC_SHORT} |
|
|
|
|
| def test_parse_macron_combining_marks(): |
| plain, labels = parse_macron_line("βᾱρῠ́ς") |
| assert plain == "βαρύς" |
| assert labels == {1: MAC_LONG, 3: MAC_SHORT} |
|
|
|
|
| @needs_src |
| def test_macron_roundtrip_against_plain_column(): |
| """TSV col1 (plain) and col2 (marked) must strip to identical letter streams.""" |
| checked = 0 |
| for name in ("hypotactic", "oga_0", "anthology", "theocritus_doric"): |
| path = SRC / "data" / f"{name}.tsv" |
| with open(path, encoding="utf-8") as f: |
| for _ in range(300): |
| line = f.readline() |
| if not line: |
| break |
| plain_col, marked = line.rstrip("\n").split("\t")[:2] |
| plain, labels = parse_macron_line(marked) |
| r1, r2 = encode_plain(plain), encode_plain(plain_col) |
| if r1 is None or r2 is None: |
| continue |
| assert np.array_equal(r1.chars, r2.chars), (name, marked) |
| if labels: |
| assert max(labels) < len(r1.chars) |
| checked += 1 |
| assert checked > 850 |
|
|
|
|
| def test_insert_marks_roundtrip(): |
| for marked in ("ἥσθην δὲ βαιά^, πά^νυ^ δὲ βαιά^, τέττα^ρα^·", |
| "Δάφνι τά_λαν, τί_ τὺ_ τά_κεαι, ἁ_ δέ τε κώρα", |
| "χρὴ γι^νώσκειν ὅτι^ πά_σης τῆς γῆς ὁ περί^μετρος 0 ."): |
| plain, labels = parse_macron_line(marked) |
| again = insert_marks(plain, labels) |
| assert parse_macron_line(again) == (plain, labels) |
| assert unicodedata.normalize("NFC", again) == unicodedata.normalize("NFC", marked) |
|
|
|
|
| |
|
|
| def _mask_via_planes(text): |
| rec = encode_plain(text) |
| return rec, ambiguous_mask(rec.chars, rec.boundary, rec.dia) |
|
|
|
|
| @needs_src |
| def test_ambiguous_mask_matches_reference(): |
| lines = [] |
| for name in ("hypotactic", "oga_1", "anthology", "drama_ia6"): |
| with open(SRC / "data" / f"{name}.tsv", encoding="utf-8") as f: |
| for _ in range(200): |
| line = f.readline() |
| if not line: |
| break |
| lines.append(line.split("\t")[0]) |
| checked = 0 |
| for text in lines: |
| text = unicodedata.normalize("NFC", text) |
| rec = encode_plain(text) |
| if rec is None: |
| continue |
| |
| chars = list(text) |
| ref = [] |
| for i, ch in enumerate(chars): |
| if _base(ch).lower() in set("αβγδεζηθικλμνξοπρστυφχψω") | {"ς", "ϲ"}: |
| ref.append(markable_ref(chars, i)) |
| if len(ref) != len(rec.chars): |
| continue |
| ours = ambiguous_mask(rec.chars, rec.boundary, rec.dia) |
| assert ref == ours.tolist(), text |
| checked += 1 |
| assert checked > 500 |
|
|
|
|
| |
|
|
| def test_parse_scan_simple(): |
| line = "[ὦ] [παῖ] {τέ}[λος] [μὲν] [Ζεὺ]{ς ἔ}[χει] {βα}[ρύκ]{τυ}[πος]" |
| plain, labels = parse_scan_line(line) |
| assert plain == "ὦ παῖ τέλος μὲν Ζεὺς ἔχει βαρύκτυπος" |
| |
| |
| assert labels[0] == SCAN_HEAVY and labels[3] == SCAN_HEAVY |
| assert labels[5] == SCAN_LIGHT and labels[8] == SCAN_HEAVY |
| assert labels[14] == SCAN_HEAVY and labels[16] == SCAN_LIGHT |
| assert labels[29] == SCAN_VERSE |
| assert max(labels) == 29 |
|
|
|
|
| @needs_src |
| def test_scan_corpus_lines_encode(): |
| ok = 0 |
| with open(SRC / "data/scanner/corpus_v3.tsv", encoding="utf-8") as f: |
| for _ in range(500): |
| line = f.readline() |
| if not line: |
| break |
| work, _meter, bracketed = line.rstrip("\n").split("\t") |
| rec = encode_scan_line(bracketed) |
| if rec is None: |
| continue |
| ends = (rec.y_scan > 0).sum() |
| assert (rec.y_scan == SCAN_VERSE).sum() == 1 |
| assert ends >= 2, bracketed |
| ok += 1 |
| assert ok > 450 |
|
|
|
|
| def test_concat_verses_boundaries(): |
| r1 = encode_scan_line("[ὦ] [παῖ] {τέ}[λος]") |
| r2 = encode_scan_line("{βα}[ρύκ]{τυ}[πος]") |
| joined = concat_verses([r1, r2]) |
| n1 = len(r1) |
| assert joined.boundary[n1 - 1] == 1 |
| assert joined.boundary[-1] == 2 |
| assert (joined.y_scan == SCAN_VERSE).sum() == 2 |
|
|
|
|
| def test_bracketize_roundtrip(): |
| line = "[ὦ] [παῖ] {τέ}[λος] [μὲν] [Ζεὺ]{ς ἔ}[χει] {βα}[ρύκ]{τυ}[πος]" |
| plain, labels = parse_scan_line(line) |
| out = bracketize(plain, {k: v for k, v in labels.items()}) |
| plain2, labels2 = parse_scan_line(out) |
| assert plain2 == plain |
| assert labels2 == labels |
|
|
|
|
| def test_enforce_circumflex_heavy_overrides_light(): |
| |
| |
| |
| rec = encode_plain("πᾶς") |
| labels = np.zeros(len(rec.chars), dtype=np.int64) |
| labels[-1] = SCAN_LIGHT |
| fixed = enforce_circumflex_heavy(rec.dia, labels) |
| assert fixed[-1] == SCAN_HEAVY |
|
|
|
|
| def test_enforce_circumflex_heavy_leaves_non_circumflex_alone(): |
| rec = encode_plain("πολις") |
| labels = np.zeros(len(rec.chars), dtype=np.int64) |
| labels[-1] = SCAN_LIGHT |
| fixed = enforce_circumflex_heavy(rec.dia, labels) |
| assert fixed[-1] == SCAN_LIGHT |
|
|
|
|
| def test_merge_vowelless_syllables(): |
| |
| |
| plain, labels = parse_scan_line("{λε}[ν]") |
| rec = encode_plain(plain) |
| arr = np.zeros(len(rec.chars), dtype=np.int64) |
| for k, v in labels.items(): |
| arr[k] = v |
| fixed = merge_vowelless_syllables(rec.chars, arr) |
| out = bracketize(plain, {i: int(l) for i, l in enumerate(fixed) if l}) |
| assert out == "[λεν]" |
|
|
|
|
| def test_merge_vowelless_syllables_leaves_real_syllables_alone(): |
| plain, labels = parse_scan_line("[ὦ] [παῖ]") |
| rec = encode_plain(plain) |
| arr = np.zeros(len(rec.chars), dtype=np.int64) |
| for k, v in labels.items(): |
| arr[k] = v |
| fixed = merge_vowelless_syllables(rec.chars, arr) |
| assert fixed.tolist() == arr.tolist() |
|
|
|
|
| |
|
|
| @needs_src |
| def test_norma_lines_parse(): |
| n_mac = n_syl = 0 |
| with open(SRC / "data/norma/test.jsonl", encoding="utf-8") as f: |
| for line in f: |
| d = json.loads(line) |
| if d["task"] == "macronize": |
| plain, labels = parse_macron_line(d["text"]) |
| rec = encode_plain(plain) |
| if labels: |
| assert rec is not None and max(labels) < len(rec.chars), d |
| n_mac += 1 |
| else: |
| parsed = parse_scan_line(d["text"]) |
| assert parsed is not None, d |
| plain, labels = parsed |
| rec = encode_plain(plain) |
| assert rec is not None and max(labels) < len(rec.chars), d |
| n_syl += 1 |
| assert n_mac == 932 and n_syl == 932 |
|
|