jvonrad commited on
Commit
5f73143
·
verified ·
1 Parent(s): d2e4870

Upload src/xscript/tok/wrapper.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/xscript/tok/wrapper.py +102 -0
src/xscript/tok/wrapper.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Uniform interface over the tokenizer flavors.
2
+
3
+ `unigram` loads a SentencePiece model; `bpe`/`pa` load a HuggingFace byte-level
4
+ BPE. Ids 0..3 are <unk>/<bos>/<eos>/<pad> in every flavor, so packing, training
5
+ and eval code never branches on flavor.
6
+ """
7
+ import json
8
+ from functools import cached_property
9
+ from pathlib import Path
10
+
11
+ UNK_ID, BOS_ID, EOS_ID, PAD_ID = 0, 1, 2, 3
12
+
13
+
14
+ class Tok:
15
+ def __init__(self, path: str | Path):
16
+ self.dir = Path(path)
17
+ self.meta = json.loads((self.dir / "meta.json").read_text())
18
+ self.flavor = self.meta["flavor"]
19
+ self.condition = self.meta["condition"]
20
+ self.name = f"{self.flavor}_{self.condition}"
21
+ if self.flavor == "unigram":
22
+ import sentencepiece as spm
23
+ self._sp = spm.SentencePieceProcessor(model_file=str(self.dir / "sp.model"))
24
+ self.vocab_size = self._sp.get_piece_size()
25
+ else:
26
+ from tokenizers import Tokenizer
27
+ self._hf = Tokenizer.from_file(str(self.dir / "tokenizer.json"))
28
+ self.vocab_size = self._hf.get_vocab_size()
29
+
30
+ def encode(self, text: str, bos: bool = False, eos: bool = False) -> list[int]:
31
+ if self.flavor == "unigram":
32
+ ids = self._sp.encode(text)
33
+ else:
34
+ ids = self._hf.encode(text).ids
35
+ if bos:
36
+ ids = [BOS_ID] + ids
37
+ if eos:
38
+ ids = ids + [EOS_ID]
39
+ return ids
40
+
41
+ def encode_batch(self, texts: list[str], bos: bool = False, eos: bool = False):
42
+ if self.flavor == "unigram":
43
+ batch = self._sp.encode(texts)
44
+ else:
45
+ batch = [e.ids for e in self._hf.encode_batch(texts)]
46
+ if bos or eos:
47
+ batch = [([BOS_ID] if bos else []) + ids + ([EOS_ID] if eos else [])
48
+ for ids in batch]
49
+ return batch
50
+
51
+ def decode(self, ids: list[int]) -> str:
52
+ if self.flavor == "unigram":
53
+ return self._sp.decode([i for i in ids if i > PAD_ID])
54
+ return self._hf.decode([i for i in ids if i > PAD_ID], skip_special_tokens=True)
55
+
56
+ # --- introspection for the analysis gate ---
57
+
58
+ def piece(self, idx: int) -> str:
59
+ """Raw vocabulary piece as stored (SP: '▁'-form / '<0xNN>'; BL: bytelevel-mapped)."""
60
+ if self.flavor == "unigram":
61
+ return self._sp.id_to_piece(idx)
62
+ return self._id_to_piece_bl[idx]
63
+
64
+ @cached_property
65
+ def _id_to_piece_bl(self) -> list[str]:
66
+ vocab = self._hf.get_vocab()
67
+ pieces = [""] * self.vocab_size
68
+ for p, i in vocab.items():
69
+ pieces[i] = p
70
+ return pieces
71
+
72
+ @cached_property
73
+ def _bl_byte_map(self) -> dict[str, int]:
74
+ # inverse of the GPT-2 bytes<->unicode table used by ByteLevel
75
+ bs = list(range(ord("!"), ord("~") + 1)) + \
76
+ list(range(0xA1, 0xAC + 1)) + list(range(0xAE, 0xFF + 1))
77
+ cs = bs[:]
78
+ n = 0
79
+ for b in range(256):
80
+ if b not in bs:
81
+ bs.append(b)
82
+ cs.append(256 + n)
83
+ n += 1
84
+ return {chr(c): b for b, c in zip(bs, cs)}
85
+
86
+ def piece_bytes(self, idx: int) -> bytes:
87
+ """The exact bytes a vocab entry emits (specials -> b'')."""
88
+ p = self.piece(idx)
89
+ if p in ("<unk>", "<bos>", "<eos>", "<pad>"):
90
+ return b""
91
+ if self.flavor == "unigram":
92
+ if len(p) == 6 and p.startswith("<0x") and p.endswith(">"):
93
+ return bytes([int(p[3:5], 16)]) # byte-fallback piece
94
+ return p.replace("▁", " ").encode("utf-8")
95
+ return bytes(self._bl_byte_map[ch] for ch in p)
96
+
97
+ def is_byte_piece(self, idx: int) -> bool:
98
+ """True if this vocab entry is a raw-byte atom (SP fallback / BL base byte)."""
99
+ p = self.piece(idx)
100
+ if self.flavor == "unigram":
101
+ return len(p) == 6 and p.startswith("<0x") and p.endswith(">")
102
+ return idx >= 4 and len(self.piece_bytes(idx)) == 1