anonymous-stoicheia's picture
Upload folder using huggingface_hub
7ed86c3 verified
Raw
History Blame Contribute Delete
4.66 kB
"""Shared normalization / tokenization / hashing for the 10-fold split pipeline.
The matching key ("skeleton") is designed so that differences between editions
(diacritics, breathings, iota subscript, sigma forms, editorial brackets,
punctuation, casing, line breaks) are invisible to the matcher.
"""
import unicodedata
import regex as re
import xxhash
# ---------------------------------------------------------------- zones
# zone codes stored per record
ZONE_B0 = 0 # rotating literary buckets B0..B9 -> codes 0..9
ZONE_PTEST = 10 # PHI/TM number ends in 3 (fixed test, all folds)
ZONE_PVAL = 11 # PHI/TM number ends in 4 (fixed val, all folds)
ZONE_TRAIN = 12 # train in every fold (repaired, bronze, digit 0-2,5-9)
N_BUCKETS = 10
NGRAM = 8 # word n-gram size for contamination matching AND
# MinHash shingles (user-approved strictness level:
# any shared normalized 8-gram = contamination)
MIN_SEG_CHARS = 100 # min chars for a stitched train segment
MASK_PTEST = 1 << 10
MASK_PVAL = 1 << 11
def fold_conflict_mask(k):
"""Zones that are val/test in fold k -> bitmask a train sentence must avoid."""
return (1 << k) | (1 << ((k + 1) % N_BUCKETS)) | MASK_PTEST | MASK_PVAL
# ---------------------------------------------------------------- skeleton
_COMBINING = re.compile(r"\p{Mn}+")
# keep only Greek-script LETTERS: two passes (drop non-Greek, then drop
# Greek-script non-letters such as keraia / numeral signs)
_NON_GREEK = re.compile(r"[^\p{Script=Greek}]+")
_NON_LETTER = re.compile(r"[^\p{L} ]+")
_SIGMA = str.maketrans({"ς": "σ", "ϲ": "σ", "Ϲ": "σ", "ϐ": "β", "ϑ": "θ", "ϰ": "κ"})
def skeleton(text):
"""Normalized matching key: diacritic-free lowercase Greek words, space-sep."""
t = unicodedata.normalize("NFD", text)
t = _COMBINING.sub("", t)
t = t.lower().translate(_SIGMA)
t = _NON_GREEK.sub(" ", t)
t = _NON_LETTER.sub(" ", t)
return " ".join(t.split())
# ---------------------------------------------------------------- sentences
# sentence terminators: period, Greek/Latin question marks, exclamation,
# ano teleia U+0387, middle dot U+00B7, semicolon (Greek question mark shares
# the codepoint in many editions), U+037E, and blank lines / colon.
_SENT_BOUNDARY = re.compile("[.;!?:\u00b7\u0387\u037e]+|\\n\\s*\\n")
def sentence_spans(text):
"""Split into sentence spans [(start, end)) covering the whole string.
The terminator and following whitespace belong to the preceding sentence,
so concatenating consecutive spans reproduces the original text exactly.
Spans whose skeleton is empty are merged into nothing (skipped) but their
characters stay attached to the previous span to keep full coverage.
"""
spans = []
prev = 0
for m in _SENT_BOUNDARY.finditer(text):
end = m.end()
spans.append((prev, end))
prev = end
if prev < len(text):
spans.append((prev, len(text)))
# attach empty-skeleton spans to their predecessor (or successor)
out = []
for s, e in spans:
if skeleton(text[s:e]):
out.append([s, e])
elif out:
out[-1][1] = e
else:
out.append([s, e]) # leading junk span; may still have empty skeleton
# drop a leading span with empty skeleton by merging into the next
if len(out) > 1 and not skeleton(text[out[0][0]:out[0][1]]):
out[1][0] = out[0][0]
out.pop(0)
return [(s, e) for s, e in out]
# ---------------------------------------------------------------- grouping
def work_prefix(source, rid):
"""Volume/work granularity used for clustering and sibling-zone lookup.
Deliberately NOT source-qualified: the same archive.org volume appears
as source greek_pd in pristine and source ia in repaired -- the bare
volume/work identifier must match across sources and tiers.
"""
if source == "oga":
return ".".join(rid.split(".")[:2]) # tlgXXXX.tlgYYY work urn
return rid.split("#")[0]
# ---------------------------------------------------------------- hashing
def h64(s):
return xxhash.xxh64_intdigest(s)
def sent_hashes(skel):
"""(exact_hash, bag_hash, word_list) for one sentence skeleton."""
words = skel.split()
exact = h64(skel)
bag = h64(" ".join(sorted(words)))
return exact, bag, words
def ngram_hashes(words, n=NGRAM):
"""Hashes of all word n-grams of a word list (empty if fewer than n words)."""
if len(words) < n:
return []
return [h64(" ".join(words[i:i + n])) for i in range(len(words) - n + 1)]