File size: 8,309 Bytes
4911af3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)