File size: 8,309 Bytes
f011463 | 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 | #!/usr/bin/env python3
"""Tokenizer pezzi goTommy2 locale: sostituisce ``gptommy.encode`` senza bytecode in /tmp.
``r08/gptommy/encode.py`` è un wrapper che carica il bytecode originale da
``/tmp/encode_ORIGINAL.cpython-314.pyc`` (cancellato dalla pulizia di macOS) o da
``__pycache__`` solo se >1000 byte; lì c'è ormai il wrapper stesso ricompilato
(722 byte), quindi l'import fallisce. Il sorgente originale non esiste più.
Il contratto dell'encoder sopravvive nel docstring del bytecode originale di
``pipeline.cpython-314.pyc`` (20 set): copia in
``vendor/gptommy_pycache_backup_20261019/``, da leggere con marshal senza
importare ``gptommy.pipeline`` (l'import sovrascriverebbe l'originale in r08):
1. NFC 2. speciali letterali ``<|...|>`` 3. whitespace (spazio→<spazio>,
\\n→<riga>, \\t→<tab>, \\r→<cr>, NBSP→<nbsp>; pezzi indent se esistono)
4. cifre: coppie 00–99 se in vocab, altrimenti cifra singola
5. longest-match greedy intra-parola, fallback 1 carattere, poi <|unk|>
6. case: match sul testo così com'è, poi ``lower()`` se non c'è pezzo.
Col vocab pezzi 1251 (nessuna coppia di cifre, nessun pezzo con spazi o indent,
nessun pezzo multi-carattere con maiuscole) il contratto coincide con
``vocab_stages.encode_pezzi_fallback``. ``encode`` replica esattamente quella
lettura (``TRAINING_VARIANT``); le altre letture ammesse dal testo del contratto
restano disponibili via ``encode_variant`` solo per la verifica
(``verify_tokenizer_equivalence.py``).
"""
from __future__ import annotations
import unicodedata
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Sequence, Tuple
ROOT = Path(__file__).resolve().parent
GOTTOMMY2_VOCAB = Path("/Users/thomasseri/Desktop/columns/logs/goTommy2_vocab_by_id.txt")
LOCAL_PEZZI_VOCAB = ROOT / "vocab_stages" / "00_pezzi_by_id.txt"
WS_PIECES = {"\n": "<riga>", "\t": "<tab>", "\r": "<cr>", "\xa0": "<nbsp>", " ": "<spazio>"}
DECODE_WS = {"<spazio>": " ", "<riga>": "\n", "<tab>": "\t"}
TRAINING_VARIANT = {
"nfc": False,
"literal_specials": False,
"case_rule": "per_length",
"unicode_ws": "isspace",
}
@dataclass
class Vocab:
id_to_token: List[str]
token_to_id: Dict[str, int]
path: Optional[Path] = None
def load_vocab(path: Path | str | None = None) -> Vocab:
"""Stesso formato TSV di ``gptommy.vocab.load_vocab`` (codice, scheda, riga, id, pezzo)."""
p = Path(path) if path is not None else (
LOCAL_PEZZI_VOCAB if LOCAL_PEZZI_VOCAB.is_file() else GOTTOMMY2_VOCAB
)
id_to_token: Dict[int, str] = {}
with p.open("r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) < 5:
continue
id_to_token[int(parts[3])] = parts[4]
if not id_to_token:
raise RuntimeError(f"vocab vuoto: {p}")
n = max(id_to_token) + 1
tokens = [id_to_token.get(i, f"<unk_{i}>") for i in range(n)]
return Vocab(id_to_token=tokens, token_to_id={t: i for i, t in enumerate(tokens)}, path=p)
class _Tables:
__slots__ = ("max_len", "specials", "unk_id")
def __init__(self, vocab) -> None:
toks = vocab.id_to_token
self.max_len = max((len(t) for t in toks if not t.startswith("<")), default=1)
self.specials = sorted(
(t for t in toks if t.startswith("<") and len(t) > 1 and t.endswith(">")),
key=len,
reverse=True,
)
self.unk_id = int(vocab.token_to_id.get("<|unk|>", 4))
_TABLE_CACHE: Dict[int, tuple] = {}
def _tables(vocab) -> _Tables:
key = id(vocab.token_to_id)
hit = _TABLE_CACHE.get(key)
if hit is not None and hit[0] is vocab.token_to_id:
return hit[1]
tab = _Tables(vocab)
_TABLE_CACHE[key] = (vocab.token_to_id, tab)
return tab
def _is_ws(ch: str, unicode_ws: str) -> bool:
if unicode_ws == "isspace":
return ch.isspace()
return ch in WS_PIECES
def encode_variant(
text: str,
vocab,
*,
nfc: bool = False,
literal_specials: bool = False,
case_rule: str = "per_length",
unicode_ws: str = "isspace",
) -> List[int]:
"""Encoder del contratto gpTommy con le letture alternative esplicite.
case_rule:
``per_length`` a ogni lunghezza prova il frammento così com'è, poi lower()
se alfabetico (= encode_pezzi_fallback; "Questo" → questo)
``per_position`` lower() solo se nessun pezzo matcha così com'è a quella
posizione ("Questo" → Q u e s t o, visto che A–Z esistono)
unicode_ws:
``isspace`` ogni carattere str.isspace() è whitespace (default <spazio>)
``listed`` solo spazio, \\n, \\t, \\r, NBSP; gli altri vanno al longest-match
literal_specials: speciali ``<...>`` di qualunque lunghezza consumati per primi
(il longest-match generico arriva solo a max_len=8 caratteri).
"""
if case_rule not in ("per_length", "per_position"):
raise ValueError(f"case_rule sconosciuta: {case_rule}")
s = unicodedata.normalize("NFC", text) if nfc else text
t2i = vocab.token_to_id
tab = _tables(vocab)
ids: List[int] = []
n = len(s)
i = 0
while i < n:
ch = s[i]
if _is_ws(ch, unicode_ws):
tid = t2i.get(WS_PIECES.get(ch, "<spazio>"))
if tid is not None:
ids.append(tid)
i += 1
continue
if literal_specials and ch == "<":
hit = next((sp for sp in tab.specials if s.startswith(sp, i)), None)
if hit is not None:
ids.append(t2i[hit])
i += len(hit)
continue
upper = min(n, i + tab.max_len)
tid = None
j = upper
while j > i:
frag = s[i:j]
tid = t2i.get(frag)
if tid is None and case_rule == "per_length" and frag.isalpha():
tid = t2i.get(frag.lower())
if tid is not None:
break
j -= 1
if tid is None and case_rule == "per_position":
j = upper
while j > i:
frag = s[i:j]
if frag.isalpha():
tid = t2i.get(frag.lower())
if tid is not None:
break
j -= 1
if tid is not None:
ids.append(tid)
i = j
continue
tid = t2i.get(ch)
if tid is None and ch.isalpha():
tid = t2i.get(ch.lower())
ids.append(tab.unk_id if tid is None else tid)
i += 1
return ids
def encode(text: str, vocab) -> List[int]:
"""Encode pezzi usato dal training (stessa lettura di encode_pezzi_fallback)."""
return encode_variant(text, vocab, **TRAINING_VARIANT)
def decode(ids: Sequence[int], vocab) -> str:
"""Concatena i pezzi; sostituisce solo <spazio>/<riga>/<tab> (contratto gpTommy)."""
toks = vocab.id_to_token
out: List[str] = []
for tid in ids:
tok = toks[tid] if 0 <= int(tid) < len(toks) else "<|unk|>"
out.append(DECODE_WS.get(tok, tok))
return "".join(out)
def cascade_from_pezzi(pezzi_ids: Sequence[int], bundle):
"""StageEncoded da pezzi già codificati: pass_to_v2 → pass_to_v3 (come encode_cascade)."""
from vocab_stages import StageEncoded, pass_to_v2, pass_to_v3
ids = [int(x) for x in pezzi_ids]
v2 = pass_to_v2(ids, bundle)
v3 = pass_to_v3(v2, bundle)
return StageEncoded(
pezzi_ids=ids,
parole_ids=list(v2.parole_ids),
unioni_ids=list(v3.unioni_ids),
spans=[],
v2=v2,
v3=v3,
)
def encode_cascade(text: str, bundle):
"""vocab_stages.encode_cascade con V1 = ``encode`` locale (mai gptommy)."""
return cascade_from_pezzi(encode(text, bundle.pezzi), bundle)
def encode_stages_aligned(text: str, bundle) -> Tuple[List[int], List[int], List[int]]:
"""(pezzi, parole, unioni) allineati sulla timeline pezzi, come nel training."""
from vocab_stages import align_stages_to_pezzi
return align_stages_to_pezzi(encode_cascade(text, bundle), bundle)
|