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.
- pixelbudget-0.3.0/.github/ISSUE_TEMPLATE/bug.yml +22 -0
- pixelbudget-0.3.0/.github/ISSUE_TEMPLATE/question.yml +11 -0
- pixelbudget-0.3.0/.github/workflows/ci.yml +20 -0
- pixelbudget-0.3.0/.gitignore +43 -0
- pixelbudget-0.3.0/CHANGELOG.md +38 -0
- pixelbudget-0.3.0/LICENSE +21 -0
- pixelbudget-0.3.0/PKG-INFO +313 -0
- pixelbudget-0.3.0/README.ko.md +296 -0
- pixelbudget-0.3.0/README.md +294 -0
- pixelbudget-0.3.0/SECURITY.md +7 -0
- pixelbudget-0.3.0/docs/how-it-works.md +46 -0
- pixelbudget-0.3.0/docs/images/01-report.ko.png +0 -0
- pixelbudget-0.3.0/docs/images/01-report.png +0 -0
- pixelbudget-0.3.0/docs/images/02-slice.ko.png +0 -0
- pixelbudget-0.3.0/docs/images/02-slice.png +0 -0
- pixelbudget-0.3.0/docs/images/03-demo.ko.png +0 -0
- pixelbudget-0.3.0/docs/images/03-demo.png +0 -0
- pixelbudget-0.3.0/docs/images/04-estimate-size.ko.png +0 -0
- pixelbudget-0.3.0/docs/images/04-estimate-size.png +0 -0
- pixelbudget-0.3.0/docs/images/compare.png +0 -0
- pixelbudget-0.3.0/docs/model-table.md +56 -0
- pixelbudget-0.3.0/examples/fit_and_slice.py +12 -0
- pixelbudget-0.3.0/examples/make_samples.py +18 -0
- pixelbudget-0.3.0/examples/quickstart.py +11 -0
- pixelbudget-0.3.0/pyproject.toml +38 -0
- pixelbudget-0.3.0/src/pixelbudget/__init__.py +45 -0
- pixelbudget-0.3.0/src/pixelbudget/__main__.py +5 -0
- pixelbudget-0.3.0/src/pixelbudget/cli.py +376 -0
- pixelbudget-0.3.0/src/pixelbudget/data/models.json +466 -0
- pixelbudget-0.3.0/src/pixelbudget/demo.py +45 -0
- pixelbudget-0.3.0/src/pixelbudget/errors.py +71 -0
- pixelbudget-0.3.0/src/pixelbudget/estimate.py +73 -0
- pixelbudget-0.3.0/src/pixelbudget/fit.py +110 -0
- pixelbudget-0.3.0/src/pixelbudget/i18n.py +43 -0
- pixelbudget-0.3.0/src/pixelbudget/locales/en.json +127 -0
- pixelbudget-0.3.0/src/pixelbudget/locales/ko.json +127 -0
- pixelbudget-0.3.0/src/pixelbudget/measure.py +113 -0
- pixelbudget-0.3.0/src/pixelbudget/py.typed +0 -0
- pixelbudget-0.3.0/src/pixelbudget/registry.py +142 -0
- pixelbudget-0.3.0/src/pixelbudget/report.py +88 -0
- pixelbudget-0.3.0/src/pixelbudget/rules/__init__.py +13 -0
- pixelbudget-0.3.0/src/pixelbudget/rules/fixed.py +9 -0
- pixelbudget-0.3.0/src/pixelbudget/rules/patch.py +71 -0
- pixelbudget-0.3.0/src/pixelbudget/rules/tile.py +32 -0
- pixelbudget-0.3.0/src/pixelbudget/slicing.py +93 -0
- pixelbudget-0.3.0/src/pixelbudget/types.py +175 -0
- pixelbudget-0.3.0/tests/conftest.py +38 -0
- pixelbudget-0.3.0/tests/fixtures/doc_examples.json +21 -0
- pixelbudget-0.3.0/tests/test_cli_polish.py +121 -0
- pixelbudget-0.3.0/tests/test_discrepancies.py +59 -0
- pixelbudget-0.3.0/tests/test_doc_examples.py +35 -0
- pixelbudget-0.3.0/tests/test_error_messages.py +212 -0
- pixelbudget-0.3.0/tests/test_estimate_size_arg.py +92 -0
- pixelbudget-0.3.0/tests/test_fit.py +85 -0
- pixelbudget-0.3.0/tests/test_fixed_honesty.py +37 -0
- pixelbudget-0.3.0/tests/test_hygiene.py +39 -0
- pixelbudget-0.3.0/tests/test_i18n.py +74 -0
- pixelbudget-0.3.0/tests/test_measure.py +65 -0
- pixelbudget-0.3.0/tests/test_models_file.py +185 -0
- pixelbudget-0.3.0/tests/test_report_cli.py +91 -0
- pixelbudget-0.3.0/tests/test_singular.py +35 -0
- pixelbudget-0.3.0/tests/test_slice.py +59 -0
- pixelbudget-0.3.0/tests/test_table_pins.py +123 -0
- 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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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.
|