token-distiller 0.1.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.
- token_distiller-0.1.0/.claude-plugin/marketplace.json +18 -0
- token_distiller-0.1.0/.github/workflows/ci.yml +61 -0
- token_distiller-0.1.0/.gitignore +16 -0
- token_distiller-0.1.0/LICENSE +133 -0
- token_distiller-0.1.0/PKG-INFO +257 -0
- token_distiller-0.1.0/README.md +220 -0
- token_distiller-0.1.0/plugin/.claude-plugin/plugin.json +12 -0
- token_distiller-0.1.0/plugin/commands/distill.md +10 -0
- token_distiller-0.1.0/plugin/hooks/hooks.json +28 -0
- token_distiller-0.1.0/plugin/skills/token-distiller/SKILL.md +50 -0
- token_distiller-0.1.0/pyproject.toml +60 -0
- token_distiller-0.1.0/src/token_distiller/__init__.py +1 -0
- token_distiller-0.1.0/src/token_distiller/activity.py +105 -0
- token_distiller-0.1.0/src/token_distiller/bash_compress.py +250 -0
- token_distiller-0.1.0/src/token_distiller/boilerplate.py +67 -0
- token_distiller-0.1.0/src/token_distiller/cache.py +186 -0
- token_distiller-0.1.0/src/token_distiller/chunker.py +79 -0
- token_distiller-0.1.0/src/token_distiller/cli.py +623 -0
- token_distiller-0.1.0/src/token_distiller/config.py +100 -0
- token_distiller-0.1.0/src/token_distiller/embeddings.py +26 -0
- token_distiller-0.1.0/src/token_distiller/exact_tokens.py +23 -0
- token_distiller-0.1.0/src/token_distiller/hook_installer.py +86 -0
- token_distiller-0.1.0/src/token_distiller/image_ingest.py +17 -0
- token_distiller-0.1.0/src/token_distiller/index_store.py +102 -0
- token_distiller-0.1.0/src/token_distiller/models.py +112 -0
- token_distiller-0.1.0/src/token_distiller/ocr.py +110 -0
- token_distiller-0.1.0/src/token_distiller/pdf_extract.py +107 -0
- token_distiller-0.1.0/src/token_distiller/pipeline.py +218 -0
- token_distiller-0.1.0/src/token_distiller/repo_pack.py +148 -0
- token_distiller-0.1.0/src/token_distiller/retrieval.py +45 -0
- token_distiller-0.1.0/src/token_distiller/session_audit.py +89 -0
- token_distiller-0.1.0/src/token_distiller/storage.py +111 -0
- token_distiller-0.1.0/src/token_distiller/tokens.py +51 -0
- token_distiller-0.1.0/src/token_distiller/vision_fallback.py +61 -0
- token_distiller-0.1.0/tests/conftest.py +258 -0
- token_distiller-0.1.0/tests/test_activity.py +65 -0
- token_distiller-0.1.0/tests/test_bash_compress.py +186 -0
- token_distiller-0.1.0/tests/test_benchmark.py +141 -0
- token_distiller-0.1.0/tests/test_boilerplate.py +74 -0
- token_distiller-0.1.0/tests/test_cache.py +110 -0
- token_distiller-0.1.0/tests/test_exact_tokens.py +86 -0
- token_distiller-0.1.0/tests/test_hook_installer.py +103 -0
- token_distiller-0.1.0/tests/test_hook_read.py +166 -0
- token_distiller-0.1.0/tests/test_models.py +64 -0
- token_distiller-0.1.0/tests/test_pipeline.py +305 -0
- token_distiller-0.1.0/tests/test_repo_pack.py +103 -0
- token_distiller-0.1.0/tests/test_retrieval.py +89 -0
- token_distiller-0.1.0/tests/test_session_audit.py +93 -0
- token_distiller-0.1.0/tests/test_tokens.py +58 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "token-distiller",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Sri PriYa N. Chen",
|
|
5
|
+
"email": "p.chen@NeoclassicalPopArt.com",
|
|
6
|
+
"url": "https://github.com/priyanchen/token-distiller"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "token-distiller",
|
|
11
|
+
"source": "./plugin",
|
|
12
|
+
"description": "Distills PDFs, photos, and repos into token-efficient context for LLM agents. Local OCR reads embedded diagrams, not just text layers; content-hash caching collapses re-reads; BM25 retrieval bounds huge documents. Nothing is discarded — anything shortened expands back in full. 33x on a 25-page PDF, 97% on pytest output.",
|
|
13
|
+
"version": "0.1.0",
|
|
14
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
15
|
+
"keywords": ["token", "context", "pdf", "ocr", "retrieval", "rag"]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
# poppler backs pdf2image's page rasterization; tesseract is the OCR engine; the
|
|
24
|
+
# DejaVu fonts are needed by the fixtures that render real text for OCR to read.
|
|
25
|
+
- name: Install system dependencies
|
|
26
|
+
run: |
|
|
27
|
+
sudo apt-get update
|
|
28
|
+
sudo apt-get install -y poppler-utils tesseract-ocr fonts-dejavu-core
|
|
29
|
+
|
|
30
|
+
- name: Install package
|
|
31
|
+
run: pip install ".[dev]"
|
|
32
|
+
|
|
33
|
+
- name: Verify OCR engine is reachable
|
|
34
|
+
run: python -c "import pytesseract; print(pytesseract.get_tesseract_version())"
|
|
35
|
+
|
|
36
|
+
- name: Default suite
|
|
37
|
+
run: pytest -q
|
|
38
|
+
|
|
39
|
+
# Deselected by default because they drive real Tesseract; run explicitly here so the
|
|
40
|
+
# compression claims in the README and the figure-reading path stay covered.
|
|
41
|
+
- name: OCR-marked suite
|
|
42
|
+
run: pytest -q -m ocr
|
|
43
|
+
|
|
44
|
+
- name: Smoke-test the CLI end to end
|
|
45
|
+
run: |
|
|
46
|
+
distill --help
|
|
47
|
+
python - <<'PY'
|
|
48
|
+
import subprocess, sys
|
|
49
|
+
sys.path.insert(0, "tests")
|
|
50
|
+
from conftest import make_pdf
|
|
51
|
+
make_pdf("/tmp/ci_smoke.pdf", [["A page of native text long enough to skip OCR entirely."]])
|
|
52
|
+
out = subprocess.run(
|
|
53
|
+
["distill", "file", "/tmp/ci_smoke.pdf", "--json"],
|
|
54
|
+
capture_output=True, text=True, check=True,
|
|
55
|
+
).stdout
|
|
56
|
+
import json
|
|
57
|
+
d = json.loads(out)
|
|
58
|
+
assert d["method_counts"] == {"native_text": 1}, d["method_counts"]
|
|
59
|
+
assert d["compression_ratio"] > 1.0, d["compression_ratio"]
|
|
60
|
+
print("CLI smoke test OK:", d["raw_tokens_est"], "->", d["distilled_tokens_est"])
|
|
61
|
+
PY
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Copyright (C) 2026 Sri PriYa N. Chen
|
|
2
|
+
|
|
3
|
+
Required Notice: Copyright Sri PriYa N. Chen
|
|
4
|
+
|
|
5
|
+
# PolyForm Noncommercial License 1.0.0
|
|
6
|
+
|
|
7
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
8
|
+
|
|
9
|
+
## Acceptance
|
|
10
|
+
|
|
11
|
+
In order to get any license under these terms, you must agree to them
|
|
12
|
+
as both strict obligations and conditions to all your licenses.
|
|
13
|
+
|
|
14
|
+
## Copyright License
|
|
15
|
+
|
|
16
|
+
The licensor grants you a copyright license for the software to do
|
|
17
|
+
everything you might do with the software that would otherwise infringe
|
|
18
|
+
the licensor's copyright in it for any permitted purpose. However, you
|
|
19
|
+
may only distribute the software according to Distribution License and
|
|
20
|
+
make changes or new works based on the software according to Changes and
|
|
21
|
+
New Works License.
|
|
22
|
+
|
|
23
|
+
## Distribution License
|
|
24
|
+
|
|
25
|
+
The licensor grants you an additional copyright license to distribute
|
|
26
|
+
copies of the software. Your license to distribute covers distributing
|
|
27
|
+
the software with changes and new works permitted by Changes and New
|
|
28
|
+
Works License.
|
|
29
|
+
|
|
30
|
+
## Notices
|
|
31
|
+
|
|
32
|
+
You must ensure that anyone who gets a copy of any part of the software
|
|
33
|
+
from you also gets a copy of these terms or the URL for them above, as
|
|
34
|
+
well as copies of any plain-text lines beginning with `Required Notice:`
|
|
35
|
+
that the licensor provided with the software.
|
|
36
|
+
|
|
37
|
+
## Changes and New Works License
|
|
38
|
+
|
|
39
|
+
The licensor grants you an additional copyright license to make changes
|
|
40
|
+
and new works based on the software for any permitted purpose.
|
|
41
|
+
|
|
42
|
+
## Patent License
|
|
43
|
+
|
|
44
|
+
The licensor grants you a patent license for the software that covers
|
|
45
|
+
patent claims the licensor can license, or becomes able to license, that
|
|
46
|
+
you would infringe by using the software.
|
|
47
|
+
|
|
48
|
+
## Noncommercial Purposes
|
|
49
|
+
|
|
50
|
+
Any noncommercial purpose is a permitted purpose.
|
|
51
|
+
|
|
52
|
+
## Personal Uses
|
|
53
|
+
|
|
54
|
+
Personal use for research, experiment, and testing for the benefit of
|
|
55
|
+
public knowledge, personal study, private entertainment, hobby projects,
|
|
56
|
+
amateur pursuits, or religious observance, without any anticipated
|
|
57
|
+
commercial application, is use for a permitted purpose.
|
|
58
|
+
|
|
59
|
+
## Noncommercial Organizations
|
|
60
|
+
|
|
61
|
+
Use by any charitable organization, educational institution, public
|
|
62
|
+
research organization, public safety or health organization,
|
|
63
|
+
environmental protection organization, or government institution is use
|
|
64
|
+
for a permitted purpose regardless of the source of funding or
|
|
65
|
+
obligations resulting from the funding.
|
|
66
|
+
|
|
67
|
+
## Fair Use
|
|
68
|
+
|
|
69
|
+
You may have "fair use" rights for the software under the law. These
|
|
70
|
+
terms do not limit them.
|
|
71
|
+
|
|
72
|
+
## No Other Rights
|
|
73
|
+
|
|
74
|
+
These terms do not allow you to sublicense or transfer any of your
|
|
75
|
+
licenses to anyone else, or prevent the licensor from granting licenses
|
|
76
|
+
to anyone else. These terms do not imply any other licenses.
|
|
77
|
+
|
|
78
|
+
## Patent Defense
|
|
79
|
+
|
|
80
|
+
If you make any written claim that the software infringes or contributes
|
|
81
|
+
to infringement of any patent, your patent license for the software
|
|
82
|
+
granted under these terms ends immediately. If your company makes such a
|
|
83
|
+
claim, your patent license ends immediately for work on behalf of your
|
|
84
|
+
company.
|
|
85
|
+
|
|
86
|
+
## Violations
|
|
87
|
+
|
|
88
|
+
The first time you are notified in writing that you have violated any of
|
|
89
|
+
these terms, or done anything with the software not covered by your
|
|
90
|
+
licenses, your licenses can nonetheless continue if you come into full
|
|
91
|
+
compliance with these terms, and take practical steps to correct past
|
|
92
|
+
violations, within 32 days of receiving notice. Otherwise, all your
|
|
93
|
+
licenses end immediately.
|
|
94
|
+
|
|
95
|
+
## No Liability
|
|
96
|
+
|
|
97
|
+
As far as the law allows, the software comes as is, without any warranty
|
|
98
|
+
or condition, and the licensor will not be liable to you for any damages
|
|
99
|
+
arising out of these terms or the use or nature of the software, under
|
|
100
|
+
any kind of legal claim.
|
|
101
|
+
|
|
102
|
+
## Definitions
|
|
103
|
+
|
|
104
|
+
The **licensor** is the individual or entity offering these terms, and
|
|
105
|
+
the **software** is the software the licensor makes available under
|
|
106
|
+
these terms.
|
|
107
|
+
|
|
108
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
109
|
+
|
|
110
|
+
**Your company** is any legal entity, sole proprietorship, or other kind
|
|
111
|
+
of organization that you work for, plus all organizations that have
|
|
112
|
+
control over, are under the control of, or are under common control with
|
|
113
|
+
that organization. **Control** means ownership of substantially all the
|
|
114
|
+
assets of an entity, or the power to direct its management and policies
|
|
115
|
+
by vote, contract, or otherwise. Control can be direct or indirect.
|
|
116
|
+
|
|
117
|
+
**Your licenses** are all the licenses granted to you for the software
|
|
118
|
+
under these terms.
|
|
119
|
+
|
|
120
|
+
**Use** means anything you do with the software requiring one of your
|
|
121
|
+
licenses.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Commercial Licensing
|
|
126
|
+
|
|
127
|
+
Personal, private, and noncommercial use (including by individuals,
|
|
128
|
+
students, hobbyists, and noncommercial/nonprofit/educational
|
|
129
|
+
organizations) is free under the terms above — no license purchase
|
|
130
|
+
needed.
|
|
131
|
+
|
|
132
|
+
For commercial use, please contact Sri PriYa N. Chen:
|
|
133
|
+
p.chen@NeoclassicalPopArt.com
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: token-distiller
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Distills PDFs, photos, and repos into token-efficient context for LLM agents. Local OCR reads embedded diagrams, not just text layers; content-hash caching collapses re-reads; BM25 retrieval bounds huge documents. Nothing is discarded — anything shortened expands back in full. 33x on a 25-page PDF, 97% on pytest output.
|
|
5
|
+
Project-URL: Homepage, https://github.com/priyanchen/token-distiller
|
|
6
|
+
Project-URL: Repository, https://github.com/priyanchen/token-distiller
|
|
7
|
+
Project-URL: Issues, https://github.com/priyanchen/token-distiller/issues
|
|
8
|
+
Author: Sri PriYa N. Chen
|
|
9
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: anthropic,claude,context,llm,ocr,pdf,rag,retrieval,tokens
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Text Processing :: General
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: anthropic>=0.40
|
|
23
|
+
Requires-Dist: pathspec>=0.12
|
|
24
|
+
Requires-Dist: pdf2image>=1.17
|
|
25
|
+
Requires-Dist: pdfplumber>=0.11
|
|
26
|
+
Requires-Dist: pillow-heif>=0.15
|
|
27
|
+
Requires-Dist: pillow>=10.0
|
|
28
|
+
Requires-Dist: pypdf>=4.0
|
|
29
|
+
Requires-Dist: pytesseract>=0.3.10
|
|
30
|
+
Requires-Dist: rank-bm25>=0.2
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest-mock; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
34
|
+
Provides-Extra: rag-semantic
|
|
35
|
+
Requires-Dist: voyageai>=0.3; extra == 'rag-semantic'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Token Distiller
|
|
39
|
+
|
|
40
|
+
Distills PDFs and photos into token-efficient text before they enter an LLM's context, packs repos Repomix-style, indexes ingested content for retrieval instead of raw dumping, and tracks session activity mode to bias what a context audit flags.
|
|
41
|
+
|
|
42
|
+
Pure Python. No shell scripts, no Node/TypeScript.
|
|
43
|
+
|
|
44
|
+
## Setup
|
|
45
|
+
|
|
46
|
+
### From PyPI
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
brew install poppler # required by pdf2image for PDF page rasterization
|
|
50
|
+
pip install token-distiller
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### As a Claude Code plugin
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
/plugin marketplace add priyanchen/token-distiller
|
|
57
|
+
/plugin install token-distiller@token-distiller
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This wires the hook configuration only. Claude Code has no mechanism to run setup
|
|
61
|
+
commands after installing a plugin, so it can't provision a Python environment for you —
|
|
62
|
+
separately run `pip install token-distiller` so the `distill` binary the hook invokes is
|
|
63
|
+
on `PATH`. Skipping that step doesn't break anything: the hook fails open, so `Read` on a
|
|
64
|
+
PDF passes through unchanged rather than erroring. It just means nothing gets distilled
|
|
65
|
+
until the binary is actually installed.
|
|
66
|
+
|
|
67
|
+
### From source
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
brew install poppler # required by pdf2image for PDF page rasterization
|
|
71
|
+
python3 -m venv .venv
|
|
72
|
+
source .venv/bin/activate
|
|
73
|
+
pip install .
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use a regular install, not `pip install -e .`. On this machine the editable install's
|
|
77
|
+
`_editable_impl_token_distiller.pth` was silently ignored by `site.py` (the file was
|
|
78
|
+
readable and its contents correct, and a byte-identical copy under a different filename
|
|
79
|
+
*was* honored — root cause unresolved), leaving `token_distiller` unimportable. A regular
|
|
80
|
+
install copies the package into `site-packages` and avoids the `.pth` indirection
|
|
81
|
+
entirely. Re-run `pip install .` after editing source.
|
|
82
|
+
|
|
83
|
+
Optional: set **`TOKEN_DISTILLER_ANTHROPIC_API_KEY`** to enable vision-model fallback for
|
|
84
|
+
figures and pages OCR can't read. Prefer that name over plain `ANTHROPIC_API_KEY` — a host
|
|
85
|
+
agent (Claude Code included) may also read `ANTHROPIC_API_KEY` and switch from subscription
|
|
86
|
+
authentication to per-token API billing if it finds one. The scoped name is checked first,
|
|
87
|
+
so this tool gets a key without changing anyone else's auth.
|
|
88
|
+
|
|
89
|
+
Set `VOYAGE_API_KEY` and `pip install ".[rag-semantic]"` to enable semantic (embedding)
|
|
90
|
+
retrieval on top of the default BM25 keyword index.
|
|
91
|
+
|
|
92
|
+
## CLI
|
|
93
|
+
|
|
94
|
+
Placeholders are uppercase; substitute your own path, directory, or question.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
distill file PATH # distill one PDF/photo
|
|
98
|
+
distill scan DIR # batch distill a directory
|
|
99
|
+
distill repo DIR # Repomix-style repo pack
|
|
100
|
+
distill index DIR # build a retrieval index
|
|
101
|
+
distill query "QUESTION" # query the index
|
|
102
|
+
distill mode # current session activity mode
|
|
103
|
+
distill audit PATH # CLAUDE.md/MEMORY.md structural audit (defaults to .)
|
|
104
|
+
distill report # cumulative token/savings report
|
|
105
|
+
distill expand HANDLE # full distilled text for a handle (--list to browse)
|
|
106
|
+
distill compress # compress verbose command output read from stdin
|
|
107
|
+
distill install-hook # wire the PreToolUse Read-interception hook into a project
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Every command that distills a PDF accepts `--no-figures` to skip reading embedded figures,
|
|
111
|
+
and `--no-vision` to stay on local OCR only. `distill file` additionally accepts
|
|
112
|
+
`--accurate-tokens`, which calls Anthropic's `count_tokens` endpoint to report the real
|
|
113
|
+
tokenizer count for the distilled output next to the chars/4 estimate. It's opt-in because
|
|
114
|
+
it needs an API key (`TOKEN_DISTILLER_ANTHROPIC_API_KEY` or `ANTHROPIC_API_KEY`) and a
|
|
115
|
+
network round trip — the call itself is free and doesn't count against message-creation
|
|
116
|
+
rate limits, but the estimate is what every other command uses by default.
|
|
117
|
+
|
|
118
|
+
## Compressing command output
|
|
119
|
+
|
|
120
|
+
Verbose CLI output is the one context cost the document pipeline doesn't touch. Pipe it in:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pytest -q | distill compress --stats
|
|
124
|
+
git status | distill compress
|
|
125
|
+
npm install | distill compress
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Measured on this repo's own output: a 169-test `pytest` run goes **289 → 8 tokens (97%)**,
|
|
129
|
+
keeping the failure list, the first assertion detail, and the summary line while dropping
|
|
130
|
+
the wall of dots. Plain `git status` goes **153 → 58 tokens (62%)**, grouped by state.
|
|
131
|
+
|
|
132
|
+
Two properties worth knowing:
|
|
133
|
+
|
|
134
|
+
- **It never inflates.** `git status --porcelain` is already denser than any per-state
|
|
135
|
+
summary of it, so when compression would produce more text than it consumed, the original
|
|
136
|
+
is returned unchanged.
|
|
137
|
+
- **It never executes anything.** `distill compress` reads stdin and writes stdout. The
|
|
138
|
+
alternative — a hook that rewrites your Bash command to route it through a wrapper —
|
|
139
|
+
means building shell strings out of model-supplied input, which is exactly where command
|
|
140
|
+
injection lives. Piping output that you already ran has no such surface. Automatic
|
|
141
|
+
interception is deliberately not implemented for that reason.
|
|
142
|
+
|
|
143
|
+
## Verified in a live session
|
|
144
|
+
|
|
145
|
+
The `Read` interception was checked against a real Claude Code session, not only against
|
|
146
|
+
synthetic hook payloads. A one-page PDF holding a canary string was read through the hook:
|
|
147
|
+
the session reported `2,572 → 38 tokens` and the model quoted the canary correctly,
|
|
148
|
+
confirming the substituted text is what actually reaches it. A second run asking for one
|
|
149
|
+
specific page of a four-page PDF passed straight through to the native ranged read with no
|
|
150
|
+
hook note, confirming a page range is never answered with whole-document content.
|
|
151
|
+
|
|
152
|
+
## How it avoids losing anything
|
|
153
|
+
|
|
154
|
+
Every distillation is stored whole, keyed by a SHA-256 of the file's bytes, before any
|
|
155
|
+
shortening happens. Anything the hook shortens carries a handle, and `distill expand
|
|
156
|
+
<handle>` returns the complete text. Concretely:
|
|
157
|
+
|
|
158
|
+
- **Re-reads collapse.** Reading the same unchanged file twice in a session returns a
|
|
159
|
+
one-line pointer the second time instead of the whole document again (7,634 → 287
|
|
160
|
+
chars measured). Edit the file and the hash changes, so it is re-distilled in full —
|
|
161
|
+
a stale cache can never be served.
|
|
162
|
+
- **Repeated page boilerplate is restated once.** A line must appear on ≥80% of pages to
|
|
163
|
+
qualify, so a running copyright footer (25/25 pages) collapses while a structural
|
|
164
|
+
marker like `Example:` (15/25) is left alone. Collapsed lines are listed at the top of
|
|
165
|
+
the output.
|
|
166
|
+
- **Large documents defer rather than truncate.** Past `TOKEN_DISTILLER_LARGE_DOC_TOKENS`
|
|
167
|
+
(default 8000) the hook returns a head plus retrieval instructions. Nothing is
|
|
168
|
+
discarded — `distill expand` or `distill index` + `distill query` reach the rest.
|
|
169
|
+
- **Embedded figures are read, not skipped.** Native-text extraction sees a page's text
|
|
170
|
+
layer only, so a diagram sitting beside that text would otherwise go unread. Each
|
|
171
|
+
embedded figure is cropped out by its bounding box and put through the same OCR →
|
|
172
|
+
vision chain used for scanned pages, then written into the output labelled
|
|
173
|
+
`[figure N on page M]`. Cropping to the figure matters: the surrounding prose is
|
|
174
|
+
already captured losslessly, so including it would pay vision tokens to re-read text
|
|
175
|
+
we already have. Hairline rules and background strips are skipped via
|
|
176
|
+
`TOKEN_DISTILLER_FIGURE_MIN_SIDE_PT` (default 48pt). `--no-figures` turns it off.
|
|
177
|
+
- **A weak OCR pass is retried on a preprocessed copy.** Figures cropped from a PDF are
|
|
178
|
+
often below the ~300 DPI Tesseract expects and sit on a tinted panel, which is exactly
|
|
179
|
+
when raw OCR returns nothing. A second attempt greyscales, stretches contrast, upscales
|
|
180
|
+
small crops, and binarizes with an Otsu threshold. It is a retry rather than the default
|
|
181
|
+
because binarizing can destroy anti-aliased text that read fine raw, so the preprocessed
|
|
182
|
+
pass has to win on word count and confidence to be used. Measured on a real 765-page
|
|
183
|
+
book: of 13 figures that raw OCR could not read at all, **10 were recovered** — one at
|
|
184
|
+
confidence 96, transcribing `Market Research / Competitive Analysis / SWOT Analysis /
|
|
185
|
+
Goal Setting / Resource Allocation`. That book ends at 60 of 63 figures read, with no
|
|
186
|
+
API key.
|
|
187
|
+
- **Figures that still can't be read are flagged, never dropped silently.** A purely
|
|
188
|
+
graphical diagram with no legible labels yields nothing from OCR, and without
|
|
189
|
+
`ANTHROPIC_API_KEY` there's no vision fallback to describe it. Those pages stay in
|
|
190
|
+
`pages_with_uncaptured_images()` and surface as one compact note — never one line per
|
|
191
|
+
page — in `distill file`, `--json`, and the hook-read response. Set `ANTHROPIC_API_KEY`
|
|
192
|
+
to close the remainder.
|
|
193
|
+
|
|
194
|
+
## What leaves your machine
|
|
195
|
+
|
|
196
|
+
With no API keys set, nothing does. OCR runs locally through Tesseract and retrieval runs
|
|
197
|
+
locally through BM25, so the default configuration makes **zero network calls**.
|
|
198
|
+
|
|
199
|
+
Two features are opt-in, and they send different things to different companies:
|
|
200
|
+
|
|
201
|
+
| Enabled by | Goes to | What is sent |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `TOKEN_DISTILLER_ANTHROPIC_API_KEY` (or `ANTHROPIC_API_KEY`) | Anthropic | A PNG of a **single cropped figure**, plus a fixed prompt. No source code, no file paths, no surrounding page text. |
|
|
204
|
+
| `VOYAGE_API_KEY` + `pip install ".[rag-semantic]"` | Voyage AI | **Chunk text** from whatever you indexed. If you indexed a repo pack, that includes your source code. |
|
|
205
|
+
|
|
206
|
+
The Voyage path is the one to think hardest about — it is a separate company under separate
|
|
207
|
+
terms, and it is the only path that can transmit code. It is off unless you both install the
|
|
208
|
+
extra and set the key; BM25 retrieval works without it.
|
|
209
|
+
|
|
210
|
+
On the Anthropic path, [their commercial terms](https://www.anthropic.com/legal/commercial-terms)
|
|
211
|
+
state that the customer "retains all rights to its Inputs", "owns its Outputs", that
|
|
212
|
+
Anthropic "disclaims any rights it receives to the Customer Content", and that Anthropic
|
|
213
|
+
"may not train models on Customer Content from Services". Read them yourself rather than
|
|
214
|
+
relying on this summary; this is not legal advice.
|
|
215
|
+
|
|
216
|
+
Turn figure reading off entirely with `--no-figures` or
|
|
217
|
+
`TOKEN_DISTILLER_DESCRIBE_FIGURES=0`, and it will never reach for the vision model.
|
|
218
|
+
|
|
219
|
+
Toggle any of it off: `TOKEN_DISTILLER_CACHE=0`, `TOKEN_DISTILLER_REREAD_COLLAPSE=0`,
|
|
220
|
+
`TOKEN_DISTILLER_BOILERPLATE=0`.
|
|
221
|
+
|
|
222
|
+
## A note on the numbers
|
|
223
|
+
|
|
224
|
+
`raw_tokens_est` models what the **host** pays to ingest the file, not what its text
|
|
225
|
+
alone would cost. Reading a PDF natively renders each page to an image and bills those
|
|
226
|
+
pixels on top of the text, so a 25-page text PDF costs ~60,000 tokens to read raw but
|
|
227
|
+
~1,800 distilled (33x). Scoring it as text-only would have reported a meaningless 1.0x.
|
|
228
|
+
|
|
229
|
+
Both figures are estimates (chars/4 for text, Anthropic's published pixel formula for
|
|
230
|
+
images) — good enough to compare methods, not exact. `distill file --accurate-tokens`
|
|
231
|
+
swaps the distilled-side estimate for a real count from Anthropic's tokenizer, to check
|
|
232
|
+
the estimate rather than trust it.
|
|
233
|
+
|
|
234
|
+
That 33x describes a *sparse* page, where a fixed per-page rendering cost dominates a
|
|
235
|
+
small amount of actual text — it is not a document-size-independent multiplier. A
|
|
236
|
+
densely-written page compresses by far less through this mechanism alone, because the
|
|
237
|
+
distilled side scales with real content: measured on a 765-page, prose-dense book, the
|
|
238
|
+
whole-document ratio was 4.77x (1,567,889 raw → 328,589 distilled), not 33x. That is
|
|
239
|
+
expected, not a regression — a page's text cannot be compressed below its own token
|
|
240
|
+
count by an extraction step that isn't lossy.
|
|
241
|
+
|
|
242
|
+
For a document that size, the number that actually matters is not the whole-document
|
|
243
|
+
ratio anyway. 328,589 distilled tokens is well past `TOKEN_DISTILLER_LARGE_DOC_TOKENS`
|
|
244
|
+
(default 8000), so the hook's large-document deferral fires: a live session reading that
|
|
245
|
+
765-page file through the hook receives a head plus a `distill index` / `distill query`
|
|
246
|
+
pointer, measured at ~1,675 tokens — roughly **940x** against the 1,567,889 raw cost.
|
|
247
|
+
(The payload embeds the file's absolute path, so the exact token count shifts a little
|
|
248
|
+
with where the file lives.)
|
|
249
|
+
For large documents, the deferral-and-retrieval path is where the real savings come
|
|
250
|
+
from; the raw-vs-native-text mechanism the 33x figure describes matters most for
|
|
251
|
+
documents small enough to be read in full.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
**PolyForm Noncommercial 1.0.0.** Free for personal, private, and noncommercial use —
|
|
256
|
+
no license purchase needed. Commercial use requires contacting Sri PriYa N. Chen
|
|
257
|
+
(p.chen@NeoclassicalPopArt.com) for a commercial license.
|