pixelbudget 0.3.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.
Files changed (64) hide show
  1. pixelbudget-0.3.0/.github/ISSUE_TEMPLATE/bug.yml +22 -0
  2. pixelbudget-0.3.0/.github/ISSUE_TEMPLATE/question.yml +11 -0
  3. pixelbudget-0.3.0/.github/workflows/ci.yml +20 -0
  4. pixelbudget-0.3.0/.gitignore +43 -0
  5. pixelbudget-0.3.0/CHANGELOG.md +38 -0
  6. pixelbudget-0.3.0/LICENSE +21 -0
  7. pixelbudget-0.3.0/PKG-INFO +313 -0
  8. pixelbudget-0.3.0/README.ko.md +296 -0
  9. pixelbudget-0.3.0/README.md +294 -0
  10. pixelbudget-0.3.0/SECURITY.md +7 -0
  11. pixelbudget-0.3.0/docs/how-it-works.md +46 -0
  12. pixelbudget-0.3.0/docs/images/01-report.ko.png +0 -0
  13. pixelbudget-0.3.0/docs/images/01-report.png +0 -0
  14. pixelbudget-0.3.0/docs/images/02-slice.ko.png +0 -0
  15. pixelbudget-0.3.0/docs/images/02-slice.png +0 -0
  16. pixelbudget-0.3.0/docs/images/03-demo.ko.png +0 -0
  17. pixelbudget-0.3.0/docs/images/03-demo.png +0 -0
  18. pixelbudget-0.3.0/docs/images/04-estimate-size.ko.png +0 -0
  19. pixelbudget-0.3.0/docs/images/04-estimate-size.png +0 -0
  20. pixelbudget-0.3.0/docs/images/compare.png +0 -0
  21. pixelbudget-0.3.0/docs/model-table.md +56 -0
  22. pixelbudget-0.3.0/examples/fit_and_slice.py +12 -0
  23. pixelbudget-0.3.0/examples/make_samples.py +18 -0
  24. pixelbudget-0.3.0/examples/quickstart.py +11 -0
  25. pixelbudget-0.3.0/pyproject.toml +38 -0
  26. pixelbudget-0.3.0/src/pixelbudget/__init__.py +45 -0
  27. pixelbudget-0.3.0/src/pixelbudget/__main__.py +5 -0
  28. pixelbudget-0.3.0/src/pixelbudget/cli.py +376 -0
  29. pixelbudget-0.3.0/src/pixelbudget/data/models.json +466 -0
  30. pixelbudget-0.3.0/src/pixelbudget/demo.py +45 -0
  31. pixelbudget-0.3.0/src/pixelbudget/errors.py +71 -0
  32. pixelbudget-0.3.0/src/pixelbudget/estimate.py +73 -0
  33. pixelbudget-0.3.0/src/pixelbudget/fit.py +110 -0
  34. pixelbudget-0.3.0/src/pixelbudget/i18n.py +43 -0
  35. pixelbudget-0.3.0/src/pixelbudget/locales/en.json +127 -0
  36. pixelbudget-0.3.0/src/pixelbudget/locales/ko.json +127 -0
  37. pixelbudget-0.3.0/src/pixelbudget/measure.py +113 -0
  38. pixelbudget-0.3.0/src/pixelbudget/py.typed +0 -0
  39. pixelbudget-0.3.0/src/pixelbudget/registry.py +142 -0
  40. pixelbudget-0.3.0/src/pixelbudget/report.py +88 -0
  41. pixelbudget-0.3.0/src/pixelbudget/rules/__init__.py +13 -0
  42. pixelbudget-0.3.0/src/pixelbudget/rules/fixed.py +9 -0
  43. pixelbudget-0.3.0/src/pixelbudget/rules/patch.py +71 -0
  44. pixelbudget-0.3.0/src/pixelbudget/rules/tile.py +32 -0
  45. pixelbudget-0.3.0/src/pixelbudget/slicing.py +93 -0
  46. pixelbudget-0.3.0/src/pixelbudget/types.py +175 -0
  47. pixelbudget-0.3.0/tests/conftest.py +38 -0
  48. pixelbudget-0.3.0/tests/fixtures/doc_examples.json +21 -0
  49. pixelbudget-0.3.0/tests/test_cli_polish.py +121 -0
  50. pixelbudget-0.3.0/tests/test_discrepancies.py +59 -0
  51. pixelbudget-0.3.0/tests/test_doc_examples.py +35 -0
  52. pixelbudget-0.3.0/tests/test_error_messages.py +212 -0
  53. pixelbudget-0.3.0/tests/test_estimate_size_arg.py +92 -0
  54. pixelbudget-0.3.0/tests/test_fit.py +85 -0
  55. pixelbudget-0.3.0/tests/test_fixed_honesty.py +37 -0
  56. pixelbudget-0.3.0/tests/test_hygiene.py +39 -0
  57. pixelbudget-0.3.0/tests/test_i18n.py +74 -0
  58. pixelbudget-0.3.0/tests/test_measure.py +65 -0
  59. pixelbudget-0.3.0/tests/test_models_file.py +185 -0
  60. pixelbudget-0.3.0/tests/test_report_cli.py +91 -0
  61. pixelbudget-0.3.0/tests/test_singular.py +35 -0
  62. pixelbudget-0.3.0/tests/test_slice.py +59 -0
  63. pixelbudget-0.3.0/tests/test_table_pins.py +123 -0
  64. pixelbudget-0.3.0/uv.lock +173 -0
@@ -0,0 +1,22 @@
1
+ name: 버그 / Bug
2
+ description: README대로 했는데 다르게 동작해요 / Something does not work as the README says
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: what
7
+ attributes:
8
+ label: 무엇이 일어났나요? / What happened?
9
+ description: 실행한 명령과 나온 메시지를 그대로 붙여 주세요. / Paste the command and the output.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: expected
14
+ attributes:
15
+ label: 기대한 결과 / Expected
16
+ - type: input
17
+ id: env
18
+ attributes:
19
+ label: 환경 / Environment
20
+ placeholder: "macOS 15 (M3) · Ubuntu 24.04 · Windows 11 / uv 0.x / mock or real mode"
21
+ validations:
22
+ required: true
@@ -0,0 +1,11 @@
1
+ name: 질문 / Question
2
+ description: 이해가 안 되는 부분이나 더 알고 싶은 점 / Something unclear or worth explaining
3
+ labels: [question]
4
+ body:
5
+ - type: textarea
6
+ id: question
7
+ attributes:
8
+ label: 질문 / Question
9
+ description: README의 어느 부분(절 제목)에 대한 질문인지 알려 주세요. / Which README section is this about?
10
+ validations:
11
+ required: true
@@ -0,0 +1,20 @@
1
+ # Fully offline: no API keys, no network access needed by the tests.
2
+ name: ci
3
+ on:
4
+ push:
5
+ pull_request:
6
+ workflow_dispatch:
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python: ["3.11", "3.12"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v7
16
+ - run: uv python install ${{ matrix.python }}
17
+ - run: uv sync --locked --python ${{ matrix.python }}
18
+ - run: uv run --python ${{ matrix.python }} pytest -q
19
+ - run: uv run --python ${{ matrix.python }} python examples/quickstart.py
20
+ - run: uv build
@@ -0,0 +1,43 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ *.egg-info/
9
+
10
+ # Node
11
+ node_modules/
12
+ dist/
13
+ .next/
14
+
15
+ # 실행 산출물·모델·데이터 캐시
16
+ runs/
17
+ outputs/
18
+ .cache/
19
+ *.safetensors
20
+ *.gguf
21
+ *.bin
22
+
23
+ # 환경·비밀
24
+ .env
25
+ .env.*
26
+ !.env.example
27
+
28
+ # OS·에디터
29
+ .DS_Store
30
+ .vscode/
31
+ .idea/
32
+
33
+ # Flutter·Dart
34
+ .dart_tool/
35
+ build/
36
+ .flutter-plugins
37
+ .flutter-plugins-dependencies
38
+ coverage/
39
+
40
+ # README walkthrough output
41
+ samples/
42
+ pixelbudget-demo/
43
+ out/
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.3.0] - 2026-11-13
8
+
9
+ ### Added
10
+ - `--models-file PATH` (top-level option) and the `PIXELBUDGET_MODELS` environment variable load a JSON file in the bundled table format. A row with the same name replaces the bundled row, a new name is added. The flag wins over the variable; the library follows the variable. Rows are validated like bundled rows; bad files exit 2 (`not valid JSON`, wrong shape, missing fields, duplicate names or aliases).
11
+ - `ModelSpec.origin` (`builtin` or `user`, also in `as_dict()`), an Origin column in `pixelbudget models`, and the `user_model_row` warning on estimates that use a user row.
12
+
13
+ ## [0.2.0] - 2026-11-13
14
+
15
+ ### Added
16
+ - `pixelbudget estimate` accepts a `WxH` size (for example `1920x1080`) in place of an image path and computes the estimate without an image. An existing file or folder with that name always wins; a size below 1 (`0x100`) is rejected with exit 2. `report` is unchanged.
17
+
18
+ ## [0.1.1] - 2026-11-13
19
+
20
+ ### Fixed
21
+ - English single-row report summary and single-piece slice summary now use singular nouns (`1 row`, `1 piece`). Korean output is unchanged.
22
+
23
+ ## [0.1.0] - 2026-11-11
24
+
25
+ ### Added
26
+ - `estimate`, `fit_size`, `fit`, `plan_slices`, `slice_image`, `report` and `measure` for offline image-token work.
27
+ - Model table (`data/models.json`) with Anthropic 28 px patches (standard/high), OpenAI 32 px patches and 512 px tiles, Gemini 2 tiles and Gemini 3 fixed `media_resolution` levels; every row carries `source_url`, `checked` and `confidence`.
28
+ - CLI: `models`, `estimate`, `fit`, `slice`, `report` (CI exit code with `--max-tokens`), `demo`.
29
+ - `report`/`estimate --strict`: exit 1 when a file could not be read as an image. Without it, skipped files are listed and the exit code stays 0.
30
+ - `--format json` for machine-readable output, and `--price MODEL=USD` (price per million tokens is your input; no prices are bundled) for a cost column.
31
+ - `build_report`: one row per image and model, and also lists skipped files (`ReportResult`). `report()`/`build_report()` accept a single path or model string as well as lists.
32
+ - English and Korean CLI output (`--lang`, `PIXELBUDGET_LANG`, locale variables), including error messages. Library exceptions keep their English text and carry `code`, `key` and `params`.
33
+ - Examples `examples/make_samples.py` and `examples/fit_and_slice.py` used by the README walkthrough.
34
+
35
+ [0.3.0]: https://github.com/LeeGyeongPil/pixelbudget/compare/v0.2.0...v0.3.0
36
+ [0.2.0]: https://github.com/LeeGyeongPil/pixelbudget/compare/v0.1.1...v0.2.0
37
+ [0.1.1]: https://github.com/LeeGyeongPil/pixelbudget/compare/v0.1.0...v0.1.1
38
+ [0.1.0]: https://github.com/LeeGyeongPil/pixelbudget/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LeeGyeongPil
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.
@@ -0,0 +1,313 @@
1
+ Metadata-Version: 2.5
2
+ Name: pixelbudget
3
+ Version: 0.3.0
4
+ Summary: Offline image-token estimates, budget-fit resizing and readable slicing for vision LLM inputs (Pillow only).
5
+ Project-URL: Homepage, https://github.com/LeeGyeongPil/pixelbudget
6
+ Project-URL: Issues, https://github.com/LeeGyeongPil/pixelbudget/issues
7
+ Author: LeeGyeongPil
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: anthropic,gemini,image,llm,multimodal,openai,pillow,tokens,vision
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Multimedia :: Graphics
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: pillow<14,>=13.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # pixelbudget
21
+
22
+ **English** | [한국어](https://github.com/LeeGyeongPil/pixelbudget/blob/v0.3.0/README.ko.md)
23
+
24
+ Know what an image costs a vision LLM before you send it, shrink it to a token budget, cut a long screenshot into pieces whose text stays readable, or audit a whole folder and fail CI with an exit code when an image is over budget. It is a small offline Python library plus a CLI that depends only on Pillow, and every rule in its model table carries a source URL, a check date and a confidence level.
25
+
26
+ ![A tall screenshot fitted to one image (left, unreadable) versus the first slice (right, readable)](https://raw.githubusercontent.com/LeeGyeongPil/pixelbudget/v0.3.0/docs/images/compare.png)
27
+
28
+ *Left: the whole 1170x9000 page shrunk to fit Claude's token cap. Right: the first 1170x1036 slice at original scale. The page is synthetic (generated by `pixelbudget demo`).*
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ uv add git+https://github.com/LeeGyeongPil/pixelbudget
34
+ # or: pip install git+https://github.com/LeeGyeongPil/pixelbudget
35
+ ```
36
+
37
+ Python 3.11+ and one runtime dependency (Pillow 13). Not published on PyPI yet. To try it from a clone:
38
+
39
+ ```bash
40
+ git clone https://github.com/LeeGyeongPil/pixelbudget && cd pixelbudget
41
+ uv sync # uv: https://docs.astral.sh/uv/ (curl -LsSf https://astral.sh/uv/install.sh | sh)
42
+ ```
43
+
44
+ No API key and no network access are needed.
45
+
46
+ ## Quick start
47
+
48
+ ```python
49
+ from pixelbudget import estimate, fit_size
50
+
51
+ est = estimate((1920, 1080), "claude-standard")
52
+ print(f"claude-standard 1920x1080 -> {est.sent_size[0]}x{est.sent_size[1]} = {est.tokens} tokens")
53
+
54
+ plan = fit_size((1920, 1080), "claude-standard", budget=800)
55
+ print(f"fit_size budget=800 -> {plan.size[0]}x{plan.size[1]} = {plan.tokens} tokens ({plan.reason})")
56
+ ```
57
+
58
+ This is `examples/quickstart.py`; run it with `uv run python examples/quickstart.py`:
59
+
60
+ ```text
61
+ claude-standard 1920x1080 -> 1456x819 = 1560 tokens
62
+ fit_size budget=800 -> 1046x588 = 798 tokens (resized_to_budget)
63
+ ```
64
+
65
+ A 1920x1080 screenshot is not sent as is: the server shrinks it to 1456x819 before counting. Sending the smaller image yourself costs the same tokens and uploads fewer pixels.
66
+
67
+ ## Core concepts
68
+
69
+ 1. **The pixel grid is what is billed, not the file size.** Each provider turns the image into patches (28 px for Claude, 32 px for newer OpenAI models), 512 px tiles (older OpenAI models, Gemini 2), or a fixed number per image (Gemini 3).
70
+ 2. **The server may shrink the image first.** Past a maximum edge or token count it resizes silently. `estimate` returns `sent_size` and `server_downscaled`, and `report` shows the `Wasted px` you uploaded for nothing.
71
+ 3. **Fit means "within the budget and not downscaled again".** Token counts are a staircase (`ceil`), so `fit` binary-searches the long edge and never upscales.
72
+ 4. **Slice by the token cap, not the edge limit.** A 1170 px wide page is 42 patches wide; 1568 tokens / 42 = 37 rows = 1036 px per piece. 1568 px pieces would be shrunk again.
73
+
74
+ ```mermaid
75
+ flowchart LR
76
+ IN["path, PIL image or (w, h)"] --> M["measure (header only)"]
77
+ M --> R{"model table"}
78
+ R -->|patch| P["28 / 32 px patches"]
79
+ R -->|tile| T["512 / 768 px tiles"]
80
+ R -->|fixed| F["media_resolution"]
81
+ P --> E["estimate"]
82
+ T --> E
83
+ F --> E
84
+ E --> FIT["fit"]
85
+ E --> SL["slice"]
86
+ E --> REP["report"]
87
+ ```
88
+
89
+ Each input is measured, run through the rule of its model, and then used for an estimate, a fit, a slice plan or a report. More detail is in [docs/how-it-works.md](https://github.com/LeeGyeongPil/pixelbudget/blob/v0.3.0/docs/how-it-works.md).
90
+
91
+ ## API
92
+
93
+ All names are importable from `pixelbudget`. Sources (`ImageSource`) are a path, a `PIL.Image.Image` or a `(width, height)` tuple.
94
+
95
+ | Name | What it does | Main arguments | Returns |
96
+ |---|---|---|---|
97
+ | `estimate` | Tokens and the size the server will process | `source`, `model`, `detail=None` | `Estimate` |
98
+ | `fit_size` | Largest size within a budget and the provider limits, no pixels touched | `size`, `model`, `budget=None`, `detail=None` | `FitPlan` |
99
+ | `fit` | `fit_size` plus the resized image (LANCZOS by default) | `image`, `model`, `budget`, `detail`, `resample`, `max_pixels` | `FitResult(image, plan)` |
100
+ | `plan_slices` | Piece boxes, tokens per piece and total | `size`, `model`, `overlap=64`, `detail=None` | `SlicePlan` |
101
+ | `slice_image` | `plan_slices` plus the cropped images | `image`, `model`, `overlap`, `detail`, `max_pixels` | `SliceResult(images, plan)` |
102
+ | `report` / `build_report` | One row per image and model; `build_report` also lists skipped files | `paths`, `models`, `detail`, `max_tokens`, `prices` | `list[ReportRow]` / `ReportResult` |
103
+ | `measure` | Image size from the header (applies EXIF orientation) | `source` | `(w, h)` |
104
+ | `list_models` / `get_model` | Read the model table | `name` | `list[ModelSpec]` / `ModelSpec` |
105
+
106
+ Errors all derive from `PixelBudgetError` and carry a stable `.code`: `UnknownModelError`, `UnsupportedDetailError`, `InvalidBudgetError`, `InvalidOverlapError`, `InvalidSizeError`, `UnreadableImageError`, `ImageTooLargeError`, `ModelTableError`.
107
+
108
+ Warnings are stable codes in `.warnings`: `server_downscaled`, `fixed_per_image`, `slice_increases_tokens`, `many_images`, `approx_rounding`, `approx_rule`, `docs_table_mismatch`, `decompression_guard`, `user_model_row`.
109
+
110
+ Run `pixelbudget models` for the bundled models (16 rows, each with source URL, check date and confidence). Override or extend them with your own file via `--models-file`.
111
+
112
+ ## Usage
113
+
114
+ The examples below use three synthetic images. Create them first (a 1920x1080 frame, a 300x200 icon and a 1170x9000 tall page, drawn with Pillow):
115
+
116
+ ```bash
117
+ uv run python examples/make_samples.py samples
118
+ ```
119
+
120
+ ```text
121
+ wrote 3 images to samples/
122
+ ```
123
+
124
+ ### 1. Audit a folder, fail CI when it is over budget
125
+
126
+ ```bash
127
+ uv run pixelbudget --lang en report samples --model claude-standard --model gpt-4o --max-tokens 1500
128
+ ```
129
+
130
+ ![report table in a terminal](https://raw.githubusercontent.com/LeeGyeongPil/pixelbudget/v0.3.0/docs/images/01-report.png)
131
+
132
+ *Each image and model gets a row. The exit code is 1 when any row is over `--max-tokens` (here `hd.png` on Claude: 1560 > 1500), so the command works as a CI check.*
133
+
134
+ ```text
135
+ Path Model Original Server size Tokens Downscaled Wasted px Over budget
136
+ ----------------- --------------- --------- ----------- ------ ---------- --------- -----------
137
+ samples/hd.png claude-standard 1920x1080 1456x819 1560 yes 881136 yes
138
+ samples/hd.png gpt-4o 1920x1080 1365x768 1105 yes 1025280 no
139
+ samples/small.png claude-standard 300x200 300x200 88 no 0 no
140
+ samples/small.png gpt-4o 300x200 300x200 255 no 0 no
141
+ samples/tall.png claude-standard 1170x9000 204x1568 448 yes 10210128 no
142
+ samples/tall.png gpt-4o 1170x9000 266x2048 765 yes 9985232 no
143
+ Warnings:
144
+ - [server_downscaled] The server will downscale this image first.
145
+ 6 rows, 1 over budget, 0 skipped
146
+ ```
147
+
148
+ `pixelbudget estimate` prints the same table without the budget column (it has no `--price` or `--max-tokens`; passing `--price` is rejected with exit 2). Only `report` shows costs, and only when you pass `--price MODEL=USD` (price per million tokens is your input; no prices are bundled). `--format json` gives machine-readable output. `pixelbudget estimate` also accepts a size such as `1920x1080` in place of a path, so you can get an estimate without an image: `uv run pixelbudget --lang en estimate 1920x1080 --model gpt-4o`. You can mix sizes with files and folders; the path column shows the text you typed. A file or folder with that name wins over the size reading, and a size below 1 (`0x100`) is rejected with exit 2. `report` still takes files and folders only.
149
+
150
+ ![estimate from a WxH size in a terminal](https://raw.githubusercontent.com/LeeGyeongPil/pixelbudget/v0.3.0/docs/images/04-estimate-size.png)
151
+
152
+ *No image needed: `1920x1080` on gpt-4o gives 1105 tokens, and the server shrinks it to 1365x768 first.*
153
+
154
+ ### 2. Fit an image to a token budget
155
+
156
+ ```bash
157
+ uv run pixelbudget --lang en fit samples/hd.png --model claude-standard --budget 800 --out out/hd-fit.png
158
+ ```
159
+
160
+ ```text
161
+ Wrote out/hd-fit.png: 1046x588, 798 tokens (scale 0.545).
162
+ ```
163
+
164
+ In Python: `fitted = fit("samples/hd.png", "claude-standard", budget=800)`; `fitted.image` is a Pillow image, `fitted.plan` the details.
165
+
166
+ Gemini 3 charges per image, so resizing cannot help, and the tool says so instead of writing a file:
167
+
168
+ ```bash
169
+ uv run pixelbudget --lang en fit samples/hd.png --model gemini-3 --budget 500
170
+ ```
171
+
172
+ ```text
173
+ Resizing does not change the cost of gemini-3: tokens are fixed per image (1120). Nothing written.
174
+ To fit the budget of 500, set media_resolution low (280).
175
+ ```
176
+
177
+ ### 3. Slice a long screenshot so text stays readable
178
+
179
+ ```bash
180
+ uv run pixelbudget --lang en slice samples/tall.png --model claude-standard --dry-run
181
+ ```
182
+
183
+ ![slice plan in a terminal](https://raw.githubusercontent.com/LeeGyeongPil/pixelbudget/v0.3.0/docs/images/02-slice.png)
184
+
185
+ *Ten pieces of 1170x1036 at scale 1.0. The whole image would cost 448 tokens but at 204x1568 the text is unreadable; the slices cost 14364 tokens in total. Slicing trades tokens for legibility.*
186
+
187
+ ```text
188
+ 10 pieces of up to 1036px along the vertical axis, overlap 64px, 14364 tokens in total (whole image: 448).
189
+ Piece Box (l,t,r,b) Size Tokens Scale Downscaled
190
+ ----- ---------------- --------- ------ ----- ----------
191
+ 1 0,0,1170,1036 1170x1036 1554 1.000 no
192
+ 2 0,972,1170,2008 1170x1036 1554 1.000 no
193
+ 3 0,1944,1170,2980 1170x1036 1554 1.000 no
194
+ 4 0,2916,1170,3952 1170x1036 1554 1.000 no
195
+ 5 0,3888,1170,4924 1170x1036 1554 1.000 no
196
+ 6 0,4860,1170,5896 1170x1036 1554 1.000 no
197
+ 7 0,5832,1170,6868 1170x1036 1554 1.000 no
198
+ 8 0,6804,1170,7840 1170x1036 1554 1.000 no
199
+ 9 0,7776,1170,8812 1170x1036 1554 1.000 no
200
+ 10 0,8748,1170,9000 1170x252 378 1.000 no
201
+ ```
202
+
203
+ Drop `--dry-run` and add `--out-dir out/pieces` to write the ten files. In Python, `examples/fit_and_slice.py`:
204
+
205
+ ```python
206
+ from pixelbudget import fit, plan_slices, slice_image
207
+
208
+ fitted = fit("samples/tall.png", "claude-standard", budget=800)
209
+ print("fit:", fitted.plan.size, fitted.plan.tokens, "tokens", fitted.plan.reason)
210
+
211
+ plan = plan_slices((1170, 9000), "claude-standard")
212
+ print("pieces:", len(plan.pieces), "of", plan.pieces[0].size, "=", plan.total_tokens, "tokens")
213
+
214
+ result = slice_image("samples/tall.png", "claude-standard")
215
+ print("images ready to send:", len(result.images), result.images[0].size)
216
+ ```
217
+
218
+ ```text
219
+ fit: (204, 1568) 448 tokens resized_to_limits
220
+ pieces: 10 of (1170, 1036) = 14364 tokens
221
+ images ready to send: 10 (1170, 1036)
222
+ ```
223
+
224
+ On a fixed-per-image model slicing only multiplies the cost, and `plan_slices` warns with `slice_increases_tokens`.
225
+
226
+ ### 4. Try it without your own images
227
+
228
+ ```bash
229
+ uv run pixelbudget --lang en demo
230
+ ```
231
+
232
+ ![demo output in a terminal](https://raw.githubusercontent.com/LeeGyeongPil/pixelbudget/v0.3.0/docs/images/03-demo.png)
233
+
234
+ *The demo draws a synthetic tall page, fits it, slices it and writes `fit.png`, `slice-01.png` and `compare.png` (the picture at the top of this README) to `pixelbudget-demo/`.*
235
+
236
+ ```text
237
+ pixelbudget demo - synthetic practice data (a generated tall screenshot, not a real capture)
238
+ Model used: claude-standard
239
+ Synthetic screenshot: 1170x9000
240
+ fit: 204x1568 (448 tokens, scale 0.174) - small text becomes unreadable
241
+ slice: 10 pieces of 1170x1036 at scale 1.0 (14364 tokens in total)
242
+ Trade-off: slicing costs more tokens but keeps the original text height.
243
+ Images written to pixelbudget-demo: fit.png, slice-01.png, compare.png
244
+ ```
245
+
246
+ ## Configuration
247
+
248
+ | Setting | How | Effect |
249
+ |---|---|---|
250
+ | Output language | `--lang en\|ko`, `PIXELBUDGET_LANG`, then `LC_ALL`/`LC_MESSAGES`/`LANG`; English if none | CLI text in English or Korean. Without `--lang` it follows your locale, so a Korean locale prints Korean. |
251
+ | Detail level | `--detail` / `detail=` | The provider's detail setting (for example `low`/`high` on tile models, `media_resolution` on Gemini 3). Defaults come from the model table. |
252
+ | Budget exit code | `report --max-tokens N` | Exit 1 when any row is above N tokens; usage errors exit 2. |
253
+ | Unreadable files | `report`/`estimate --strict` | Exit 1 when a file could not be read as an image. Without it, skipped files are listed and the exit code stays 0. |
254
+ | Decode guard | `--max-pixels` / `max_pixels=` | `fit` and `slice` need pixels; images above this count raise `ImageTooLargeError` (default: Pillow's `MAX_IMAGE_PIXELS`). |
255
+ | Your own model rows | `--models-file PATH`, `PIXELBUDGET_MODELS` | A JSON file in the bundled format (`{"models": [...]}`). A row with the same name replaces the bundled row, a new name is added. User rows show Origin `user` in `pixelbudget models` and carry the `user_model_row` warning. The flag wins over the variable. |
256
+
257
+ The option goes before the subcommand, for example:
258
+
259
+ ```bash
260
+ pixelbudget --models-file my-models.json models
261
+ ```
262
+
263
+ ## Limits (read this before trusting a number)
264
+
265
+ - **Estimates, not invoices.** Counts come from the providers' public documentation, checked on 2026-11-11. They can differ from billed usage. Each row says `documented`, `measured` or `approx`.
266
+ - **OpenAI patch models over the patch budget are `approx`.** The docs say "scale down, round down, recompute" without the exact rounding, so those results carry `approx_rounding`.
267
+ - **Gemini 2 is `approx`.** The AI Studio and Vertex pages describe the rule differently, and forum reports show counts and billed usage disagreeing. Treat it as a rough number (`approx_rule`).
268
+ - **Gemini 3 and later: resizing does not reduce tokens.** Cost depends only on `media_resolution` (low 280, medium 560, high 1120, ultra_high 2240 per image). `fit` leaves the image alone and suggests a level; slicing multiplies the cost.
269
+ - **Docs disagree with themselves in one place.** Claude standard at 2000x1500: the docs table says 1269x952, the reference code on the same site gives 1270x952 (both 1564 tokens). pixelbudget follows the code and warns with `docs_table_mismatch`. Details in [docs/model-table.md](https://github.com/LeeGyeongPil/pixelbudget/blob/v0.3.0/docs/model-table.md).
270
+ - **Cosmetic quirks.** In the Korean README screenshots of `report` and `slice` some columns look shifted because the capture font draws Hangul narrower than two cells; the text output itself is aligned.
271
+ - **Only models with documented limits are in the table.** Models whose limits could not be confirmed are left out (for example `gpt-5.4` has `high` only). Providers change these rules; rows are plain JSON in `src/pixelbudget/data/models.json`. To update a stale row, follow [docs/model-table.md](https://github.com/LeeGyeongPil/pixelbudget/blob/v0.3.0/docs/model-table.md#updating-a-row); the same page lists how each method counts tokens. Do not edit the installed package: override a row with `--models-file` (see Configuration); the numbers, source and confidence of such a row are yours.
272
+ - **Not implemented yet:** scanning JSONL request logs and calibrating against real usage records. No pricing data is bundled.
273
+ - Slicing costs more tokens than a fitted single image. It does not OCR or check that text is actually readable; it only keeps the original scale.
274
+
275
+ Sources: [Anthropic vision](https://platform.claude.com/docs/en/build-with-claude/vision), [OpenAI images and vision](https://developers.openai.com/api/docs/guides/images-vision), [Gemini tokens](https://aistudio.google.com/docs/tokens), [Gemini media resolution](https://aistudio.google.com/docs/media-resolution) (all checked 2026-11-11).
276
+
277
+ ## When to use it
278
+
279
+ - Good fit: you send many screenshots or document pages to a vision model and want to know the cost and any server-side shrinking before the request, offline, in code or CI.
280
+ - Another tool may suit you better: you want a request gateway that counts tokens for many kinds of calls, see [LiteLLM `token_counter`](https://docs.litellm.ai/docs/count_tokens); you want a billing-accurate number, use the usage fields returned by the provider's own API.
281
+
282
+ ## Troubleshooting
283
+
284
+ | Symptom | Cause | Fix |
285
+ |---|---|---|
286
+ | `error: Unknown model 'foo'. Run `pixelbudget models` to list supported models.` (exit 2) | Model name not in the table | Run `pixelbudget models`; aliases such as `gemini-4` map to `gemini-3`. |
287
+ | `error: models file ...: not valid JSON (...)` (exit 2) | The file given to `--models-file` / `PIXELBUDGET_MODELS` is not valid JSON (or not UTF-8) | Save the file as UTF-8 and check the format; see [docs/model-table.md](https://github.com/LeeGyeongPil/pixelbudget/blob/v0.3.0/docs/model-table.md) for the row format. |
288
+ | `error: [Errno 2] No such file or directory: 'nope.png'` (exit 2) | Wrong path | Check the path; folders are scanned for images. |
289
+ | `error: Cannot save '...y.weird': unknown image file extension '.weird'. Use .png, .jpg or .webp.` (exit 2) | `--out` extension is not png, jpg or webp | Use a supported extension such as `--out out.png`. |
290
+ | `error: [Errno 17] File exists: '...'` (exit 2) | A file occupies the place of the output folder | Move the file or choose another `--out` folder. |
291
+ | CLI prints Korean although you expected English | `--lang` not set and your locale is Korean | Add `--lang en` or set `PIXELBUDGET_LANG=en`. |
292
+ | `fit` writes nothing | The model charges per image (Gemini 3) | Follow the printed `media_resolution` hint. |
293
+ | `ImageTooLargeError` | `fit`/`slice` need to decode a huge image | Raise `--max-pixels` / `max_pixels=` if you trust the file. |
294
+ | `report` prints `No readable images found.` and exits 0 | The named file is not a valid image | Add `--strict` so unreadable files fail the run (exit 1); fix or remove the file. |
295
+ | `report` counts extra rows | Folders are scanned recursively, including earlier output | Write results to a different folder such as `out/`. |
296
+
297
+ ## Versions and compatibility
298
+
299
+ Semantic Versioning. While the version is 0.x the API may change in a minor release; changes are listed in [CHANGELOG.md](https://github.com/LeeGyeongPil/pixelbudget/blob/v0.3.0/CHANGELOG.md).
300
+
301
+ ## Development
302
+
303
+ ```bash
304
+ uv sync
305
+ uv run pytest -q # fully offline (sockets are blocked, API key variables removed)
306
+ uv run python examples/quickstart.py
307
+ ```
308
+
309
+ CI runs the tests on Python 3.11 and 3.12.
310
+
311
+ ## License
312
+
313
+ MIT.