ctok 1.0.0__tar.gz
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-1.0.0/.github/workflows/ci.yml +54 -0
- ctok-1.0.0/.github/workflows/publish.yml +51 -0
- ctok-1.0.0/.gitignore +15 -0
- ctok-1.0.0/LICENSE +21 -0
- ctok-1.0.0/PKG-INFO +133 -0
- ctok-1.0.0/README.md +112 -0
- ctok-1.0.0/ctok/__init__.py +5 -0
- ctok-1.0.0/ctok/constants.py +224 -0
- ctok-1.0.0/ctok/data/pieces_v3.json +48722 -0
- ctok-1.0.0/ctok/data/pieces_v4_7.json +15315 -0
- ctok-1.0.0/ctok/engine.py +220 -0
- ctok-1.0.0/ctok/main.py +214 -0
- ctok-1.0.0/ctok/normalize.py +550 -0
- ctok-1.0.0/ctok/notation.py +87 -0
- ctok-1.0.0/ctok/witness.py +139 -0
- ctok-1.0.0/pyproject.toml +50 -0
- ctok-1.0.0/tests/fixtures/multipl_e.jsonl.gz +0 -0
- ctok-1.0.0/tests/fixtures/multipl_e_counts.json +83 -0
- ctok-1.0.0/tests/fixtures/rosetta.jsonl.gz +0 -0
- ctok-1.0.0/tests/fixtures/rosetta_counts.json +5240 -0
- ctok-1.0.0/tests/fixtures/rosetta_holdout.jsonl.gz +0 -0
- ctok-1.0.0/tests/fixtures/rosetta_holdout_counts.json +767 -0
- ctok-1.0.0/tests/fixtures/udhr.jsonl.gz +0 -0
- ctok-1.0.0/tests/fixtures/udhr_counts.json +1520 -0
- ctok-1.0.0/tests/gates.py +290 -0
- ctok-1.0.0/tests/test_api.py +414 -0
- ctok-1.0.0/tests/test_engine.py +20 -0
- ctok-1.0.0/tests/test_multipl_e.py +13 -0
- ctok-1.0.0/tests/test_notation.py +14 -0
- ctok-1.0.0/tests/test_rosetta.py +25 -0
- ctok-1.0.0/tests/test_udhr.py +11 -0
- ctok-1.0.0/tests/test_witness.py +165 -0
- ctok-1.0.0/uv.lock +116 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
pull-requests: write # to post the gate report as a PR comment
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.12", "3.13"]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v5
|
|
24
|
+
|
|
25
|
+
- uses: astral-sh/setup-uv@v6
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
|
|
29
|
+
- name: Install
|
|
30
|
+
run: uv sync --dev --python ${{ matrix.python-version }}
|
|
31
|
+
|
|
32
|
+
- name: Test
|
|
33
|
+
run: uv run --python ${{ matrix.python-version }} pytest -q
|
|
34
|
+
|
|
35
|
+
- name: Report gates, vocabulary and witness coverage
|
|
36
|
+
if: matrix.python-version == '3.12'
|
|
37
|
+
run: |
|
|
38
|
+
uv run --python ${{ matrix.python-version }} python tests/gates.py \
|
|
39
|
+
| tee gates.md >> "$GITHUB_STEP_SUMMARY"
|
|
40
|
+
cat gates.md
|
|
41
|
+
|
|
42
|
+
# Same-repo pull requests only: a fork's GITHUB_TOKEN is read-only and cannot comment.
|
|
43
|
+
# The step summary above is always written, so a fork PR still reports, just not inline.
|
|
44
|
+
- name: Comment gates and witness coverage on the pull request
|
|
45
|
+
if: >-
|
|
46
|
+
matrix.python-version == '3.12'
|
|
47
|
+
&& github.event_name == 'pull_request'
|
|
48
|
+
&& github.event.pull_request.head.repo.full_name == github.repository
|
|
49
|
+
env:
|
|
50
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
PR: ${{ github.event.pull_request.number }}
|
|
52
|
+
run: |
|
|
53
|
+
gh pr comment "$PR" --body-file gates.md --edit-last --create-if-none \
|
|
54
|
+
|| gh pr comment "$PR" --body-file gates.md
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
with:
|
|
18
|
+
enable-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Test
|
|
21
|
+
run: |
|
|
22
|
+
uv sync --dev
|
|
23
|
+
uv run pytest -q
|
|
24
|
+
|
|
25
|
+
- name: Build
|
|
26
|
+
run: uv build
|
|
27
|
+
|
|
28
|
+
- name: Check metadata
|
|
29
|
+
run: uvx twine check dist/*
|
|
30
|
+
|
|
31
|
+
- uses: actions/upload-artifact@v4
|
|
32
|
+
with:
|
|
33
|
+
name: dist
|
|
34
|
+
path: dist/
|
|
35
|
+
|
|
36
|
+
publish:
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment:
|
|
40
|
+
name: pypi
|
|
41
|
+
url: https://pypi.org/p/ctok
|
|
42
|
+
permissions:
|
|
43
|
+
# Required for PyPI trusted publishing (OIDC); no API token is stored.
|
|
44
|
+
id-token: write
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/download-artifact@v5
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist/
|
|
50
|
+
|
|
51
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
ctok-1.0.0/.gitignore
ADDED
ctok-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sander Land
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ctok-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ctok
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Offline reconstruction of Claude's tokenizer token counts
|
|
5
|
+
Project-URL: Repository, https://github.com/sanderland/ctok
|
|
6
|
+
Project-URL: Issues, https://github.com/sanderland/ctok/issues
|
|
7
|
+
Project-URL: Research, https://tokencontributions.substack.com/p/on-the-biology-of-claudes-tokenizer
|
|
8
|
+
Author: Sander Land
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: anthropic,claude,nlp,token-count,tokenizer
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# ctok
|
|
23
|
+
|
|
24
|
+
`ctok` reconstructs Claude token counts offline, with no API call, network access, or runtime
|
|
25
|
+
dependencies. It is unofficial and is not affiliated with Anthropic.
|
|
26
|
+
|
|
27
|
+
The reconstruction targets counts. Claude does not expose token boundaries, so `tokenize()` returns
|
|
28
|
+
one valid minimum-cost tiling, not a claim about Anthropic's exact segmentation. The research behind
|
|
29
|
+
the model is described in [On the biology of Claude's tokenizer](https://tokencontributions.substack.com/p/on-the-biology-of-claudes-tokenizer).
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install ctok
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from ctok import token_count, tokenize
|
|
41
|
+
|
|
42
|
+
token_count("hello, world") # 10, using the v3 family
|
|
43
|
+
token_count("hello, world", "4.7") # 15
|
|
44
|
+
token_count("hello, world", "5.0") # 10
|
|
45
|
+
|
|
46
|
+
tokens = tokenize("NASA likes tokenizers")
|
|
47
|
+
assert len(tokens) == token_count("NASA likes tokenizers")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The command-line interface prints the marked stream and its tiling:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ctok "hello, world"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Supported families
|
|
57
|
+
|
|
58
|
+
`version` is a string such as `"4.7"`. Components are compared as integers, so `"4.10"` sorts
|
|
59
|
+
after `"4.9"`. Python reads the float literal `4.10` as `4.1`, so passing a non-string version
|
|
60
|
+
raises `TypeError`.
|
|
61
|
+
|
|
62
|
+
| requested version | family | model generation |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `"3.0" <= version < "4.7"` | v3, the default | Claude 3 through Opus 4.6 |
|
|
65
|
+
| `"4.7" <= version < "5.0"` | v4.7 | Opus 4.7 through 4.9 |
|
|
66
|
+
| `version >= "5.0"` | v5 | Opus 5 and Sonnet 5 |
|
|
67
|
+
|
|
68
|
+
v5 uses the v4.7 vocabulary with a different message frame.
|
|
69
|
+
|
|
70
|
+
## How it works
|
|
71
|
+
|
|
72
|
+
For one user message, `ctok`:
|
|
73
|
+
|
|
74
|
+
1. normalizes the text, including NFC and family-specific quote folding;
|
|
75
|
+
2. rewrites it into a stream with word, case, and byte markers;
|
|
76
|
+
3. finds a minimum-cost tiling over the measured vocabulary and UTF-8 byte fallback;
|
|
77
|
+
4. adds the measured message frame.
|
|
78
|
+
|
|
79
|
+
`token_count(text)` is `len(tokenize(text))`. The output notation makes internal structure visible:
|
|
80
|
+
|
|
81
|
+
| notation | meaning |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `⟨bow⟩`, `⟨eow⟩` | word boundaries |
|
|
84
|
+
| `⟨shift⟩`, `⟨caps⟩` | case rewrites |
|
|
85
|
+
| `⟨0xNN⟩` | a byte-fallback token |
|
|
86
|
+
| `⟨pad⟩` | part of the single-message frame |
|
|
87
|
+
|
|
88
|
+
## Measured accuracy
|
|
89
|
+
|
|
90
|
+
These results compare `ctok` with recorded `count_tokens` responses:
|
|
91
|
+
|
|
92
|
+
| corpus | role | v3 exact | v4.7 exact |
|
|
93
|
+
|---|---|---:|---:|
|
|
94
|
+
| Goldfish, 350 languages and 350,000 rows | mining | 350,000 | 350,000 |
|
|
95
|
+
| MultiPL-E, 22 programming languages | held out | 22 | 22 |
|
|
96
|
+
| Rosetta Code, 1,741 documents | mining | 1,741 | 1,741 |
|
|
97
|
+
| Rosetta Code, separate 250 documents | held out | 250 | 250 |
|
|
98
|
+
| UDHR, 501 languages | mining (in-sample since 2026-08-12) | 501 | 501 |
|
|
99
|
+
|
|
100
|
+
v5 is omitted from the table because its deviation from recorded counts matches v4.7's on every
|
|
101
|
+
gated document. Separate API tests cover its message-frame rules.
|
|
102
|
+
|
|
103
|
+
The stored measurement sets contain no under-counts: 0 of 1,664,940 v3 texts and 0 of 1,722,961
|
|
104
|
+
v4.7 texts. This does not guarantee the result for arbitrary input. Goldfish, the main Rosetta
|
|
105
|
+
sample, and UDHR informed piece selection. MultiPL-E and the separate Rosetta sample did not.
|
|
106
|
+
|
|
107
|
+
Run the public gates with:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv run pytest
|
|
111
|
+
uv run python tests/gates.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Vocabulary evidence
|
|
115
|
+
|
|
116
|
+
The two vocabulary files contain 48,645 v3 pieces and 15,240 v4.7 pieces. Every entry has a fixed
|
|
117
|
+
membership witness or is one of the structural marker atoms checked by the test suite.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from ctok import pieces, witness
|
|
121
|
+
|
|
122
|
+
len(pieces("4.7")) # 15240
|
|
123
|
+
witness("⟨bow⟩the⟨eow⟩", "4.7")
|
|
124
|
+
# {'probe': 'the', 'raw': 12, 'kind': 'raw'}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A witness says that one marked piece costs one token in a calibrated probe. It does not prove the
|
|
128
|
+
encoder rewrite or resolve ties between equal-cost tilings. `tests/test_witness.py` checks every
|
|
129
|
+
published witness and requires complete witnessed-or-special coverage.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
ctok-1.0.0/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# ctok
|
|
2
|
+
|
|
3
|
+
`ctok` reconstructs Claude token counts offline, with no API call, network access, or runtime
|
|
4
|
+
dependencies. It is unofficial and is not affiliated with Anthropic.
|
|
5
|
+
|
|
6
|
+
The reconstruction targets counts. Claude does not expose token boundaries, so `tokenize()` returns
|
|
7
|
+
one valid minimum-cost tiling, not a claim about Anthropic's exact segmentation. The research behind
|
|
8
|
+
the model is described in [On the biology of Claude's tokenizer](https://tokencontributions.substack.com/p/on-the-biology-of-claudes-tokenizer).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install ctok
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from ctok import token_count, tokenize
|
|
20
|
+
|
|
21
|
+
token_count("hello, world") # 10, using the v3 family
|
|
22
|
+
token_count("hello, world", "4.7") # 15
|
|
23
|
+
token_count("hello, world", "5.0") # 10
|
|
24
|
+
|
|
25
|
+
tokens = tokenize("NASA likes tokenizers")
|
|
26
|
+
assert len(tokens) == token_count("NASA likes tokenizers")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The command-line interface prints the marked stream and its tiling:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
ctok "hello, world"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Supported families
|
|
36
|
+
|
|
37
|
+
`version` is a string such as `"4.7"`. Components are compared as integers, so `"4.10"` sorts
|
|
38
|
+
after `"4.9"`. Python reads the float literal `4.10` as `4.1`, so passing a non-string version
|
|
39
|
+
raises `TypeError`.
|
|
40
|
+
|
|
41
|
+
| requested version | family | model generation |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `"3.0" <= version < "4.7"` | v3, the default | Claude 3 through Opus 4.6 |
|
|
44
|
+
| `"4.7" <= version < "5.0"` | v4.7 | Opus 4.7 through 4.9 |
|
|
45
|
+
| `version >= "5.0"` | v5 | Opus 5 and Sonnet 5 |
|
|
46
|
+
|
|
47
|
+
v5 uses the v4.7 vocabulary with a different message frame.
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
For one user message, `ctok`:
|
|
52
|
+
|
|
53
|
+
1. normalizes the text, including NFC and family-specific quote folding;
|
|
54
|
+
2. rewrites it into a stream with word, case, and byte markers;
|
|
55
|
+
3. finds a minimum-cost tiling over the measured vocabulary and UTF-8 byte fallback;
|
|
56
|
+
4. adds the measured message frame.
|
|
57
|
+
|
|
58
|
+
`token_count(text)` is `len(tokenize(text))`. The output notation makes internal structure visible:
|
|
59
|
+
|
|
60
|
+
| notation | meaning |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `⟨bow⟩`, `⟨eow⟩` | word boundaries |
|
|
63
|
+
| `⟨shift⟩`, `⟨caps⟩` | case rewrites |
|
|
64
|
+
| `⟨0xNN⟩` | a byte-fallback token |
|
|
65
|
+
| `⟨pad⟩` | part of the single-message frame |
|
|
66
|
+
|
|
67
|
+
## Measured accuracy
|
|
68
|
+
|
|
69
|
+
These results compare `ctok` with recorded `count_tokens` responses:
|
|
70
|
+
|
|
71
|
+
| corpus | role | v3 exact | v4.7 exact |
|
|
72
|
+
|---|---|---:|---:|
|
|
73
|
+
| Goldfish, 350 languages and 350,000 rows | mining | 350,000 | 350,000 |
|
|
74
|
+
| MultiPL-E, 22 programming languages | held out | 22 | 22 |
|
|
75
|
+
| Rosetta Code, 1,741 documents | mining | 1,741 | 1,741 |
|
|
76
|
+
| Rosetta Code, separate 250 documents | held out | 250 | 250 |
|
|
77
|
+
| UDHR, 501 languages | mining (in-sample since 2026-08-12) | 501 | 501 |
|
|
78
|
+
|
|
79
|
+
v5 is omitted from the table because its deviation from recorded counts matches v4.7's on every
|
|
80
|
+
gated document. Separate API tests cover its message-frame rules.
|
|
81
|
+
|
|
82
|
+
The stored measurement sets contain no under-counts: 0 of 1,664,940 v3 texts and 0 of 1,722,961
|
|
83
|
+
v4.7 texts. This does not guarantee the result for arbitrary input. Goldfish, the main Rosetta
|
|
84
|
+
sample, and UDHR informed piece selection. MultiPL-E and the separate Rosetta sample did not.
|
|
85
|
+
|
|
86
|
+
Run the public gates with:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run pytest
|
|
90
|
+
uv run python tests/gates.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Vocabulary evidence
|
|
94
|
+
|
|
95
|
+
The two vocabulary files contain 48,645 v3 pieces and 15,240 v4.7 pieces. Every entry has a fixed
|
|
96
|
+
membership witness or is one of the structural marker atoms checked by the test suite.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from ctok import pieces, witness
|
|
100
|
+
|
|
101
|
+
len(pieces("4.7")) # 15240
|
|
102
|
+
witness("⟨bow⟩the⟨eow⟩", "4.7")
|
|
103
|
+
# {'probe': 'the', 'raw': 12, 'kind': 'raw'}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A witness says that one marked piece costs one token in a calibrated probe. It does not prove the
|
|
107
|
+
encoder rewrite or resolve ties between equal-cost tilings. `tests/test_witness.py` checks every
|
|
108
|
+
published witness and requires complete witnessed-or-special coverage.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -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]")
|