Fill-Mask
Transformers
Safetensors
Ancient Greek (to 1453)
char_bert
ancient-greek
classical-philology
character-level
masked-diffusion
custom_code
Instructions to use Ericu950/CharDiff-grc-doc_clean with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Ericu950/CharDiff-grc-doc_clean with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="Ericu950/CharDiff-grc-doc_clean", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Ericu950/CharDiff-grc-doc_clean", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 15,303 Bytes
d287da5 ce67eac d287da5 ac228d5 d287da5 ce67eac d287da5 ac228d5 d287da5 ac228d5 d287da5 ac228d5 d287da5 ac228d5 d287da5 ac228d5 d287da5 ce67eac d287da5 ac228d5 d287da5 ac228d5 d287da5 ce67eac d287da5 ce67eac d287da5 ac228d5 d287da5 ac228d5 d287da5 ac228d5 d287da5 ac228d5 d287da5 ce67eac d287da5 ac228d5 d287da5 ac228d5 ce67eac 24f5059 ce67eac 24f5059 ce67eac 24f5059 ce67eac 24f5059 ce67eac 24f5059 ce67eac 24f5059 ce67eac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 | """HF-Hub-compatible processor for CharDiff-grc: text <-> the model's four input planes.
Wraps the reference normalization/denormalization logic (character classification,
diacritic packing, word/sentence-boundary detection) into a single callable that
produces model-ready tensors, plus decode helpers for the three masking use cases
described in the model card (restoration, accent recovery, re-segmentation).
This is intentionally NOT a `PreTrainedTokenizer` subclass: the underlying encoding is
a row-per-letter, four-parallel-plane structure (not a single token-id stream), which
doesn't fit that base class's assumptions. It follows the same `register_for_auto_class`
mechanism transformers uses for tokenizers/feature extractors, so
`AutoProcessor.from_pretrained(repo_id, trust_remote_code=True)` works the same way.
"""
from __future__ import annotations
import json
import re
import unicodedata
from dataclasses import dataclass, field
from pathlib import Path
import numpy as np
import torch
MASK, BLANK, PAD = 24, 25, 26
UNK_BND, UNK_DIA, UNK_PUNCT = 3, 48, 6
ALPHABET = "αβγδεζηθικλμνξοπρστυφχψω"
LETTER_IDS = {c: i for i, c in enumerate(ALPHABET)}
ID2LETTER = np.array(list(ALPHABET))
_EXTRA_BASE = {
"ς": "σ", "ϲ": "σ", "Ϲ": "σ", "ϐ": "β", "ϑ": "θ", "ϕ": "φ", "ϰ": "κ", "ϱ": "ρ", "ϖ": "π",
}
_MARK_MAP = {
0x0301: "acute", 0x0341: "acute", 0x0300: "grave", 0x0340: "grave",
0x0342: "circ", 0x0302: "circ", 0x0313: "smooth", 0x0343: "smooth",
0x0314: "rough", 0x0345: "iota", 0x0308: "diaer",
}
_ACC = {"acute": 1, "grave": 2, "circ": 3}
_BR = {"smooth": 1, "rough": 2}
_MARK_CHARS = {"acute": "́", "grave": "̀", "circ": "͂",
"smooth": "̓", "rough": "̔", "iota": "ͅ", "diaer": "̈"}
# punct plane classes (matches data/normalize.py's canonical 6-way scheme):
# 0 none, 1 comma, 2 high-dot(·), 3 colon, 4 period, 5 question/exclamation
_PUNCT_CHARS = {1: ",", 2: "·", 3: ":", 4: ".", 5: ";"}
# "λόγ[5±3]καὶ" -- a lacuna of uncertain width: best guess 5 letters, plausible
# range 5-3..5+3. See CharBertProcessor.restore_elastic.
_ELASTIC_RE = re.compile(r"\[(\d+)±(\d+)\]")
def _pack_dia(acc, br, iota, diaer):
return ((acc * 3 + br) * 2 + iota) * 2 + diaer
def _unpack_dia(d):
diaer = d % 2; d //= 2
iota = d % 2; d //= 2
br = d % 3; acc = d // 3
return acc, br, iota, diaer
@dataclass
class _Encoded:
chars: list # int, may include MASK
boundary: list
dia: list
punct: list
cap: list # original capitalization, for round-tripping non-masked positions
class CharBertProcessor:
"""`processor(text)` -> dict of batched tensors ready for `CharBertModel(**batch)`."""
def __init__(self):
pass
@classmethod
def from_pretrained(cls, *_args, **_kwargs):
return cls()
def save_pretrained(self, save_directory, **_kwargs):
Path(save_directory).mkdir(parents=True, exist_ok=True)
(Path(save_directory) / "processor_config.json").write_text(json.dumps({"processor_class": "CharBertProcessor"}))
# ---------------------------------------------------------------- encode
def _classify(self, text: str) -> _Encoded:
"""Turn raw NFC/NFD polytonic text into per-letter plane lists, damage ('-' runs)
preserved as MASK/UNK positions, gold values kept for every other position."""
nfd = unicodedata.normalize("NFD", text)
chars, boundary, dia, punct, cap = [], [], [], [], []
acc = br = iota = diaer = 0
pending_bnd = 0
i = 0
while i < len(nfd):
ch = nfd[i]
if ch == "-":
run = 0
while i < len(nfd) and nfd[i] == "-":
run += 1
i += 1
for _ in range(run):
chars.append(MASK); boundary.append(UNK_BND)
dia.append(UNK_DIA); punct.append(UNK_PUNCT); cap.append(0)
continue
low = ch.lower()
base = low if low in LETTER_IDS else _EXTRA_BASE.get(low)
if base is not None:
if chars and pending_bnd:
boundary[-1] = pending_bnd
pending_bnd = 0
chars.append(LETTER_IDS[base])
cap.append(1 if ch != low else 0)
boundary.append(0)
dia.append(0) # filled in by trailing combining marks below
punct.append(0)
acc = br = iota = diaer = 0
elif unicodedata.combining(ch) or ord(ch) in _MARK_MAP:
kind = _MARK_MAP.get(ord(ch))
if kind in _ACC:
acc = _ACC[kind]
elif kind in _BR:
br = _BR[kind]
elif kind == "iota":
iota = 1
elif kind == "diaer":
diaer = 1
if dia:
dia[-1] = _pack_dia(acc, br, iota, diaer)
elif ch.isspace():
pending_bnd = max(pending_bnd, 1)
elif ch in ".;!?":
pending_bnd = max(pending_bnd, 2)
if punct:
punct[-1] = 4 if ch == "." else 5
elif ch in ",:··":
if punct:
punct[-1] = {",": 1, "·": 2, "·": 2, ":": 3}.get(ch, 0)
i += 1
if boundary:
boundary[-1] = max(boundary[-1], 2)
return _Encoded(chars, boundary, dia, punct, cap)
def __call__(self, text: str, mask_planes: list[str] | None = None, has_boundaries: bool = True):
"""Encode `text` into model-ready tensors.
mask_planes: any subset of {"chars", "boundary", "dia", "punct"} to force to
UNKNOWN at every position (in addition to any '-' runs, which are always
treated as a damaged/masked span regardless of mask_planes).
has_boundaries: set False for scriptio continua input (no real spaces) so the
boundary plane starts fully UNKNOWN rather than "no boundaries found".
"""
mask_planes = set(mask_planes or [])
enc = self._classify(text)
n = len(enc.chars)
chars = np.array(enc.chars, dtype=np.int64)
boundary = np.array(enc.boundary, dtype=np.int64)
dia = np.array(enc.dia, dtype=np.int64)
punct = np.array(enc.punct, dtype=np.int64)
if "chars" in mask_planes:
chars[:] = MASK
if "boundary" in mask_planes or not has_boundaries:
boundary[:] = UNK_BND
if "dia" in mask_planes:
dia[:] = UNK_DIA
if "punct" in mask_planes:
punct[:] = UNK_PUNCT
batch = dict(
input_ids=torch.from_numpy(chars)[None],
boundary=torch.from_numpy(boundary)[None],
dia=torch.from_numpy(dia)[None],
punct=torch.from_numpy(punct)[None],
seg_id=torch.zeros(1, n, dtype=torch.long),
)
batch["_cap"] = enc.cap # kept out-of-band; not a model input (cap is output-only)
return batch
# ---------------------------------------------------------------- decode
@staticmethod
def _restore_polytonic(chars, dia, cap, boundary, punct=None) -> str:
words, cur = [], []
for i in range(len(chars)):
ch = ID2LETTER[chars[i]] if chars[i] < 24 else "?"
a, b, io, dd = _unpack_dia(int(dia[i]))
if cap[i]:
ch = ch.upper()
s = ch
if b:
s += _MARK_CHARS[{1: "smooth", 2: "rough"}[b]]
if dd:
s += _MARK_CHARS["diaer"]
if a:
s += _MARK_CHARS[{1: "acute", 2: "grave", 3: "circ"}[a]]
if io:
s += _MARK_CHARS["iota"]
cur.append(s)
p = int(punct[i]) if punct is not None else 0
if boundary[i] >= 1:
w = "".join(cur)
if w and w[-1] == "σ":
w = w[:-1] + "ς"
w = unicodedata.normalize("NFC", w)
if p in _PUNCT_CHARS:
w += _PUNCT_CHARS[p]
elif boundary[i] == 2:
w += "."
words.append(w)
cur = []
if cur:
w = unicodedata.normalize("NFC", "".join(cur))
p = int(punct[-1]) if punct is not None else 0
if p in _PUNCT_CHARS:
w += _PUNCT_CHARS[p]
words.append(w)
return " ".join(words)
def decode_restoration(self, model_out, batch) -> str:
"""Fill masked positions with the model's argmax predictions; keep every
other position exactly as given. Each plane is filled independently
wherever IT is unknown -- chars/cap only inside a '-' gap (chars==MASK),
but boundary/dia/punct wherever THAT plane is UNK, which may be the whole
sequence if mask_planes was also used for joint gap+accent+boundary
restoration (not just the '-' gap itself)."""
pred_char = model_out.char.argmax(-1)[0].tolist()
pred_bnd = model_out.boundary.argmax(-1)[0].tolist()
pred_dia = model_out.dia.argmax(-1)[0].tolist()
pred_cap = model_out.cap.argmax(-1)[0].tolist()
pred_punct = model_out.punct.argmax(-1)[0].tolist()
chars = batch["input_ids"][0].tolist()
boundary = batch["boundary"][0].tolist()
dia = batch["dia"][0].tolist()
punct = batch["punct"][0].tolist()
cap = batch["_cap"]
for i in range(len(chars)):
if chars[i] == MASK:
chars[i] = pred_char[i] if pred_char[i] < 24 else 0
cap[i] = pred_cap[i]
if boundary[i] == UNK_BND:
boundary[i] = pred_bnd[i]
if dia[i] == UNK_DIA:
dia[i] = pred_dia[i]
if punct[i] == UNK_PUNCT:
punct[i] = pred_punct[i]
return self._restore_polytonic(chars, dia, cap, boundary, punct)
def decode_diacritics(self, model_out, batch) -> str:
"""Replace the diacritic plane with the model's predictions; letters/boundaries/
capitalization/punctuation are taken from the input as given."""
pred_dia = model_out.dia.argmax(-1)[0].tolist()
chars = batch["input_ids"][0].tolist()
boundary = batch["boundary"][0].tolist()
punct = batch["punct"][0].tolist()
cap = batch["_cap"]
return self._restore_polytonic(chars, pred_dia, cap, boundary, punct)
def decode_boundaries(self, model_out, batch) -> str:
"""Replace the boundary plane with the model's predictions (0/1/2); letters/
diacritics/capitalization/punctuation are taken from the input as given.
Only useful when the input truly has no accents either (a spaced-out or
scriptio-continua text that already carries accents gives the boundary head
a strong shortcut -- each word carries exactly one accent -- so this isn't a
meaningful standalone test of the boundary head specifically; see
decode_restoration/restore_elastic for the realistic joint case)."""
pred_bnd = model_out.boundary.argmax(-1)[0].tolist()
chars = batch["input_ids"][0].tolist()
dia = batch["dia"][0].tolist()
punct = batch["punct"][0].tolist()
cap = batch["_cap"]
return self._restore_polytonic(chars, dia, cap, pred_bnd, punct)
def restore_elastic(self, model, text: str, min_width: int = 1,
mask_dia_boundary: bool = False):
"""Restore a lacuna of *uncertain* width -- the realistic editorial case,
since editors estimate a lacuna's length, they rarely know it exactly.
`text` must contain exactly one `[N±M]` marker (best-guess width N,
plausible range N-M..N+M), e.g. `"λόγ[5±3]καὶ ὁ λόγος ἦν πρὸς τὸν θεόν"`.
For every candidate width in that range, this fills the gap, then scores
each candidate by the mean log-probability of the model's own letter
predictions inside the gap specifically (that's what distinguishes widths).
`mask_dia_boundary` controls what happens OUTSIDE the gap:
- False (default): real accents/word-boundaries already present in
`text` are kept as given -- only the gap itself is filled. Use this
for text where the surrounding context is already known/accented (the
common editorial case: a lacuna in an otherwise-legible inscription).
- True: accents and word-boundaries are masked and reconstructed
everywhere, not just inside the gap -- for fully bare scriptio
continua surrounding the lacuna too (no spaces, no accents at all).
Returns `(best_text, best_width, candidates)`, where `candidates` is every
`(width, filled_text, mean_logp)` tried, sorted best-first. Needs the model
(not just its output), since it runs one forward pass per candidate width.
"""
m = _ELASTIC_RE.search(text)
if not m:
raise ValueError("text must contain one '[N±M]' marker, e.g. 'λόγ[5±3]καὶ'")
n, spread = int(m.group(1)), int(m.group(2))
prefix, suffix = text[:m.start()], text[m.end():]
gap_start = len(self._classify(prefix).chars)
candidates = []
for L in range(max(min_width, n - spread), n + spread + 1):
probe = prefix + ("-" * L) + suffix
if mask_dia_boundary:
batch = self(probe, mask_planes=["dia", "boundary"], has_boundaries=False)
else:
batch = self(probe)
with torch.no_grad():
out = model(**{k: v for k, v in batch.items() if not k.startswith("_")})
logp = torch.log_softmax(out.char, dim=-1)[0]
pred_char = out.char.argmax(-1)[0].tolist()
gap_logp = sum(logp[gap_start + i, pred_char[gap_start + i]].item()
for i in range(L)) / L
pred_bnd = out.boundary.argmax(-1)[0].tolist()
pred_dia = out.dia.argmax(-1)[0].tolist()
pred_cap = out.cap.argmax(-1)[0].tolist()
pred_punct = out.punct.argmax(-1)[0].tolist()
chars = batch["input_ids"][0].tolist()
boundary = batch["boundary"][0].tolist()
dia = batch["dia"][0].tolist()
punct = batch["punct"][0].tolist()
cap = batch["_cap"]
for i in range(len(chars)):
if chars[i] == MASK:
chars[i] = pred_char[i] if pred_char[i] < 24 else 0
cap[i] = pred_cap[i]
if boundary[i] == UNK_BND:
boundary[i] = pred_bnd[i]
if dia[i] == UNK_DIA:
dia[i] = pred_dia[i]
if punct[i] == UNK_PUNCT:
punct[i] = pred_punct[i]
filled = self._restore_polytonic(chars, dia, cap, boundary, punct)
candidates.append((L, filled, gap_logp))
candidates.sort(key=lambda c: -c[2])
best_L, best_text, _ = candidates[0]
return best_text, best_L, candidates
|