ctok 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ctok/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Offline reconstruction of Claude's tokenizer token counts."""
2
+
3
+ from .main import marked_stream, normalize, pieces, token_count, tokenize, witness
4
+
5
+ __all__ = ["token_count", "tokenize", "normalize", "marked_stream", "pieces", "witness"]
ctok/constants.py ADDED
@@ -0,0 +1,224 @@
1
+ """The marker notation and the measured character tables.
2
+
3
+ The tables were measured against ``count_tokens``. Tables marked as enumerated must stay
4
+ enumerated: no Unicode-category rule reproduces them.
5
+ """
6
+
7
+ import re
8
+
9
+ # ---- marker notation ----------------------------------------------------------------------------
10
+
11
+ # The engine computes over marked strings in which each structural marker is one codepoint.
12
+ BOW_G, EOW_G, PAD_G = "\ufdd0", "\ufdd1", "\ufdd2"
13
+ SHIFT_G, CAPS_G = "\ufdd3", "\ufdd4"
14
+
15
+ MARKER_GLYPHS = frozenset({BOW_G, EOW_G, SHIFT_G, CAPS_G})
16
+
17
+ # The public form: named atoms in mathematical angle brackets, practically absent from real text.
18
+ # Literal brackets in text are byte-escaped at render time, so a rendered token always parses back
19
+ # unambiguously.
20
+ L, R = "⟨", "⟩"
21
+ BOW, EOW, SHIFT, CAPS = f"{L}bow{R}", f"{L}eow{R}", f"{L}shift{R}", f"{L}caps{R}"
22
+ PAD = f"{L}pad{R}"
23
+
24
+ GLYPH_TO_ATOM = {BOW_G: BOW, EOW_G: EOW, PAD_G: PAD, SHIFT_G: SHIFT, CAPS_G: CAPS}
25
+ ATOM_TO_GLYPH = {BOW: BOW_G, EOW: EOW_G, SHIFT: SHIFT_G, CAPS: CAPS_G}
26
+
27
+ # Scraped text can contain the noncharacters used above. NFC strips BMP private-use input, so these
28
+ # five private-use codepoints are safe internal escapes inserted after NFC. The byte floor and
29
+ # renderer map them back to the original noncharacters.
30
+ LITERAL_MARKER_ESCAPES = {glyph: chr(0xE000 + i) for i, glyph in enumerate(GLYPH_TO_ATOM)}
31
+ ESCAPED_MARKER_LITERALS = {escape: glyph for glyph, escape in LITERAL_MARKER_ESCAPES.items()}
32
+ LITERAL_MARKER_ESCAPE_TABLE = str.maketrans(LITERAL_MARKER_ESCAPES)
33
+
34
+ # ---- character classes --------------------------------------------------------------------------
35
+
36
+ WORDY, HARD, DIGIT, PUNCT, SPACE = "wordy", "hard", "digit", "punct", "space"
37
+
38
+ # Non-ASCII symbols/punctuation that tile over the punct vocabulary rather than standing alone.
39
+ # Enumerated: the behaviour splits per codepoint with no categorical rule (`(` is punct but `)`
40
+ # is hard). Anything unlisted stays HARD.
41
+ PUNCT_SYMS = frozenset("—»«•°„–−£§€…√→(№†└│།·─═█")
42
+
43
+ # Symbol-letters measured to take the full word model (⟨bow⟩ and ⟨eow⟩ flanks, seam absorption, case
44
+ # markers) exactly like Latin letters. Enumerated blocks: category Nl/So/Lu splits both ways, and
45
+ # the Hangzhou numerals (also Nl) measured markerless, so it is the block that predicts, not the
46
+ # category.
47
+ SYMBOL_LETTERS = (
48
+ (0x16EE, 0x16F0), # Runic golden numbers (Nl, caseless)
49
+ (0x2160, 0x2188), # Roman numerals (Nl/Lu/Ll, cased pairs)
50
+ (0x24B6, 0x24E9), # circled letters (So, cased)
51
+ (0xA6E6, 0xA6EF), # Bamum number-letters (Nl, caseless)
52
+ )
53
+
54
+ # Variation selectors are gc=Mn but take no word model: each costs one more than its base
55
+ # character, where ⟨bow⟩ and ⟨eow⟩ flanks would read three more. The supplementary selectors
56
+ # (U+E0100 to U+E01EF) are astral and already HARD.
57
+ VARIATION_SELECTORS = (0xFE00, 0xFE0F)
58
+
59
+ # The one canonical-combining-class-9 character that does not separate word runs: measured over
60
+ # as a separator on its own script's consonants and under wherever no letter precedes it. See
61
+ # :func:`normalize.is_separator` for the grid.
62
+ NON_SEPARATORS = frozenset("ฺ") # THAI CHARACTER PHINTHU
63
+
64
+ # Marks that separate word runs the way a virama does, but without combining class 9. Measured
65
+ # one character at a time on byte-floor consonant hosts; enumerated, because the rule is
66
+ # orthographic (the neighbouring codepoint is usually a vowel sign that does not split). The
67
+ # U+0300 block is handled as a range instead. See :data:`SEPARATOR_MARKS`.
68
+ SEPARATOR_SIGNS = frozenset((
69
+ "\u0740", # SYRIAC FEMININE DOT
70
+ "\u0741", # SYRIAC QUSHSHAYA
71
+ "\u0742", # SYRIAC RUKKAKHA
72
+ "\u0743", # SYRIAC TWO VERTICAL DOTS ABOVE
73
+ "\u0744", # SYRIAC TWO VERTICAL DOTS BELOW
74
+ "\u0745", # SYRIAC THREE DOTS ABOVE
75
+ "\u0746", # SYRIAC THREE DOTS BELOW
76
+ "\u0747", # SYRIAC OBLIQUE LINE ABOVE
77
+ "\u0748", # SYRIAC OBLIQUE LINE BELOW
78
+ "\u0749", # SYRIAC MUSIC
79
+ "\u074a", # SYRIAC BARREKH
80
+ "\u093c", # DEVANAGARI SIGN NUKTA
81
+ "\u0951", # DEVANAGARI STRESS SIGN UDATTA
82
+ "\u0952", # DEVANAGARI STRESS SIGN ANUDATTA
83
+ "\u0953", # DEVANAGARI GRAVE ACCENT
84
+ "\u0954", # DEVANAGARI ACUTE ACCENT
85
+ "\u09bc", # BENGALI SIGN NUKTA
86
+ "\u09fe", # BENGALI SANDHI MARK
87
+ "\u0a3c", # GURMUKHI SIGN NUKTA
88
+ "\u0abc", # GUJARATI SIGN NUKTA
89
+ "\u0afd", # GUJARATI SIGN THREE-DOT NUKTA ABOVE
90
+ "\u0afe", # GUJARATI SIGN CIRCLE NUKTA ABOVE
91
+ "\u0aff", # GUJARATI SIGN TWO-CIRCLE NUKTA ABOVE
92
+ "\u0b3c", # ORIYA SIGN NUKTA
93
+ "\u0b55", # ORIYA SIGN OVERLINE
94
+ # U+0C03 TELUGU SIGN VISARGA is deliberately not here: it is a spacing mark (Mc, ccc 0) that
95
+ # measures as plain word material, closing a word with a `ః⟨eow⟩` piece rather than splitting it.
96
+ "\u0c3c", # TELUGU SIGN NUKTA
97
+ "\u0cbc", # KANNADA SIGN NUKTA
98
+ "\u0e47", # THAI CHARACTER MAITAIKHU
99
+ "\u0e48", # THAI CHARACTER MAI EK
100
+ "\u0e49", # THAI CHARACTER MAI THO
101
+ "\u0e4a", # THAI CHARACTER MAI TRI
102
+ "\u0e4b", # THAI CHARACTER MAI CHATTAWA
103
+ "\u0e4c", # THAI CHARACTER THANTHAKHAT
104
+ "\u0e4e", # THAI CHARACTER YAMAKKAN
105
+ "\u0ec8", # LAO TONE MAI EK
106
+ "\u0ec9", # LAO TONE MAI THO
107
+ "\u0eca", # LAO TONE MAI TI
108
+ "\u0ecb", # LAO TONE MAI CATAWA
109
+ "\u0ecc", # LAO CANCELLATION MARK
110
+ "\u0ece", # LAO YAMAKKAN
111
+ "\u0f18", # TIBETAN ASTROLOGICAL SIGN -KHYUD PA
112
+ "\u0f19", # TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS
113
+ "\u0f35", # TIBETAN MARK NGAS BZUNG NYI ZLA
114
+ "\u0f37", # TIBETAN MARK NGAS BZUNG SGOR RTAGS
115
+ "\u0f39", # TIBETAN MARK TSA -PHRU
116
+ "\u0f3e", # TIBETAN SIGN YAR TSHES
117
+ "\u0f3f", # TIBETAN SIGN MAR TSHES
118
+ "\u0f86", # TIBETAN SIGN LCI RTAGS
119
+ "\u0f87", # TIBETAN SIGN YANG RTAGS
120
+ "\u0fc6", # TIBETAN SYMBOL PADMA GDAN
121
+ "\u1037", # MYANMAR SIGN DOT BELOW
122
+ "\u17b4", # KHMER VOWEL INHERENT AQ
123
+ "\u17b5", # KHMER VOWEL INHERENT AA
124
+ "\u17c9", # KHMER SIGN MUUSIKATOAN
125
+ "\u17ca", # KHMER SIGN TRIISAP
126
+ "\u17cb", # KHMER SIGN BANTOC
127
+ "\u17cc", # KHMER SIGN ROBAT
128
+ "\u17cd", # KHMER SIGN TOANDAKHIAT
129
+ "\u17ce", # KHMER SIGN KAKABAT
130
+ "\u17cf", # KHMER SIGN AHSDA
131
+ "\u17d0", # KHMER SIGN SAMYOK SANNYA
132
+ "\u17d1", # KHMER SIGN VIRIAM
133
+ "\u17d3", # KHMER SIGN BATHAMASAT
134
+ "\u17dd", # KHMER SIGN ATTHACAN
135
+ "\u1a75", # TAI THAM SIGN TONE-1
136
+ "\u1a76", # TAI THAM SIGN TONE-2
137
+ "\u1a77", # TAI THAM SIGN KHUEN TONE-3
138
+ "\u1a78", # TAI THAM SIGN KHUEN TONE-4
139
+ "\u1a79", # TAI THAM SIGN KHUEN TONE-5
140
+ "\u1a7a", # TAI THAM SIGN RA HAAM
141
+ "\u1a7b", # TAI THAM SIGN MAI SAM
142
+ "\u1a7c", # TAI THAM SIGN KHUEN-LUE KARAN
143
+ "\u1a7f", # TAI THAM COMBINING CRYPTOGRAMMIC DOT
144
+ "\u1b34", # BALINESE SIGN REREKAN
145
+ "\u1b6b", # BALINESE MUSICAL SYMBOL COMBINING TEGEH
146
+ "\u1b6c", # BALINESE MUSICAL SYMBOL COMBINING ENDEP
147
+ "\u1b6d", # BALINESE MUSICAL SYMBOL COMBINING KEMPUL
148
+ "\u1b6e", # BALINESE MUSICAL SYMBOL COMBINING KEMPLI
149
+ "\u1b6f", # BALINESE MUSICAL SYMBOL COMBINING JEGOGAN
150
+ "\u1b70", # BALINESE MUSICAL SYMBOL COMBINING KEMPUL WITH JEGOGAN
151
+ "\u1b71", # BALINESE MUSICAL SYMBOL COMBINING KEMPLI WITH JEGOGAN
152
+ "\u1b72", # BALINESE MUSICAL SYMBOL COMBINING BENDE
153
+ "\u1b73", # BALINESE MUSICAL SYMBOL COMBINING GONG
154
+ "\ua9b3", # JAVANESE SIGN CECAK TELU
155
+ ))
156
+
157
+ # ---- normalization ------------------------------------------------------------------------------
158
+
159
+ # C0/C1 controls the API strips before tokenizing (cost 0), i.e. every gc=Cc except TAB, LF and NUL.
160
+ STRIP_CONTROL = re.compile("[\x01-\x08\x0B-\x1F\x7F\x80-\x9F]")
161
+
162
+ # BMP private use is stripped the same way, and its two neighbours join into one word. Unassigned
163
+ # codepoints survive and pay their bytes; the supplementary private-use planes are unprobed and
164
+ # deliberately left out.
165
+ STRIP_PRIVATE = re.compile("[-]")
166
+
167
+ # Lone surrogates are valid in a str but not UTF-8-encodable, so they would crash the byte floor.
168
+ # They can never be genuine tokenizer input; fold each to U+FFFD, which prices as an ordinary
169
+ # 3-byte HARD character. This is what keeps token_count total on any str.
170
+ SURROGATE = re.compile(r"[\ud800-\udfff]")
171
+
172
+ # Space separators the tokenizer treats identically to U+0020: all Zs except U+3000 (ideographic
173
+ # space), plus Zl/Zp. NB U+3000, TAB and LF each have their own cost. NUL is not stripped but
174
+ # tokenizes exactly like a space, so it is folded here too.
175
+ FUNNY_SPACE = re.compile("[\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F]")
176
+
177
+ # The four standard curly quotes fold to their ASCII forms (v3 only; NFC does not do this). The
178
+ # low-9 mark U+201E is a different token and is deliberately not folded.
179
+ QUOTE_FOLD = str.maketrans({"\u2018": "'", "\u2019": "'", "\u201C": '"', "\u201D": '"'})
180
+
181
+ # The suffixes an apostrophe binds into the word ahead of it, deleting that word's ⟨bow⟩. This is the
182
+ # standard English contraction set, lowercase and whole-word only. Measured per member; other
183
+ # suffixes pay a full boundary even when their own piece exists, so no piece-based rule reproduces
184
+ # this. Case-sensitive and whole-word (`x'S`, `x'LL`, `x'llo`, `x'lls` all pay).
185
+ CONTRACTION_SUFFIXES = frozenset({"s", "t", "d", "m", "ll", "re", "ve"})
186
+
187
+ # ---- the seam law -------------------------------------------------------------------------------
188
+
189
+ # ⟨eow⟩ ' ' [case markers] ⟨bow⟩ -> ⟨eow⟩ [case markers] ⟨bow⟩: a single space between two marked
190
+ # spans is the seam and is not written as a character.
191
+ SEAM_RE = re.compile("(.)" + EOW_G + " " + "([" + SHIFT_G + CAPS_G + "]*)" + BOW_G)
192
+
193
+ # ---- marks that stand outside the word -----------------------------------------------------------
194
+
195
+ # Combining marks that close the word before themselves, exactly as a virama does
196
+ # (`normalize.is_separator` folds this range in): `q́z` = `⟨bow⟩q⟨eow⟩` + mark + `⟨bow⟩z⟨eow⟩`, so an
197
+ # accent makes two words out of one. Two oracle frames on one-token hosts distinguish the
198
+ # spellings. A sweep covered U+0300 to U+036F on 21 hosts across 13 scripts and both families, with
199
+ # no dissent. U+0345 and the combining Latin letters U+0363 to U+036F pin both ends of the range
200
+ # from outside by reading "inside the word". U+0340, U+0341, U+0343, and U+0344 are NFC-folded away
201
+ # and inside the range for that reason alone.
202
+ SEPARATOR_MARKS = re.compile("[\u0300-\u0344\u0346-\u0362]")
203
+
204
+ # The same question asked of every other combining block of the BMP: 418 marks swept on the same
205
+ # two frames, both families, no dissent. The split is orthographic, as for Thai:
206
+ #
207
+ # OUTSIDE the word accents, tone marks, cantillation and annotation
208
+ # INSIDE the word vowel points and combining letters (niqqud, harakat, the Syriac vowels,
209
+ # Thaana, Samaritan, the combining Latin/Cyrillic letter blocks)
210
+ #
211
+ # Most ranges are pinned on both sides by an inside-the-word neighbour in the same block; where a
212
+ # range abuts unassigned codepoints, it is written to the marks actually probed. The N'Ko and
213
+ # Mandaic marks have no in-script one-token host and rest on `q` and `б` alone.
214
+ SEPARATOR_ANNOTATIONS = re.compile(
215
+ "[\u0483-\u0489\u0591-\u05af\u0658\u06df-\u06e0\u06ea-\u06ec"
216
+ "\u07eb-\u07f3\u07fd\u0859-\u085b\u0898-\u089f\u08ca-\u08d3\u08e0-\u08e1"
217
+ "\u0818-\u0819\u082d\u08ea-\u08ef\u135d-\u135f"
218
+ "\u180b-\u180d\u180f\u1939-\u193b"
219
+ "\u1ab0-\u1abe\u1ac1-\u1acb\u1cd0-\u1ce8\u1ced\u1cf4\u1cf8-\u1cf9"
220
+ "\u1be6\u1c37\u1cf7"
221
+ "\u1dc0-\u1dd2\u1df5-\u1dff\u20d0-\u20f0\ua66f-\ua672\ua67c-\ua67d"
222
+ "\u2cef-\u2cf1\u302a-\u302f\u3099-\u309a"
223
+ "\ua6f0-\ua6f1\ua8e0-\ua8f1\ua92b-\ua92d\uaabf\uaac1\uabec"
224
+ "\ufe20-\ufe2f]")