dougvk commited on
Commit
f340984
·
verified ·
1 Parent(s): cb69571

Publish Unlimited-OCR RDNA 4 runtime v0.1.0

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tests/fixtures/rdna4-smoke.pdf filter=lfs diff=lfs merge=lfs -text
37
+ tests/fixtures/rdna4-smoke.png filter=lfs diff=lfs merge=lfs -text
.github/workflows/ci.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
15
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
16
+ with:
17
+ python-version: "3.12"
18
+ cache: pip
19
+ - run: python -m pip install -e '.[dev]'
20
+ - run: ruff check .
21
+ - run: ruff format --check .
22
+ - run: pytest
23
+ - run: bash -n scripts/bootstrap-rocm.sh scripts/validate-smoke.sh
24
+ - run: python scripts/check-validation.py --help
25
+ - run: scripts/bootstrap-rocm.sh --dry-run
26
+ - run: python -m build
27
+ - run: twine check dist/*
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ .venv-*/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ __pycache__/
6
+ *.py[cod]
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .coverage
11
+ htmlcov/
12
+ *.ocr.md
13
+ *.partial/
14
+ models/
CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented here.
4
+
5
+ ## Unreleased
6
+
7
+ - add a checksum-pinned ROCm 7.2.1 bootstrap for Python 3.12;
8
+ - prepare Baidu Unlimited-OCR revision `07dea832e22aefee32ad281d4b80551282e1c168` without redistributing weights;
9
+ - add audited remote-code hardening and device-compatibility transforms;
10
+ - add single-RDNA-4 GPU diagnostics and stable ROCr UUID selection;
11
+ - support atomic Markdown output from images and page-scoped PDFs;
12
+ - add deterministic `gfx1201` image/PDF acceptance fixtures and validation evidence.
CITATION.cff ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cff-version: 1.2.0
2
+ message: "If you use this runtime, cite both this project and the original Unlimited-OCR paper."
3
+ title: "Unlimited-OCR RDNA 4 Runtime"
4
+ type: software
5
+ authors:
6
+ - family-names: "von Kohorn"
7
+ given-names: "Douglas"
8
+ version: 0.1.0
9
+ url: "https://huggingface.co/dougvk/Unlimited-OCR-RDNA4"
10
+ repository-code: "https://huggingface.co/dougvk/Unlimited-OCR-RDNA4"
11
+ license: MIT
12
+ references:
13
+ - type: article
14
+ title: "Unlimited OCR Works"
15
+ authors:
16
+ - family-names: "Yin"
17
+ given-names: "Youyang"
18
+ - family-names: "Liu"
19
+ given-names: "Huanhuan"
20
+ - name: "YY"
21
+ - family-names: "Xie"
22
+ given-names: "Qunyi"
23
+ - family-names: "Liu"
24
+ given-names: "Chaorun"
25
+ - family-names: "Yang"
26
+ given-names: "Shiqi"
27
+ - family-names: "Wang"
28
+ given-names: "Shaohua"
29
+ - family-names: "Liu"
30
+ given-names: "Zhanlong"
31
+ - family-names: "Zou"
32
+ given-names: "Hao"
33
+ - family-names: "Chen"
34
+ given-names: "Jinyue"
35
+ - family-names: "Wei"
36
+ given-names: "Shu"
37
+ - family-names: "Wu"
38
+ given-names: "Jingjing"
39
+ - family-names: "Huang"
40
+ given-names: "Mingxin"
41
+ - family-names: "Wu"
42
+ given-names: "Zhen"
43
+ - family-names: "Wang"
44
+ given-names: "Guibin"
45
+ - family-names: "Du"
46
+ given-names: "Tengyu"
47
+ - family-names: "Jia"
48
+ given-names: "Lei"
49
+ year: 2026
50
+ url: "https://arxiv.org/abs/2606.23050"
CONTRIBUTING.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing
2
+
3
+ Contributions are welcome, especially independently reproduced RDNA 4 results.
4
+
5
+ Before opening a pull request:
6
+
7
+ ```bash
8
+ ruff check .
9
+ ruff format --check .
10
+ pytest
11
+ python -m build
12
+ twine check dist/*
13
+ ```
14
+
15
+ GPU-dependent changes must include:
16
+
17
+ - GPU model and architecture;
18
+ - OS, kernel, host ROCm, PyTorch, torchvision, Triton, and Transformers versions;
19
+ - the exact model revision;
20
+ - the command used;
21
+ - peak allocated VRAM and wall time;
22
+ - a statement about output comparison or quality evidence;
23
+ - confirmation that unrelated GPUs and system Python were not modified.
24
+
25
+ Do not commit model weights, generated OCR from private documents, Hugging Face tokens, or raw home-directory paths.
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Douglas von Kohorn
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.
README.md ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: baidu/Unlimited-OCR
3
+ pipeline_tag: image-text-to-text
4
+ license: mit
5
+ tags:
6
+ - rocm
7
+ - amd
8
+ - rdna4
9
+ - gfx1201
10
+ - ocr
11
+ - runtime
12
+ ---
13
+
14
+ # Unlimited-OCR RDNA 4
15
+
16
+ Single-GPU [Baidu Unlimited-OCR](https://huggingface.co/baidu/Unlimited-OCR) inference for AMD RDNA 4
17
+ (`gfx1200` / `gfx1201`) using native ROCm PyTorch, verified end to end on `gfx1201`.
18
+
19
+ This project does **not** redistribute or rename Baidu's weights. It provides a reproducible runtime around the original
20
+ BF16 checkpoint: an isolated AMD wheel bootstrap, pinned model preparation, audited custom-code patching, single-GPU
21
+ selection, local-only model resolution during inference, atomic output, bounded PDF handling, diagnostics, and
22
+ validation evidence.
23
+
24
+ > Status: beta. Verified on 31 July 2026 with an AMD Radeon RX 9070 XT (`gfx1201`). This is not an AMD or Baidu project.
25
+
26
+ ## Verified configuration
27
+
28
+ | Component | Verified value |
29
+ |---|---|
30
+ | GPU | AMD Radeon RX 9070 XT, 16 GB |
31
+ | GPU allocation | One isolated GPU |
32
+ | OS | Ubuntu 24.04.4 |
33
+ | Kernel | 6.17.0-40-generic |
34
+ | Host ROCm | 7.2.1 |
35
+ | PyTorch | 2.9.1 + ROCm 7.2.1 |
36
+ | Transformers | 4.57.1 |
37
+ | Matplotlib | 3.10.8 |
38
+ | Model revision | `07dea832e22aefee32ad281d4b80551282e1c168` |
39
+ | Weight precision | Original BF16; no quantization |
40
+ | Peak allocated VRAM | 8.25 GiB on the 1600×2000 validation page |
41
+
42
+ The initial validation recovered headings, reading order, a structured table, euro amounts, a formula, and an exact
43
+ checksum. Repeated greedy generation was byte-identical after deterministic tag cleanup. See
44
+ [docs/VALIDATION.md](docs/VALIDATION.md) for the evidence and its limits.
45
+
46
+ ## Quick start
47
+
48
+ The bootstrap downloads checksum-pinned AMD ROCm wheels into your XDG cache, installs every other runtime/build
49
+ dependency from `requirements/bootstrap.lock` with hashes required, builds this checkout without build isolation, and
50
+ creates a repository-local virtual environment. It does not use `sudo`, alter `/opt/rocm`, or touch the system Python.
51
+
52
+ ```bash
53
+ git clone https://huggingface.co/dougvk/Unlimited-OCR-RDNA4
54
+ cd Unlimited-OCR-RDNA4
55
+
56
+ ./scripts/bootstrap-rocm.sh
57
+ .venv/bin/unlimited-ocr-rdna4 prepare
58
+ .venv/bin/unlimited-ocr-rdna4 run --input page.png
59
+ ```
60
+
61
+ The model download is approximately 6.78 GB. The verified AMD wheel set is approximately 1.9 GB before installation.
62
+
63
+ PDFs are rendered and parsed page by page:
64
+
65
+ ```bash
66
+ .venv/bin/unlimited-ocr-rdna4 run \
67
+ --input document.pdf \
68
+ --output document.md \
69
+ --max-pages 10 \
70
+ --dpi 200
71
+ ```
72
+
73
+ ## Commands
74
+
75
+ ### `prepare`
76
+
77
+ Downloads the pinned original checkpoint, fully hashes the 6.67 GB safetensors file and every behavior-defining model,
78
+ configuration, tokenizer, and index file, preserves Baidu's original model source, and applies a narrowly scoped audited
79
+ patch.
80
+
81
+ ```bash
82
+ unlimited-ocr-rdna4 prepare
83
+ unlimited-ocr-rdna4 prepare --dry-run --json
84
+ unlimited-ocr-rdna4 prepare --model-dir /data/models/unlimited-ocr
85
+ ```
86
+
87
+ Preparation is serialized and idempotent. It uses a unique partial directory, validates the exact manifest and file set,
88
+ and refuses unknown, extra, symlinked, or mismatched files instead of executing them.
89
+
90
+ ### `doctor`
91
+
92
+ Reports the active PyTorch/HIP stack, visible devices, RDNA 4 architecture, BF16 capability, and model readiness.
93
+
94
+ ```bash
95
+ unlimited-ocr-rdna4 doctor
96
+ unlimited-ocr-rdna4 doctor --device 0 --require-model
97
+ unlimited-ocr-rdna4 doctor --json
98
+ ```
99
+
100
+ ### `run`
101
+
102
+ Parses one image or PDF and publishes untrusted model output atomically. The content may include Markdown and raw HTML;
103
+ do not render it in a privileged origin without sanitization.
104
+
105
+ ```bash
106
+ unlimited-ocr-rdna4 run --input scan.png
107
+ unlimited-ocr-rdna4 run --input scan.png --output scan.md --device 0
108
+ unlimited-ocr-rdna4 run --input report.pdf --start-page 21 --max-pages 10
109
+ ```
110
+
111
+ Important flags:
112
+
113
+ | Flag | Default | Meaning |
114
+ |---|---:|---|
115
+ | `--device` | `$UNLIMITED_OCR_DEVICE` or `0` | ROCm ordinal or stable ROCr UUID |
116
+ | `--mode` | `gundam` | `gundam` for detailed single-page parsing; `base` for lower-detail input |
117
+ | `--max-length` | `4096` | Total input + output sequence limit; maximum `32768` |
118
+ | `--dpi` | `200` | PDF rendering resolution |
119
+ | `--max-pages` | `20` | Per-run PDF safety cap |
120
+ | `--max-page-pixels` | `60000000` | Per-page rendered-pixel cap |
121
+ | `--max-total-pixels` | `400000000` | Aggregate rendered-pixel cap |
122
+ | `--max-page-rendered-mib` | `512` | Per-page rendered temporary-byte cap |
123
+ | `--max-rendered-mib` | `2048` | Aggregate rendered temporary-byte cap |
124
+ | `--force` | off | Replace an existing output file |
125
+ | `--json` | off | Stable machine-readable summary on stdout |
126
+ | `--quiet` | off | Suppress progress diagnostics |
127
+
128
+ OCR content is written to the output file. Progress and warnings go to stderr. Human or JSON summaries go to stdout.
129
+
130
+ ## GPU selection
131
+
132
+ PyTorch uses the CUDA-compatible API name on ROCm. `--device` sets `ROCR_VISIBLE_DEVICES` before PyTorch is imported, so
133
+ the process sees one logical `cuda:0` backed by the selected AMD card.
134
+
135
+ For ordinary one-GPU systems:
136
+
137
+ ```bash
138
+ unlimited-ocr-rdna4 run --input page.png --device 0
139
+ ```
140
+
141
+ For multi-GPU systems, a stable ROCr UUID avoids dependence on enumeration order:
142
+
143
+ ```bash
144
+ unlimited-ocr-rdna4 run --input page.png --device GPU-0123456789abcdef
145
+ ```
146
+
147
+ Run `unlimited-ocr-rdna4 doctor` before choosing a device. Do not assume another application's GPU numbering matches
148
+ ROCm's ordinal numbering.
149
+
150
+ ## What the RDNA 4 adaptation changes
151
+
152
+ - Pins the Baidu checkpoint and verifies the complete expected local model tree before custom code loads.
153
+ - Uses AMD's production ROCm 7.2.1 PyTorch, torchvision, and Triton wheels for Python 3.12.
154
+ - Isolates one `gfx1200`/`gfx1201` GPU before importing PyTorch.
155
+ - Replaces unsafe `eval()` calls in optional model-output geometry parsing with `ast.literal_eval()`.
156
+ - Makes one internal mask transfer follow the active tensor device.
157
+ - Supplies the missing all-ones attention mask and pad token for single-sequence generation.
158
+ - Loads only the prepared local model during inference and enables Hugging Face offline mode.
159
+ - Avoids vLLM, SGLang, quantization, tensor parallelism, and unverified custom serving kernels.
160
+ - Removes only complete, exact layout sentinel pairs without rewriting recognized Unicode or TeX.
161
+ - Detects obvious terminal repetition and warns without silently rewriting recognition content.
162
+
163
+ The model weights and mathematical operators are unchanged. PyTorch's ROCm backend provides the RDNA 4 kernels.
164
+
165
+ ## Configuration
166
+
167
+ Precedence is flags, then environment variables, then XDG defaults.
168
+
169
+ | Environment variable | Purpose |
170
+ |---|---|
171
+ | `UNLIMITED_OCR_DEVICE` | Default ROCm ordinal or UUID |
172
+ | `UNLIMITED_OCR_MODEL_DIR` | Prepared model directory |
173
+ | `XDG_DATA_HOME` | Default model storage root |
174
+ | `XDG_CACHE_HOME` | Wheel, Hugging Face, and temporary-work cache root |
175
+ | `NO_COLOR` | Accepted implicitly; the CLI currently emits no color |
176
+
177
+ This project contains no analytics or telemetry code.
178
+
179
+ ## Safety and limitations
180
+
181
+ - The default 4096 sequence limit includes visual-prefill tokens. Dense pages may require a larger value.
182
+ - PDF mode uses permissively licensed PDFium bindings and renders one bounded page at a time. A failed run leaves no
183
+ published partial document; retry with `--start-page` to resume manually.
184
+ - Generative OCR can omit or hallucinate content. Verify consequential documents against the source.
185
+ - Rotated text and repetitive pages are known upstream weak spots.
186
+ - Only the configuration above has completed the full repository acceptance test. Newer ROCm/PyTorch versions may work,
187
+ but `doctor` reports them as unverified until measured.
188
+ - The hardware guard accepts `gfx1200`, but reports `hardware_verified=false`; only `gfx1201` has completed this GPU gate.
189
+ - The bootstrap currently supports Linux x86_64, Python 3.12, and host ROCm 7.2.1.
190
+
191
+ See [SECURITY.md](SECURITY.md) before processing untrusted documents.
192
+
193
+ ## Development
194
+
195
+ CPU-only tests do not install PyTorch. The commands below are convenient for development; the release bootstrap is the
196
+ hash-locked installation path.
197
+
198
+ ```bash
199
+ python3.12 -m venv .venv-dev
200
+ .venv-dev/bin/pip install -e '.[dev]'
201
+ .venv-dev/bin/ruff check .
202
+ .venv-dev/bin/ruff format --check .
203
+ .venv-dev/bin/pytest
204
+ .venv-dev/bin/python -m build
205
+ .venv-dev/bin/twine check dist/*
206
+ ```
207
+
208
+ The GPU smoke gate is intentionally separate. It uses tracked, hash-checked fixtures; parses structured JSON; runs the
209
+ image twice in fresh processes; and checks image/PDF output hashes, structure, stack identity, revision, VRAM, and process
210
+ release. On the validated host, run it in a root-created private network namespace:
211
+
212
+ ```bash
213
+ VALIDATION_NETWORK_ISOLATED=1 \
214
+ UNLIMITED_OCR_DEVICE=GPU-0123456789abcdef \
215
+ AMD_SMI_GPU=3 \
216
+ ./scripts/validate-smoke.sh
217
+ ```
218
+
219
+ ## Credits and license
220
+
221
+ - Model and model code: [Baidu Unlimited-OCR](https://github.com/baidu/Unlimited-OCR), MIT licensed.
222
+ - ROCm evaluation and batching research: [AIwork4me/Unlimited-OCR-ROCm](https://github.com/AIwork4me/Unlimited-OCR-ROCm).
223
+ - AMD wheel source and compatibility guidance: [ROCm documentation](https://rocm.docs.amd.com/).
224
+ - PDF rendering: [pypdfium2](https://pypi.org/project/pypdfium2/) and PDFium, under permissive licenses.
225
+
226
+ The runtime is MIT licensed. Baidu's original copyright and license are preserved; see
227
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
SECURITY.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Security policy
2
+
3
+ ## Custom model code
4
+
5
+ Unlimited-OCR requires custom Python. This project never follows a floating model revision during inference.
6
+
7
+ `prepare`:
8
+
9
+ 1. downloads one pinned Baidu revision into a unique partial directory under a preparation lock;
10
+ 2. fully hashes every expected code, configuration, tokenizer, index, license, and weight file;
11
+ 3. applies exact, count-checked source transformations;
12
+ 4. verifies the patched tree, rejects symlinks and unexpected files, and records an exact local manifest;
13
+ 5. publishes the completed directory only after all checks pass.
14
+
15
+ `run` uses only the prepared local directory with `local_files_only=True`, `HF_HUB_OFFLINE=1`, and
16
+ `TRANSFORMERS_OFFLINE=1`. Those settings prevent model resolution from using the network; they are not a process sandbox.
17
+
18
+ ## Untrusted documents
19
+
20
+ Model output is untrusted Markdown/raw HTML data. The runtime does not call `eval()` on it. Output paths use an exclusive,
21
+ random same-directory temporary file and an atomic no-replace publish; symlinks and input/output aliases are rejected.
22
+ `--force` alone enables atomic replacement.
23
+
24
+ Images and PDFs can still trigger bugs in Pillow, PDFium/pypdfium2, PyTorch, Transformers, or the model code. PDF rendering
25
+ has per-page and aggregate pixel/byte limits, but hostile inputs still belong under a dedicated unprivileged account or
26
+ an appropriately restricted container. Do not expose this CLI directly as a public upload service without process/network
27
+ sandboxing, input-byte limits, timeouts, and admission controls.
28
+
29
+ ## Reporting
30
+
31
+ Please report vulnerabilities privately through GitHub's security-advisory interface for this repository. Do not include
32
+ private documents, credentials, model-cache tokens, or sensitive OCR output in a report.
THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party notices
2
+
3
+ ## Baidu Unlimited-OCR
4
+
5
+ The `prepare` command downloads and modifies one source file from
6
+ [`baidu/Unlimited-OCR`](https://huggingface.co/baidu/Unlimited-OCR) revision
7
+ `07dea832e22aefee32ad281d4b80551282e1c168`. The original source is preserved beside the patched file as
8
+ `modeling_unlimitedocr.py.upstream`.
9
+
10
+ MIT License
11
+
12
+ Copyright (c) 2026 Baidu
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+
32
+ ## Unlimited-OCR-ROCm
33
+
34
+ The project does not copy its package code. Its public ROCm evaluation, accuracy analysis, and direct-Transformers
35
+ research informed this runtime and are credited in the README.
36
+
37
+ ## pypdfium2 and PDFium
38
+
39
+ PDF support depends on [`pypdfium2`](https://github.com/pypdfium2-team/pypdfium2), which is available under
40
+ BSD-3-Clause or Apache-2.0 terms and distributes PDFium plus its dependency license notices. PDFium itself uses a
41
+ BSD-style license. This repository does not vendor either binary; the hash-locked bootstrap installs the official
42
+ pypdfium2 wheel, which carries its applicable notices.
dist/unlimited_ocr_rdna4-0.1.0-py3-none-any.whl ADDED
Binary file (24.7 kB). View file
 
dist/unlimited_ocr_rdna4-0.1.0.tar.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2146c24804ef7d6633156e7d48010b40c08f6ce04a29bd40b404455bb751d3c
3
+ size 252988
docs/PUBLISHING.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Publishing checklist
2
+
3
+ The repository intentionally separates source publication from package/model publication.
4
+
5
+ ## Hugging Face
6
+
7
+ 1. Review `git diff --check`, tests, build artifacts, and validation evidence.
8
+ 2. Create `dougvk/Unlimited-OCR-RDNA4` as a private model repository.
9
+ 3. Push the reviewed initial commit.
10
+ 4. Verify the model card, source tree, package artifacts, and remote hashes.
11
+ 5. Make the repository public only after those checks pass.
12
+ 6. Tag `v0.1.0` only after the clean-environment gate in `docs/VALIDATION.md` is reproduced.
13
+
14
+ ## PyPI (deferred)
15
+
16
+ Do not publish `v0.1.0` to PyPI. The supported installation contract is the repository's hash-locked bootstrap, while an
17
+ installed wheel cannot yet reproduce that bootstrap without returning to the source checkout. Building and running
18
+ `twine check` remains a packaging gate, not authorization to upload. A later PyPI release should bundle an equivalent
19
+ bootstrap entry point and independently verify a fresh wheel installation first.
20
+
21
+ Do not upload or duplicate Baidu's checkpoint. The repository contains the runtime source and packages and links to the
22
+ original `baidu/Unlimited-OCR` weights.
23
+
24
+ The repository should be classified as documentation for a runtime/integration, not as a runnable Transformers
25
+ pipeline, new model, checkpoint, fine-tune, or kernel port.
26
+
27
+ ## GitHub (optional mirror)
28
+
29
+ If a GitHub mirror is created later, update the project URLs consistently and enable private vulnerability reporting.
30
+
31
+ ## Upstream
32
+
33
+ After publication:
34
+
35
+ 1. open a concise Baidu discussion or documentation pull request with the verified configuration and evidence;
36
+ 2. offer the gfx1201 bootstrap and validation findings to `AIwork4me/Unlimited-OCR-ROCm`;
37
+ 3. avoid claiming universal ROCm or all-RDNA support until independently reproduced.
docs/VALIDATION.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RDNA 4 validation
2
+
3
+ ## Scope
4
+
5
+ This gate validates a narrow runtime integration:
6
+
7
+ - original Baidu BF16 checkpoint at one immutable revision;
8
+ - direct Hugging Face Transformers inference on one visible AMD GPU;
9
+ - no quantization, serving framework, tensor parallelism, custom kernel package, renamed weights, or fine-tuning;
10
+ - exact software inventory, model-tree integrity, deterministic output, bounded PDF rendering, and released VRAM.
11
+
12
+ It is not a new model/checkpoint/kernel port or an OCR-quality benchmark.
13
+
14
+ ## Verified host
15
+
16
+ Validated 31 July 2026 on one GPU in a Tinybox Red v2:
17
+
18
+ - AMD Radeon RX 9070 XT, `gfx1201`, 16 GB;
19
+ - Ubuntu 24.04.4, kernel `6.17.0-40-generic`;
20
+ - host ROCm 7.2.1 and HIP runtime `7.2.53211-e1a6bc5663`;
21
+ - PyTorch `2.9.1+rocm7.2.1.gitff65f5bc`;
22
+ - torchvision `0.24.0+rocm7.2.1.gitb919bd0c`;
23
+ - Triton `3.5.1+rocm7.2.1.gita272dfa8`;
24
+ - Transformers 4.57.1 and pypdfium2 5.12.1;
25
+ - model revision `07dea832e22aefee32ad281d4b80551282e1c168`;
26
+ - weight SHA-256 `2bc48a7a110061ea58fff65d3169367eebe3aee371ca6968dc2219c1b2855fc6`.
27
+
28
+ The exact parsed evidence is checked in as [`validation-gfx1201.json`](validation-gfx1201.json).
29
+
30
+ ## Tracked gate
31
+
32
+ The repository carries one synthetic 1600×2000 document as PNG and one-page PDF. Their bytes are fixed:
33
+
34
+ | Fixture | SHA-256 |
35
+ |---|---|
36
+ | PNG | `f5099e17be868abfb4213dbdab220deac82a2db93ba87ab22f03219178246972` |
37
+ | PDF | `cdd2b484d0ac90bd98b489dd97565a65eb17246f713359524cddd41d78cc10cb` |
38
+
39
+ The gate:
40
+
41
+ 1. checksum-verifies both fixtures;
42
+ 2. parses `doctor --json` and requires exactly one visible `gfx1201`, BF16, the exact package/HIP stack, and the pinned prepared model;
43
+ 3. proves the root-created systemd `PrivateNetwork` namespace cannot make an outbound connection;
44
+ 4. runs PNG inference twice in fresh processes and PDF inference once;
45
+ 5. checks heading/table/reading order, euro amount, formula, checksum text, page counts, revision, architecture, and a sub-16-GiB peak;
46
+ 6. requires byte-identical repeated PNG output and the recorded PNG/PDF output hashes;
47
+ 7. parses AMD SMI JSON after completion and rejects a remaining Python GPU process.
48
+
49
+ Final results:
50
+
51
+ | Test | Model load | Inference | Peak allocated VRAM | Output SHA-256 |
52
+ |---|---:|---:|---:|---|
53
+ | PNG process 1 | 9.287 s | 15.275 s | 8.253 GiB | `13df4005…cbc01` |
54
+ | PNG process 2 | 9.000 s | 14.995 s | 8.253 GiB | `13df4005…cbc01` |
55
+ | One-page PDF, 150 DPI | 8.983 s | 15.368 s | 8.253 GiB | `fb17a639…42438` |
56
+
57
+ AMD SMI reported `No running processes detected` after the final process. The two PNG results were byte-identical.
58
+
59
+ ## Reproduce
60
+
61
+ Start with no competing owner of the selected GPU, then run:
62
+
63
+ ```bash
64
+ ./scripts/bootstrap-rocm.sh
65
+ .venv/bin/unlimited-ocr-rdna4 prepare
66
+
67
+ VALIDATION_NETWORK_ISOLATED=1 \
68
+ UNLIMITED_OCR_DEVICE=GPU-0123456789abcdef \
69
+ AMD_SMI_GPU=3 \
70
+ ./scripts/validate-smoke.sh
71
+ ```
72
+
73
+ `VALIDATION_NETWORK_ISOLATED=1` requires non-interactive permission for the script's narrowly scoped root
74
+ `systemd-run` command. Without it, the functional gate still runs but correctly records `network_isolated=false`.
75
+ `AMD_SMI_GPU` is the physical AMD SMI index used only for the post-run process check.
76
+
77
+ Transformers reports `model.vision_model.embeddings.position_ids` as newly initialized. That object is a non-persistent
78
+ derived position-index buffer, not a learned checkpoint parameter; the original safetensors file is fully hashed before
79
+ every load.
80
+
81
+ ## Limits
82
+
83
+ - Only `gfx1201` has passed this exact gate. `gfx1200` is accepted by the architecture guard but reported as not hardware
84
+ verified.
85
+ - The synthetic fixture demonstrates runtime compatibility, deterministic decoding, basic structure recovery, and the
86
+ PDF path. It does not establish real-document accuracy.
87
+ - Generative OCR can omit or hallucinate text. Consequential output must be checked against the source.
docs/validation-gfx1201.json ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "doctor": {
3
+ "model": {
4
+ "model_dir": "<prepared-model>",
5
+ "prepared": true,
6
+ "revision": "07dea832e22aefee32ad281d4b80551282e1c168"
7
+ },
8
+ "runtime": {
9
+ "accepted_architecture": true,
10
+ "bf16_supported": true,
11
+ "cuda_available": true,
12
+ "devices": [
13
+ {
14
+ "architecture": "gfx1201",
15
+ "index": 0,
16
+ "name": "AMD Radeon RX 9070 XT",
17
+ "uuid": "39663934-3932-3539-3931-333733663736"
18
+ }
19
+ ],
20
+ "hardware_verified": true,
21
+ "hip_version": "7.2.53211-e1a6bc5663",
22
+ "package_versions": {
23
+ "numpy": "1.26.4",
24
+ "pillow": "12.1.1",
25
+ "pypdfium2": "5.12.1",
26
+ "torch": "2.9.1+rocm7.2.1.lw.gitff65f5bc",
27
+ "torchvision": "0.24.0+rocm7.2.1.gitb919bd0c",
28
+ "transformers": "4.57.1",
29
+ "triton": "3.5.1+rocm7.2.1.gita272dfa8"
30
+ },
31
+ "software_verified": true,
32
+ "torch_version": "2.9.1+rocm7.2.1.gitff65f5bc",
33
+ "visible_devices": 1,
34
+ "warnings": []
35
+ },
36
+ "runtime_issues": [],
37
+ "schema_version": 1,
38
+ "status": "ok"
39
+ },
40
+ "fixtures": {
41
+ "image_sha256": "f5099e17be868abfb4213dbdab220deac82a2db93ba87ab22f03219178246972",
42
+ "pdf_sha256": "cdd2b484d0ac90bd98b489dd97565a65eb17246f713359524cddd41d78cc10cb"
43
+ },
44
+ "gpu_processes_after": [
45
+ {
46
+ "gpu": 3,
47
+ "process_list": [
48
+ {
49
+ "process_info": "No running processes detected"
50
+ }
51
+ ]
52
+ }
53
+ ],
54
+ "image_runs": [
55
+ {
56
+ "architecture": "gfx1201",
57
+ "gpu": "AMD Radeon RX 9070 XT",
58
+ "hardware_verified": true,
59
+ "hip_version": "7.2.53211-e1a6bc5663",
60
+ "inference_seconds": 15.275,
61
+ "input": "tests/fixtures/rdna4-smoke.png",
62
+ "mode": "gundam",
63
+ "model_load_seconds": 9.287,
64
+ "model_revision": "07dea832e22aefee32ad281d4b80551282e1c168",
65
+ "output": "<temporary-output>/smoke-image-1.md",
66
+ "page_seconds": [
67
+ 15.274
68
+ ],
69
+ "pages_processed": 1,
70
+ "pages_total": 1,
71
+ "peak_allocated_gib": 8.253,
72
+ "schema_version": 1,
73
+ "software_verified": true,
74
+ "status": "ok",
75
+ "torch_version": "2.9.1+rocm7.2.1.gitff65f5bc",
76
+ "warnings": []
77
+ },
78
+ {
79
+ "architecture": "gfx1201",
80
+ "gpu": "AMD Radeon RX 9070 XT",
81
+ "hardware_verified": true,
82
+ "hip_version": "7.2.53211-e1a6bc5663",
83
+ "inference_seconds": 14.995,
84
+ "input": "tests/fixtures/rdna4-smoke.png",
85
+ "mode": "gundam",
86
+ "model_load_seconds": 9.0,
87
+ "model_revision": "07dea832e22aefee32ad281d4b80551282e1c168",
88
+ "output": "<temporary-output>/smoke-image-2.md",
89
+ "page_seconds": [
90
+ 14.995
91
+ ],
92
+ "pages_processed": 1,
93
+ "pages_total": 1,
94
+ "peak_allocated_gib": 8.253,
95
+ "schema_version": 1,
96
+ "software_verified": true,
97
+ "status": "ok",
98
+ "torch_version": "2.9.1+rocm7.2.1.gitff65f5bc",
99
+ "warnings": []
100
+ }
101
+ ],
102
+ "image_sha256": "13df40051aceb3bd14f75622697dda1f2a96515bb95c0483e0920cd4d41cbc01",
103
+ "network_isolated": true,
104
+ "pdf_run": {
105
+ "architecture": "gfx1201",
106
+ "gpu": "AMD Radeon RX 9070 XT",
107
+ "hardware_verified": true,
108
+ "hip_version": "7.2.53211-e1a6bc5663",
109
+ "inference_seconds": 15.368,
110
+ "input": "tests/fixtures/rdna4-smoke.pdf",
111
+ "mode": "gundam",
112
+ "model_load_seconds": 8.983,
113
+ "model_revision": "07dea832e22aefee32ad281d4b80551282e1c168",
114
+ "output": "<temporary-output>/smoke-pdf.md",
115
+ "page_seconds": [
116
+ 15.015
117
+ ],
118
+ "pages_processed": 1,
119
+ "pages_total": 1,
120
+ "peak_allocated_gib": 8.253,
121
+ "schema_version": 1,
122
+ "software_verified": true,
123
+ "status": "ok",
124
+ "torch_version": "2.9.1+rocm7.2.1.gitff65f5bc",
125
+ "warnings": []
126
+ },
127
+ "pdf_sha256": "fb17a6396c3e73a025e388a09f73357960ca0bf95124e38179e06a2e64442438",
128
+ "schema_version": 1,
129
+ "validation": "PASS"
130
+ }
huggingface/README.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: baidu/Unlimited-OCR
3
+ license: mit
4
+ tags:
5
+ - rocm
6
+ - amd
7
+ - rdna4
8
+ - gfx1201
9
+ - ocr
10
+ - runtime
11
+ ---
12
+
13
+ # Unlimited-OCR RDNA 4 runtime
14
+
15
+ This is documentation for a lightweight runtime companion to
16
+ [`baidu/Unlimited-OCR`](https://huggingface.co/baidu/Unlimited-OCR), not a hosted pipeline, new checkpoint, fine-tune, or
17
+ kernel port. No model weights are duplicated here.
18
+
19
+ The linked runtime verifies direct BF16 Transformers inference on one 16 GB AMD Radeon RX 9070 XT (`gfx1201`) with ROCm
20
+ 7.2.1 and AMD's PyTorch 2.9.1 wheel set. It provides pinned model preparation, code-integrity checks, offline inference,
21
+ bounded PDF handling, and tracked reproducible validation fixtures.
22
+
23
+ Source and instructions: <https://huggingface.co/dougvk/Unlimited-OCR-RDNA4>
24
+
25
+ Please cite and follow the license terms of the original Baidu model.
pyproject.toml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling==1.27.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "unlimited-ocr-rdna4"
7
+ dynamic = ["version"]
8
+ description = "Verified local Unlimited-OCR inference on AMD RDNA 4 with ROCm"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12,<3.13"
11
+ license = {file = "LICENSE"}
12
+ authors = [{name = "Douglas von Kohorn"}]
13
+ keywords = ["ocr", "rocm", "rdna4", "gfx1201", "amd", "transformers"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ ]
25
+ dependencies = [
26
+ "addict==2.4.0",
27
+ "easydict==1.13",
28
+ "einops==0.8.2",
29
+ "huggingface-hub==0.36.2",
30
+ "matplotlib==3.10.8",
31
+ "numpy==1.26.4",
32
+ "Pillow==12.1.1",
33
+ "psutil==7.2.2",
34
+ "pypdfium2==5.12.1",
35
+ "requests==2.34.2",
36
+ "safetensors==0.8.0",
37
+ "tqdm==4.70.0",
38
+ "transformers==4.57.1",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ dev = ["build==1.5.0", "pytest==8.4.2", "pytest-cov==6.3.0", "ruff==0.14.14", "twine==6.2.0"]
43
+
44
+ [project.scripts]
45
+ unlimited-ocr-rdna4 = "unlimited_ocr_rdna4.cli:main"
46
+
47
+ [project.urls]
48
+ Homepage = "https://huggingface.co/dougvk/Unlimited-OCR-RDNA4"
49
+ Documentation = "https://huggingface.co/dougvk/Unlimited-OCR-RDNA4#readme"
50
+ Issues = "https://huggingface.co/dougvk/Unlimited-OCR-RDNA4/discussions"
51
+ Source = "https://huggingface.co/dougvk/Unlimited-OCR-RDNA4"
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/unlimited_ocr_rdna4"]
55
+
56
+ [tool.hatch.version]
57
+ path = "src/unlimited_ocr_rdna4/constants.py"
58
+
59
+ [tool.pytest.ini_options]
60
+ addopts = "-q"
61
+ testpaths = ["tests"]
62
+
63
+ [tool.ruff]
64
+ line-length = 120
65
+ target-version = "py312"
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
69
+ allowed-confusables = ["|"]
70
+
71
+ [tool.ruff.format]
72
+ quote-style = "double"
requirements/bootstrap.in ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Runtime dependencies: keep direct pins synchronized with pyproject.toml.
2
+ addict==2.4.0
3
+ easydict==1.13
4
+ einops==0.8.2
5
+ huggingface-hub==0.36.2
6
+ matplotlib==3.10.8
7
+ numpy==1.26.4
8
+ Pillow==12.1.1
9
+ psutil==7.2.2
10
+ pypdfium2==5.12.1
11
+ requests==2.34.2
12
+ safetensors==0.8.0
13
+ tqdm==4.70.0
14
+ transformers==4.57.1
15
+
16
+ # Build backend used with --no-build-isolation.
17
+ hatchling==1.27.0
18
+
19
+ # Dependencies declared by AMD's checksum-pinned PyTorch wheel.
20
+ filelock==3.32.2
21
+ fsspec==2026.7.0
22
+ Jinja2==3.1.6
23
+ networkx==3.6.1
24
+ setuptools==83.0.0
25
+ sympy==1.14.0
26
+ typing-extensions==4.16.0
requirements/bootstrap.lock ADDED
@@ -0,0 +1,1061 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile requirements/bootstrap.in --generate-hashes --only-binary=:all: --python-version 3.12 --python-platform x86_64-manylinux_2_28 -o requirements/bootstrap.lock
3
+ addict==2.4.0 \
4
+ --hash=sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc \
5
+ --hash=sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494
6
+ # via -r requirements/bootstrap.in
7
+ certifi==2026.7.22 \
8
+ --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
9
+ --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
10
+ # via requests
11
+ charset-normalizer==3.4.9 \
12
+ --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \
13
+ --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \
14
+ --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \
15
+ --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \
16
+ --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \
17
+ --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \
18
+ --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \
19
+ --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \
20
+ --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \
21
+ --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \
22
+ --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \
23
+ --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \
24
+ --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \
25
+ --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \
26
+ --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \
27
+ --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \
28
+ --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \
29
+ --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \
30
+ --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \
31
+ --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \
32
+ --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \
33
+ --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \
34
+ --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \
35
+ --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \
36
+ --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \
37
+ --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \
38
+ --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \
39
+ --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \
40
+ --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \
41
+ --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \
42
+ --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \
43
+ --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \
44
+ --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \
45
+ --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \
46
+ --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \
47
+ --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \
48
+ --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \
49
+ --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \
50
+ --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \
51
+ --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \
52
+ --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \
53
+ --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \
54
+ --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \
55
+ --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \
56
+ --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \
57
+ --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \
58
+ --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \
59
+ --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \
60
+ --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \
61
+ --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \
62
+ --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \
63
+ --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \
64
+ --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \
65
+ --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \
66
+ --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \
67
+ --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \
68
+ --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \
69
+ --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \
70
+ --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \
71
+ --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \
72
+ --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \
73
+ --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \
74
+ --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \
75
+ --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \
76
+ --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \
77
+ --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \
78
+ --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \
79
+ --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \
80
+ --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \
81
+ --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \
82
+ --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \
83
+ --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \
84
+ --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \
85
+ --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \
86
+ --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \
87
+ --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \
88
+ --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \
89
+ --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \
90
+ --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \
91
+ --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \
92
+ --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \
93
+ --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \
94
+ --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \
95
+ --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \
96
+ --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \
97
+ --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \
98
+ --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \
99
+ --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \
100
+ --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \
101
+ --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \
102
+ --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \
103
+ --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \
104
+ --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115
105
+ # via requests
106
+ contourpy==1.3.3 \
107
+ --hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \
108
+ --hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \
109
+ --hash=sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880 \
110
+ --hash=sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a \
111
+ --hash=sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8 \
112
+ --hash=sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc \
113
+ --hash=sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470 \
114
+ --hash=sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5 \
115
+ --hash=sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263 \
116
+ --hash=sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b \
117
+ --hash=sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5 \
118
+ --hash=sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381 \
119
+ --hash=sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3 \
120
+ --hash=sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4 \
121
+ --hash=sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e \
122
+ --hash=sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f \
123
+ --hash=sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772 \
124
+ --hash=sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286 \
125
+ --hash=sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42 \
126
+ --hash=sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301 \
127
+ --hash=sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77 \
128
+ --hash=sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7 \
129
+ --hash=sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411 \
130
+ --hash=sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 \
131
+ --hash=sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 \
132
+ --hash=sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a \
133
+ --hash=sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b \
134
+ --hash=sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db \
135
+ --hash=sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 \
136
+ --hash=sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620 \
137
+ --hash=sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989 \
138
+ --hash=sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea \
139
+ --hash=sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67 \
140
+ --hash=sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5 \
141
+ --hash=sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d \
142
+ --hash=sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36 \
143
+ --hash=sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99 \
144
+ --hash=sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1 \
145
+ --hash=sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e \
146
+ --hash=sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b \
147
+ --hash=sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8 \
148
+ --hash=sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d \
149
+ --hash=sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7 \
150
+ --hash=sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 \
151
+ --hash=sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339 \
152
+ --hash=sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1 \
153
+ --hash=sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659 \
154
+ --hash=sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4 \
155
+ --hash=sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f \
156
+ --hash=sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20 \
157
+ --hash=sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36 \
158
+ --hash=sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb \
159
+ --hash=sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d \
160
+ --hash=sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8 \
161
+ --hash=sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0 \
162
+ --hash=sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b \
163
+ --hash=sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7 \
164
+ --hash=sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe \
165
+ --hash=sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77 \
166
+ --hash=sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497 \
167
+ --hash=sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd \
168
+ --hash=sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1 \
169
+ --hash=sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216 \
170
+ --hash=sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13 \
171
+ --hash=sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae \
172
+ --hash=sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae \
173
+ --hash=sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77 \
174
+ --hash=sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3 \
175
+ --hash=sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f \
176
+ --hash=sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff \
177
+ --hash=sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9 \
178
+ --hash=sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a
179
+ # via matplotlib
180
+ cycler==0.12.1 \
181
+ --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \
182
+ --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c
183
+ # via matplotlib
184
+ easydict==1.13 \
185
+ --hash=sha256:6b787daf4dcaf6377b4ad9403a5cee5a86adbc0ca9a5bcf5410e9902002aeac2 \
186
+ --hash=sha256:b1135dedbc41c8010e2bc1f77ec9744c7faa42bce1a1c87416791449d6c87780
187
+ # via -r requirements/bootstrap.in
188
+ einops==0.8.2 \
189
+ --hash=sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193 \
190
+ --hash=sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827
191
+ # via -r requirements/bootstrap.in
192
+ filelock==3.32.2 \
193
+ --hash=sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82 \
194
+ --hash=sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8
195
+ # via
196
+ # -r requirements/bootstrap.in
197
+ # huggingface-hub
198
+ # transformers
199
+ fonttools==4.63.0 \
200
+ --hash=sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69 \
201
+ --hash=sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c \
202
+ --hash=sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac \
203
+ --hash=sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096 \
204
+ --hash=sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d \
205
+ --hash=sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68 \
206
+ --hash=sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616 \
207
+ --hash=sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78 \
208
+ --hash=sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f \
209
+ --hash=sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b \
210
+ --hash=sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b \
211
+ --hash=sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02 \
212
+ --hash=sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d \
213
+ --hash=sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f \
214
+ --hash=sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8 \
215
+ --hash=sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272 \
216
+ --hash=sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49 \
217
+ --hash=sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419 \
218
+ --hash=sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001 \
219
+ --hash=sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03 \
220
+ --hash=sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196 \
221
+ --hash=sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9 \
222
+ --hash=sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e \
223
+ --hash=sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5 \
224
+ --hash=sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007 \
225
+ --hash=sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380 \
226
+ --hash=sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8 \
227
+ --hash=sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27 \
228
+ --hash=sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40 \
229
+ --hash=sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e \
230
+ --hash=sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0 \
231
+ --hash=sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263 \
232
+ --hash=sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb \
233
+ --hash=sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94 \
234
+ --hash=sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b \
235
+ --hash=sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6 \
236
+ --hash=sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579 \
237
+ --hash=sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4 \
238
+ --hash=sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59 \
239
+ --hash=sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0 \
240
+ --hash=sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e \
241
+ --hash=sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be \
242
+ --hash=sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd \
243
+ --hash=sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18 \
244
+ --hash=sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22 \
245
+ --hash=sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0 \
246
+ --hash=sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b \
247
+ --hash=sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b \
248
+ --hash=sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af \
249
+ --hash=sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745
250
+ # via matplotlib
251
+ fsspec==2026.7.0 \
252
+ --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \
253
+ --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88
254
+ # via
255
+ # -r requirements/bootstrap.in
256
+ # huggingface-hub
257
+ hatchling==1.27.0 \
258
+ --hash=sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6 \
259
+ --hash=sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b
260
+ # via -r requirements/bootstrap.in
261
+ hf-xet==1.5.2 \
262
+ --hash=sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed \
263
+ --hash=sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d \
264
+ --hash=sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b \
265
+ --hash=sha256:580e59e29bf37aece1f2b68537de1e3fb04f43a23d910dcf6f128280b5bfbba4 \
266
+ --hash=sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f \
267
+ --hash=sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47 \
268
+ --hash=sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025 \
269
+ --hash=sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576 \
270
+ --hash=sha256:8d7446f72abbf7e01ca5ff131786bc2e74a56393462c17a6bf1e303fbab81db4 \
271
+ --hash=sha256:bee28c619622d36968056532fd49cf2b35ca75099b1d616c31a618a893491380 \
272
+ --hash=sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097 \
273
+ --hash=sha256:d6f9c58549407b84b9a5383afd68db0acc42345326a3159990b36a5ca8a20e4e \
274
+ --hash=sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c \
275
+ --hash=sha256:e396ab0faf6298199ad7a95305c3ca8498cb825978a6485be6d00587ee4ec577 \
276
+ --hash=sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65 \
277
+ --hash=sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799 \
278
+ --hash=sha256:fd3add255549e8ef58fa35b2e42dc016961c050600444e7d77d030ba6b57120e
279
+ # via huggingface-hub
280
+ huggingface-hub==0.36.2 \
281
+ --hash=sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a \
282
+ --hash=sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270
283
+ # via
284
+ # -r requirements/bootstrap.in
285
+ # tokenizers
286
+ # transformers
287
+ idna==3.18 \
288
+ --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
289
+ --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
290
+ # via requests
291
+ jinja2==3.1.6 \
292
+ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
293
+ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
294
+ # via -r requirements/bootstrap.in
295
+ kiwisolver==1.5.0 \
296
+ --hash=sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9 \
297
+ --hash=sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679 \
298
+ --hash=sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0 \
299
+ --hash=sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8 \
300
+ --hash=sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276 \
301
+ --hash=sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96 \
302
+ --hash=sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e \
303
+ --hash=sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac \
304
+ --hash=sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f \
305
+ --hash=sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a \
306
+ --hash=sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15 \
307
+ --hash=sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7 \
308
+ --hash=sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368 \
309
+ --hash=sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02 \
310
+ --hash=sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9 \
311
+ --hash=sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681 \
312
+ --hash=sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57 \
313
+ --hash=sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27 \
314
+ --hash=sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4 \
315
+ --hash=sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920 \
316
+ --hash=sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374 \
317
+ --hash=sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3 \
318
+ --hash=sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa \
319
+ --hash=sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23 \
320
+ --hash=sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859 \
321
+ --hash=sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb \
322
+ --hash=sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d \
323
+ --hash=sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc \
324
+ --hash=sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581 \
325
+ --hash=sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c \
326
+ --hash=sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099 \
327
+ --hash=sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05 \
328
+ --hash=sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9 \
329
+ --hash=sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd \
330
+ --hash=sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc \
331
+ --hash=sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796 \
332
+ --hash=sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303 \
333
+ --hash=sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca \
334
+ --hash=sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314 \
335
+ --hash=sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489 \
336
+ --hash=sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57 \
337
+ --hash=sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1 \
338
+ --hash=sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797 \
339
+ --hash=sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021 \
340
+ --hash=sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db \
341
+ --hash=sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22 \
342
+ --hash=sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028 \
343
+ --hash=sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083 \
344
+ --hash=sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65 \
345
+ --hash=sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588 \
346
+ --hash=sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0 \
347
+ --hash=sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a \
348
+ --hash=sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1 \
349
+ --hash=sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c \
350
+ --hash=sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac \
351
+ --hash=sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476 \
352
+ --hash=sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53 \
353
+ --hash=sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3 \
354
+ --hash=sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4 \
355
+ --hash=sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615 \
356
+ --hash=sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb \
357
+ --hash=sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18 \
358
+ --hash=sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b \
359
+ --hash=sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1 \
360
+ --hash=sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2 \
361
+ --hash=sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c \
362
+ --hash=sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac \
363
+ --hash=sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d \
364
+ --hash=sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf \
365
+ --hash=sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2 \
366
+ --hash=sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f \
367
+ --hash=sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f \
368
+ --hash=sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4 \
369
+ --hash=sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9 \
370
+ --hash=sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e \
371
+ --hash=sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737 \
372
+ --hash=sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b \
373
+ --hash=sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed \
374
+ --hash=sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3 \
375
+ --hash=sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7 \
376
+ --hash=sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08 \
377
+ --hash=sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e \
378
+ --hash=sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902 \
379
+ --hash=sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd \
380
+ --hash=sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6 \
381
+ --hash=sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310 \
382
+ --hash=sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537 \
383
+ --hash=sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554 \
384
+ --hash=sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e \
385
+ --hash=sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87 \
386
+ --hash=sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a \
387
+ --hash=sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c \
388
+ --hash=sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79 \
389
+ --hash=sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e \
390
+ --hash=sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16 \
391
+ --hash=sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1 \
392
+ --hash=sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875 \
393
+ --hash=sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd \
394
+ --hash=sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0 \
395
+ --hash=sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9 \
396
+ --hash=sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646 \
397
+ --hash=sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657 \
398
+ --hash=sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4 \
399
+ --hash=sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232 \
400
+ --hash=sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 \
401
+ --hash=sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 \
402
+ --hash=sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309 \
403
+ --hash=sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede \
404
+ --hash=sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2 \
405
+ --hash=sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203 \
406
+ --hash=sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7 \
407
+ --hash=sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df \
408
+ --hash=sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c \
409
+ --hash=sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167 \
410
+ --hash=sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3 \
411
+ --hash=sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09 \
412
+ --hash=sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398
413
+ # via matplotlib
414
+ markupsafe==3.0.3 \
415
+ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
416
+ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \
417
+ --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \
418
+ --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \
419
+ --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \
420
+ --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \
421
+ --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \
422
+ --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \
423
+ --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \
424
+ --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \
425
+ --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \
426
+ --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \
427
+ --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \
428
+ --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \
429
+ --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \
430
+ --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \
431
+ --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \
432
+ --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \
433
+ --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \
434
+ --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \
435
+ --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \
436
+ --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \
437
+ --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \
438
+ --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \
439
+ --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \
440
+ --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \
441
+ --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \
442
+ --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \
443
+ --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \
444
+ --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \
445
+ --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \
446
+ --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \
447
+ --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \
448
+ --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \
449
+ --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \
450
+ --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \
451
+ --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \
452
+ --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \
453
+ --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \
454
+ --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \
455
+ --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
456
+ --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \
457
+ --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \
458
+ --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \
459
+ --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \
460
+ --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \
461
+ --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \
462
+ --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \
463
+ --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \
464
+ --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \
465
+ --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \
466
+ --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \
467
+ --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \
468
+ --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \
469
+ --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \
470
+ --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \
471
+ --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \
472
+ --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \
473
+ --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \
474
+ --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \
475
+ --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \
476
+ --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \
477
+ --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \
478
+ --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \
479
+ --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \
480
+ --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \
481
+ --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \
482
+ --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \
483
+ --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \
484
+ --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \
485
+ --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \
486
+ --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \
487
+ --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \
488
+ --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \
489
+ --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \
490
+ --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \
491
+ --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \
492
+ --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \
493
+ --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \
494
+ --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \
495
+ --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \
496
+ --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \
497
+ --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \
498
+ --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \
499
+ --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \
500
+ --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \
501
+ --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \
502
+ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \
503
+ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50
504
+ # via jinja2
505
+ matplotlib==3.10.8 \
506
+ --hash=sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7 \
507
+ --hash=sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a \
508
+ --hash=sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f \
509
+ --hash=sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3 \
510
+ --hash=sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5 \
511
+ --hash=sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9 \
512
+ --hash=sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2 \
513
+ --hash=sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3 \
514
+ --hash=sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6 \
515
+ --hash=sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f \
516
+ --hash=sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b \
517
+ --hash=sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8 \
518
+ --hash=sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008 \
519
+ --hash=sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b \
520
+ --hash=sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656 \
521
+ --hash=sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958 \
522
+ --hash=sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04 \
523
+ --hash=sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b \
524
+ --hash=sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6 \
525
+ --hash=sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908 \
526
+ --hash=sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c \
527
+ --hash=sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1 \
528
+ --hash=sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d \
529
+ --hash=sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1 \
530
+ --hash=sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c \
531
+ --hash=sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a \
532
+ --hash=sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce \
533
+ --hash=sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a \
534
+ --hash=sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160 \
535
+ --hash=sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1 \
536
+ --hash=sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11 \
537
+ --hash=sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a \
538
+ --hash=sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466 \
539
+ --hash=sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486 \
540
+ --hash=sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78 \
541
+ --hash=sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17 \
542
+ --hash=sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077 \
543
+ --hash=sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565 \
544
+ --hash=sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f \
545
+ --hash=sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50 \
546
+ --hash=sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58 \
547
+ --hash=sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2 \
548
+ --hash=sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645 \
549
+ --hash=sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2 \
550
+ --hash=sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39 \
551
+ --hash=sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf \
552
+ --hash=sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149 \
553
+ --hash=sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22 \
554
+ --hash=sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df \
555
+ --hash=sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4 \
556
+ --hash=sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933 \
557
+ --hash=sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6 \
558
+ --hash=sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8 \
559
+ --hash=sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a \
560
+ --hash=sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7
561
+ # via -r requirements/bootstrap.in
562
+ mpmath==1.3.0 \
563
+ --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \
564
+ --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c
565
+ # via sympy
566
+ networkx==3.6.1 \
567
+ --hash=sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509 \
568
+ --hash=sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762
569
+ # via -r requirements/bootstrap.in
570
+ numpy==1.26.4 \
571
+ --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \
572
+ --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \
573
+ --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \
574
+ --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \
575
+ --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \
576
+ --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \
577
+ --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \
578
+ --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \
579
+ --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \
580
+ --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \
581
+ --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \
582
+ --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \
583
+ --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \
584
+ --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \
585
+ --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \
586
+ --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \
587
+ --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \
588
+ --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \
589
+ --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \
590
+ --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \
591
+ --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \
592
+ --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \
593
+ --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \
594
+ --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \
595
+ --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \
596
+ --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \
597
+ --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \
598
+ --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \
599
+ --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \
600
+ --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \
601
+ --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \
602
+ --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \
603
+ --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \
604
+ --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \
605
+ --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \
606
+ --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f
607
+ # via
608
+ # -r requirements/bootstrap.in
609
+ # contourpy
610
+ # matplotlib
611
+ # transformers
612
+ packaging==26.2 \
613
+ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
614
+ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
615
+ # via
616
+ # hatchling
617
+ # huggingface-hub
618
+ # matplotlib
619
+ # transformers
620
+ pathspec==1.1.1 \
621
+ --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
622
+ --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
623
+ # via hatchling
624
+ pillow==12.1.1 \
625
+ --hash=sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9 \
626
+ --hash=sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da \
627
+ --hash=sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f \
628
+ --hash=sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642 \
629
+ --hash=sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713 \
630
+ --hash=sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850 \
631
+ --hash=sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9 \
632
+ --hash=sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0 \
633
+ --hash=sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9 \
634
+ --hash=sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8 \
635
+ --hash=sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6 \
636
+ --hash=sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd \
637
+ --hash=sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5 \
638
+ --hash=sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c \
639
+ --hash=sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35 \
640
+ --hash=sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1 \
641
+ --hash=sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff \
642
+ --hash=sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38 \
643
+ --hash=sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4 \
644
+ --hash=sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af \
645
+ --hash=sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60 \
646
+ --hash=sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986 \
647
+ --hash=sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13 \
648
+ --hash=sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 \
649
+ --hash=sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e \
650
+ --hash=sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b \
651
+ --hash=sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15 \
652
+ --hash=sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a \
653
+ --hash=sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb \
654
+ --hash=sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d \
655
+ --hash=sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b \
656
+ --hash=sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e \
657
+ --hash=sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a \
658
+ --hash=sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f \
659
+ --hash=sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a \
660
+ --hash=sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce \
661
+ --hash=sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc \
662
+ --hash=sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f \
663
+ --hash=sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586 \
664
+ --hash=sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f \
665
+ --hash=sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9 \
666
+ --hash=sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8 \
667
+ --hash=sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40 \
668
+ --hash=sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60 \
669
+ --hash=sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c \
670
+ --hash=sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0 \
671
+ --hash=sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334 \
672
+ --hash=sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af \
673
+ --hash=sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735 \
674
+ --hash=sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524 \
675
+ --hash=sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf \
676
+ --hash=sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b \
677
+ --hash=sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2 \
678
+ --hash=sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9 \
679
+ --hash=sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7 \
680
+ --hash=sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e \
681
+ --hash=sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4 \
682
+ --hash=sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4 \
683
+ --hash=sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b \
684
+ --hash=sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397 \
685
+ --hash=sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c \
686
+ --hash=sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e \
687
+ --hash=sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029 \
688
+ --hash=sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3 \
689
+ --hash=sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052 \
690
+ --hash=sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984 \
691
+ --hash=sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293 \
692
+ --hash=sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523 \
693
+ --hash=sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f \
694
+ --hash=sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b \
695
+ --hash=sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80 \
696
+ --hash=sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f \
697
+ --hash=sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79 \
698
+ --hash=sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23 \
699
+ --hash=sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8 \
700
+ --hash=sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e \
701
+ --hash=sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3 \
702
+ --hash=sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e \
703
+ --hash=sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36 \
704
+ --hash=sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f \
705
+ --hash=sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5 \
706
+ --hash=sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f \
707
+ --hash=sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6 \
708
+ --hash=sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32 \
709
+ --hash=sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20 \
710
+ --hash=sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202 \
711
+ --hash=sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0 \
712
+ --hash=sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3 \
713
+ --hash=sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563 \
714
+ --hash=sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090 \
715
+ --hash=sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289
716
+ # via
717
+ # -r requirements/bootstrap.in
718
+ # matplotlib
719
+ pluggy==1.6.0 \
720
+ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
721
+ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
722
+ # via hatchling
723
+ psutil==7.2.2 \
724
+ --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \
725
+ --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \
726
+ --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \
727
+ --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \
728
+ --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \
729
+ --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \
730
+ --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \
731
+ --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \
732
+ --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \
733
+ --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \
734
+ --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \
735
+ --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \
736
+ --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \
737
+ --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \
738
+ --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \
739
+ --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \
740
+ --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \
741
+ --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \
742
+ --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \
743
+ --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \
744
+ --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8
745
+ # via -r requirements/bootstrap.in
746
+ pyparsing==3.3.2 \
747
+ --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
748
+ --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
749
+ # via matplotlib
750
+ pypdfium2==5.12.1 \
751
+ --hash=sha256:05bab9b1ba2de7fc299ae2af25cb9c8a0543bc8bb893e879fe8c9ba8310e9ce4 \
752
+ --hash=sha256:05bfa20a08a96584253bbe38b60e13f81a037eac31c5579e607ec1480ad25dbf \
753
+ --hash=sha256:07eeebb2784f4cd38d386b924235df43217a397442796673296bb6efbdaad1d0 \
754
+ --hash=sha256:236dbdc88aa54f14b27937ccb2ebe3dcf08c10dbb8652f432ea982dc9af39732 \
755
+ --hash=sha256:4648f0905441bcb141687ca2263bbf38a1aa056b943eef06019f91cff3e1da4a \
756
+ --hash=sha256:5c3e6cbe43581af79526184643920ab03a9401a0c79f2226bea9d4d1e3d34008 \
757
+ --hash=sha256:5f257bb40fa44ce9ba18d2c919777dbd3f16bf22548b1d68fd56c7c92f1de530 \
758
+ --hash=sha256:66a9ed40d70a5d728cd42148fecb9d7a0917c6161d6bb67c844093a4ed1df089 \
759
+ --hash=sha256:6eabf028ad8e7bc7811c9acf3a72718c180569b624b844d2c6cc974609784275 \
760
+ --hash=sha256:715ae16b34ea1d64884d58800155179ba700e9ea65a2f583b020666acd2bfb12 \
761
+ --hash=sha256:7857cfa6642ec5a09db12ff8f5cf6b6494585b5e3a605399fddc4fb862837b63 \
762
+ --hash=sha256:847378a5ab41332998b2621b21bab2e96dc8c3eff36a08bce26695b964163983 \
763
+ --hash=sha256:9609be73a6701a68f29dffe0335f7a2e4b3ba581542ed65d35d49f761a4600ca \
764
+ --hash=sha256:974082344172da76a5c3c0782eaedfe6069dbe88db77d8c671ef36b61e9b14e2 \
765
+ --hash=sha256:9c8856ce7dd77a7827476c7d75afe1197d6cd505f5cb4167b6aacf661f3f8ea5 \
766
+ --hash=sha256:9f059f7bdbdf4352eb83691071096940d769d6ae5930b8734237fdb1bd78fbc2 \
767
+ --hash=sha256:afc0b7e0c975a429abc75875209ce17b66d749f6ac5cbe8ba72470e83901e304 \
768
+ --hash=sha256:bdff622181fab64f32328591c9c8287cdc745c9a1f2afc26ca3feba39e3e6645 \
769
+ --hash=sha256:d0e0648fb2e28f50efcd1ec0a5a18ced9f4d66b2c227fae9b603f0a883b2d13f \
770
+ --hash=sha256:d4ee061e566a6422b660cdddaaa799a2d1cbf2f016921bcaf24d61426d01d942 \
771
+ --hash=sha256:e10cbf41b21233ec5e20adfc170cf60edd77abead86a97dc708fff55a8a886c7 \
772
+ --hash=sha256:e5358d2ce4ebc5c899aab1df9ca5d215357244e9168aa443225d3c1e649c7eac
773
+ # via -r requirements/bootstrap.in
774
+ python-dateutil==2.9.0.post0 \
775
+ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
776
+ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
777
+ # via matplotlib
778
+ pyyaml==6.0.3 \
779
+ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
780
+ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
781
+ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
782
+ --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
783
+ --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
784
+ --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
785
+ --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
786
+ --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
787
+ --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
788
+ --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
789
+ --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
790
+ --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
791
+ --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
792
+ --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
793
+ --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
794
+ --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
795
+ --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
796
+ --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
797
+ --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
798
+ --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
799
+ --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
800
+ --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
801
+ --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
802
+ --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
803
+ --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
804
+ --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
805
+ --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
806
+ --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
807
+ --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
808
+ --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
809
+ --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
810
+ --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
811
+ --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
812
+ --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
813
+ --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
814
+ --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
815
+ --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
816
+ --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
817
+ --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
818
+ --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
819
+ --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
820
+ --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
821
+ --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
822
+ --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
823
+ --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
824
+ --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
825
+ --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
826
+ --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
827
+ --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
828
+ --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
829
+ --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
830
+ --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
831
+ --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
832
+ --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
833
+ --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
834
+ --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
835
+ --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
836
+ --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
837
+ --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
838
+ --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
839
+ --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
840
+ --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
841
+ --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
842
+ --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
843
+ --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
844
+ --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
845
+ --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
846
+ --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
847
+ --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
848
+ --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
849
+ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
850
+ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
851
+ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
852
+ # via
853
+ # huggingface-hub
854
+ # transformers
855
+ regex==2026.7.19 \
856
+ --hash=sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0 \
857
+ --hash=sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6 \
858
+ --hash=sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62 \
859
+ --hash=sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af \
860
+ --hash=sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc \
861
+ --hash=sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13 \
862
+ --hash=sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd \
863
+ --hash=sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951 \
864
+ --hash=sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc \
865
+ --hash=sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511 \
866
+ --hash=sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12 \
867
+ --hash=sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518 \
868
+ --hash=sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db \
869
+ --hash=sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae \
870
+ --hash=sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009 \
871
+ --hash=sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986 \
872
+ --hash=sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1 \
873
+ --hash=sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a \
874
+ --hash=sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2 \
875
+ --hash=sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0 \
876
+ --hash=sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78 \
877
+ --hash=sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d \
878
+ --hash=sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4 \
879
+ --hash=sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0 \
880
+ --hash=sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11 \
881
+ --hash=sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52 \
882
+ --hash=sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e \
883
+ --hash=sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902 \
884
+ --hash=sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11 \
885
+ --hash=sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6 \
886
+ --hash=sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba \
887
+ --hash=sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e \
888
+ --hash=sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac \
889
+ --hash=sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939 \
890
+ --hash=sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb \
891
+ --hash=sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc \
892
+ --hash=sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095 \
893
+ --hash=sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b \
894
+ --hash=sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b \
895
+ --hash=sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220 \
896
+ --hash=sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c \
897
+ --hash=sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae \
898
+ --hash=sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3 \
899
+ --hash=sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44 \
900
+ --hash=sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665 \
901
+ --hash=sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5 \
902
+ --hash=sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97 \
903
+ --hash=sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218 \
904
+ --hash=sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864 \
905
+ --hash=sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e \
906
+ --hash=sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4 \
907
+ --hash=sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda \
908
+ --hash=sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459 \
909
+ --hash=sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18 \
910
+ --hash=sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3 \
911
+ --hash=sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5 \
912
+ --hash=sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a \
913
+ --hash=sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035 \
914
+ --hash=sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa \
915
+ --hash=sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5 \
916
+ --hash=sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78 \
917
+ --hash=sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20 \
918
+ --hash=sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a \
919
+ --hash=sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a \
920
+ --hash=sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a \
921
+ --hash=sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965 \
922
+ --hash=sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5 \
923
+ --hash=sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797 \
924
+ --hash=sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276 \
925
+ --hash=sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c \
926
+ --hash=sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547 \
927
+ --hash=sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9 \
928
+ --hash=sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d \
929
+ --hash=sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1 \
930
+ --hash=sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68 \
931
+ --hash=sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a \
932
+ --hash=sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd \
933
+ --hash=sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c \
934
+ --hash=sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6 \
935
+ --hash=sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82 \
936
+ --hash=sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7 \
937
+ --hash=sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15 \
938
+ --hash=sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e \
939
+ --hash=sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38 \
940
+ --hash=sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96 \
941
+ --hash=sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2 \
942
+ --hash=sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8 \
943
+ --hash=sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732 \
944
+ --hash=sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966 \
945
+ --hash=sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053 \
946
+ --hash=sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3 \
947
+ --hash=sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0 \
948
+ --hash=sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f \
949
+ --hash=sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e \
950
+ --hash=sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327 \
951
+ --hash=sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac \
952
+ --hash=sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6 \
953
+ --hash=sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2 \
954
+ --hash=sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a \
955
+ --hash=sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435 \
956
+ --hash=sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5 \
957
+ --hash=sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d \
958
+ --hash=sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312 \
959
+ --hash=sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b \
960
+ --hash=sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40 \
961
+ --hash=sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974 \
962
+ --hash=sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404 \
963
+ --hash=sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff \
964
+ --hash=sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf \
965
+ --hash=sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175 \
966
+ --hash=sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da \
967
+ --hash=sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d \
968
+ --hash=sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1 \
969
+ --hash=sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2
970
+ # via transformers
971
+ requests==2.34.2 \
972
+ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
973
+ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
974
+ # via
975
+ # -r requirements/bootstrap.in
976
+ # huggingface-hub
977
+ # transformers
978
+ safetensors==0.8.0 \
979
+ --hash=sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358 \
980
+ --hash=sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f \
981
+ --hash=sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d \
982
+ --hash=sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d \
983
+ --hash=sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0 \
984
+ --hash=sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc \
985
+ --hash=sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235 \
986
+ --hash=sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98 \
987
+ --hash=sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4 \
988
+ --hash=sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846 \
989
+ --hash=sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca \
990
+ --hash=sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0 \
991
+ --hash=sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25 \
992
+ --hash=sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452 \
993
+ --hash=sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d \
994
+ --hash=sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78 \
995
+ --hash=sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774
996
+ # via
997
+ # -r requirements/bootstrap.in
998
+ # transformers
999
+ setuptools==83.0.0 \
1000
+ --hash=sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef \
1001
+ --hash=sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3
1002
+ # via -r requirements/bootstrap.in
1003
+ six==1.17.0 \
1004
+ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
1005
+ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
1006
+ # via python-dateutil
1007
+ sympy==1.14.0 \
1008
+ --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \
1009
+ --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5
1010
+ # via -r requirements/bootstrap.in
1011
+ tokenizers==0.22.2 \
1012
+ --hash=sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113 \
1013
+ --hash=sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37 \
1014
+ --hash=sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e \
1015
+ --hash=sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001 \
1016
+ --hash=sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee \
1017
+ --hash=sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7 \
1018
+ --hash=sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd \
1019
+ --hash=sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4 \
1020
+ --hash=sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012 \
1021
+ --hash=sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67 \
1022
+ --hash=sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a \
1023
+ --hash=sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5 \
1024
+ --hash=sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917 \
1025
+ --hash=sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c \
1026
+ --hash=sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195 \
1027
+ --hash=sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4 \
1028
+ --hash=sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a \
1029
+ --hash=sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc \
1030
+ --hash=sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92 \
1031
+ --hash=sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5 \
1032
+ --hash=sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48 \
1033
+ --hash=sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b \
1034
+ --hash=sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c \
1035
+ --hash=sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5
1036
+ # via transformers
1037
+ tqdm==4.70.0 \
1038
+ --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \
1039
+ --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953
1040
+ # via
1041
+ # -r requirements/bootstrap.in
1042
+ # huggingface-hub
1043
+ # transformers
1044
+ transformers==4.57.1 \
1045
+ --hash=sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267 \
1046
+ --hash=sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55
1047
+ # via -r requirements/bootstrap.in
1048
+ trove-classifiers==2026.6.1.19 \
1049
+ --hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \
1050
+ --hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745
1051
+ # via hatchling
1052
+ typing-extensions==4.16.0 \
1053
+ --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
1054
+ --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
1055
+ # via
1056
+ # -r requirements/bootstrap.in
1057
+ # huggingface-hub
1058
+ urllib3==2.7.0 \
1059
+ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
1060
+ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
1061
+ # via requests
scripts/bootstrap-rocm.sh ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5
+ PYTHON_BIN=python3.12
6
+ VENV_DIR="$ROOT/.venv"
7
+ DRY_RUN=0
8
+ DEPENDENCIES_ONLY=0
9
+ LOCK_FILE="$ROOT/requirements/bootstrap.lock"
10
+
11
+ usage() {
12
+ printf '%s\n' \
13
+ "Usage: scripts/bootstrap-rocm.sh [--python PATH] [--venv PATH] [--dependencies-only] [--dry-run]" \
14
+ "" \
15
+ "Creates an isolated Python 3.12 environment with AMD's verified ROCm 7.2.1 wheels." \
16
+ "It never modifies the system Python or the host ROCm installation."
17
+ }
18
+
19
+ while (($#)); do
20
+ case "$1" in
21
+ --python)
22
+ PYTHON_BIN=${2:?--python requires a path}
23
+ shift 2
24
+ ;;
25
+ --venv)
26
+ VENV_DIR=${2:?--venv requires a path}
27
+ shift 2
28
+ ;;
29
+ --dependencies-only)
30
+ DEPENDENCIES_ONLY=1
31
+ shift
32
+ ;;
33
+ -n|--dry-run)
34
+ DRY_RUN=1
35
+ shift
36
+ ;;
37
+ -h|--help)
38
+ usage
39
+ exit 0
40
+ ;;
41
+ *)
42
+ printf 'error: unknown argument: %s\n' "$1" >&2
43
+ usage >&2
44
+ exit 2
45
+ ;;
46
+ esac
47
+ done
48
+
49
+ ROCM_RELEASE=7.2.1
50
+ CACHE_BASE=${XDG_CACHE_HOME:-${HOME}/.cache}/unlimited-ocr-rdna4
51
+ WHEEL_DIR="$CACHE_BASE/wheels/rocm-$ROCM_RELEASE"
52
+ TASK_TMP="$CACHE_BASE/tmp"
53
+
54
+ TORCH_FILE='torch-2.9.1+rocm7.2.1.lw.gitff65f5bc-cp312-cp312-linux_x86_64.whl'
55
+ TORCH_URL='https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.1/torch-2.9.1%2Brocm7.2.1.lw.gitff65f5bc-cp312-cp312-linux_x86_64.whl'
56
+ TORCH_SHA='fb45ace0a27e9f0d0e3c4c6efd8932162743f8376f2aa4752a4d31ef5a1bd3d7'
57
+
58
+ VISION_FILE='torchvision-0.24.0+rocm7.2.1.gitb919bd0c-cp312-cp312-linux_x86_64.whl'
59
+ VISION_URL='https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.1/torchvision-0.24.0%2Brocm7.2.1.gitb919bd0c-cp312-cp312-linux_x86_64.whl'
60
+ VISION_SHA='d5fca8cda173235a3b7434baeebe04c3ebffec3c6fc191e79aa8aa300633f2c9'
61
+
62
+ TRITON_FILE='triton-3.5.1+rocm7.2.1.gita272dfa8-cp312-cp312-linux_x86_64.whl'
63
+ TRITON_URL='https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.1/triton-3.5.1%2Brocm7.2.1.gita272dfa8-cp312-cp312-linux_x86_64.whl'
64
+ TRITON_SHA='07787af1d28c273852f897bfeaa7bca29f2fa4a13ca0f28f535832b240ce7016'
65
+
66
+ if ((DRY_RUN)); then
67
+ printf 'python=%s\nvenv=%s\nrocm_release=%s\nwheel_cache=%s\ndependency_lock=%s\n' \
68
+ "$PYTHON_BIN" "$VENV_DIR" "$ROCM_RELEASE" "$WHEEL_DIR" "$LOCK_FILE"
69
+ printf 'dependencies_only=%s\n' "$DEPENDENCIES_ONLY"
70
+ printf 'download=%s\ndownload=%s\ndownload=%s\n' "$TORCH_URL" "$VISION_URL" "$TRITON_URL"
71
+ printf 'next=%s/bin/unlimited-ocr-rdna4 doctor --device 0\n' "$VENV_DIR"
72
+ exit 0
73
+ fi
74
+
75
+ case "$(uname -s)-$(uname -m)" in
76
+ Linux-x86_64) ;;
77
+ *)
78
+ printf 'error: the verified bootstrap supports Linux x86_64 only\n' >&2
79
+ exit 3
80
+ ;;
81
+ esac
82
+
83
+ if [[ ! -r /opt/rocm/.info/version ]]; then
84
+ printf 'error: /opt/rocm/.info/version is missing; install a supported host ROCm stack first\n' >&2
85
+ exit 3
86
+ fi
87
+ HOST_ROCM=$(tr -d '\r\n' < /opt/rocm/.info/version)
88
+ if [[ $HOST_ROCM != "$ROCM_RELEASE" ]]; then
89
+ printf 'error: this bootstrap is verified for host ROCm 7.2.1, found %s\n' "$HOST_ROCM" >&2
90
+ exit 3
91
+ fi
92
+
93
+ PYTHON_VERSION=$("$PYTHON_BIN" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
94
+ if [[ $PYTHON_VERSION != 3.12 ]]; then
95
+ printf 'error: AMD wheel set requires Python 3.12, found %s\n' "$PYTHON_VERSION" >&2
96
+ exit 3
97
+ fi
98
+ command -v curl >/dev/null || { printf 'error: curl is required\n' >&2; exit 3; }
99
+ command -v sha256sum >/dev/null || { printf 'error: sha256sum is required\n' >&2; exit 3; }
100
+ command -v flock >/dev/null || { printf 'error: flock is required\n' >&2; exit 3; }
101
+ [[ -f $LOCK_FILE && ! -L $LOCK_FILE ]] || { printf 'error: dependency lock is missing or symlinked: %s\n' "$LOCK_FILE" >&2; exit 3; }
102
+
103
+ mkdir -p "$WHEEL_DIR" "$TASK_TMP"
104
+ export TMPDIR="$TASK_TMP"
105
+ export PIP_CACHE_DIR="$CACHE_BASE/pip"
106
+ exec 9>"$WHEEL_DIR/.bootstrap.lock"
107
+ flock 9
108
+
109
+ download_wheel() {
110
+ local filename=$1
111
+ local url=$2
112
+ local expected=$3
113
+ local destination="$WHEEL_DIR/$filename"
114
+ local partial="$destination.part"
115
+
116
+ if [[ -L $destination || -L $partial ]]; then
117
+ printf 'error: refusing symlinked wheel cache entry: %s\n' "$destination" >&2
118
+ exit 4
119
+ fi
120
+ if [[ -f $destination ]]; then
121
+ if printf '%s %s\n' "$expected" "$destination" | sha256sum --check --status; then
122
+ printf 'wheel=reused path=%s\n' "$destination"
123
+ return
124
+ fi
125
+ printf 'wheel=discard-corrupt path=%s\n' "$destination" >&2
126
+ rm -f -- "$destination"
127
+ fi
128
+
129
+ for attempt in 1 2; do
130
+ printf 'downloading=%s attempt=%s\n' "$filename" "$attempt"
131
+ if ! curl --fail --location --proto '=https' --tlsv1.2 --continue-at - --output "$partial" "$url"; then
132
+ printf 'wheel=discard-failed-partial path=%s\n' "$partial" >&2
133
+ rm -f -- "$partial"
134
+ continue
135
+ fi
136
+ if printf '%s %s\n' "$expected" "$partial" | sha256sum --check --status; then
137
+ mv "$partial" "$destination"
138
+ return
139
+ fi
140
+ printf 'wheel=discard-corrupt-partial path=%s\n' "$partial" >&2
141
+ rm -f -- "$partial"
142
+ done
143
+ printf 'error: downloaded wheel checksum mismatch after clean retry: %s\n' "$filename" >&2
144
+ exit 4
145
+ }
146
+
147
+ download_wheel "$TORCH_FILE" "$TORCH_URL" "$TORCH_SHA"
148
+ download_wheel "$VISION_FILE" "$VISION_URL" "$VISION_SHA"
149
+ download_wheel "$TRITON_FILE" "$TRITON_URL" "$TRITON_SHA"
150
+
151
+ validate_venv() {
152
+ if [[ -L $VENV_DIR || ! -d $VENV_DIR || -L $VENV_DIR/pyvenv.cfg || ! -f $VENV_DIR/pyvenv.cfg ]]; then
153
+ printf 'error: existing --venv path is not a regular Python virtual environment: %s\n' "$VENV_DIR" >&2
154
+ exit 3
155
+ fi
156
+ if [[ ! -x $VENV_DIR/bin/python ]]; then
157
+ printf 'error: virtual environment has no executable bin/python: %s\n' "$VENV_DIR" >&2
158
+ exit 3
159
+ fi
160
+ "$VENV_DIR/bin/python" - "$VENV_DIR" <<'PY'
161
+ import os
162
+ import sys
163
+
164
+ expected = os.path.realpath(sys.argv[1])
165
+ if os.path.realpath(sys.prefix) != expected or sys.prefix == sys.base_prefix:
166
+ raise SystemExit("virtual environment prefix validation failed")
167
+ if sys.version_info[:2] != (3, 12):
168
+ raise SystemExit(f"virtual environment requires Python 3.12, found {sys.version_info.major}.{sys.version_info.minor}")
169
+ PY
170
+ }
171
+
172
+ if [[ -L $VENV_DIR ]]; then
173
+ printf 'error: --venv path must not be a symlink: %s\n' "$VENV_DIR" >&2
174
+ exit 3
175
+ fi
176
+ if [[ -e $VENV_DIR && ! -d $VENV_DIR ]]; then
177
+ printf 'error: --venv path exists and is not a directory: %s\n' "$VENV_DIR" >&2
178
+ exit 3
179
+ fi
180
+ if [[ ! -e $VENV_DIR ]]; then
181
+ "$PYTHON_BIN" -m venv "$VENV_DIR"
182
+ fi
183
+ validate_venv
184
+ "$VENV_DIR/bin/python" -m pip install --require-hashes --only-binary=:all: -r "$LOCK_FILE"
185
+ "$VENV_DIR/bin/python" -m pip install \
186
+ --no-deps \
187
+ "$WHEEL_DIR/$TORCH_FILE" \
188
+ "$WHEEL_DIR/$VISION_FILE" \
189
+ "$WHEEL_DIR/$TRITON_FILE"
190
+ if ((DEPENDENCIES_ONLY)); then
191
+ "$VENV_DIR/bin/python" -m pip check
192
+ printf 'bootstrap=PASS mode=dependencies-only venv=%s\n' "$VENV_DIR"
193
+ exit 0
194
+ fi
195
+ BUILD_DIR=$(mktemp -d "$TASK_TMP/package-build.XXXXXX")
196
+ trap 'rm -rf -- "$BUILD_DIR"' EXIT
197
+ "$VENV_DIR/bin/python" -m pip wheel --no-build-isolation --no-deps --wheel-dir "$BUILD_DIR" "$ROOT"
198
+ PACKAGE_WHEEL=$(find "$BUILD_DIR" -maxdepth 1 -type f -name 'unlimited_ocr_rdna4-*.whl' -print -quit)
199
+ [[ -n $PACKAGE_WHEEL ]] || { printf 'error: local package wheel was not produced\n' >&2; exit 4; }
200
+ "$VENV_DIR/bin/python" -m pip install --force-reinstall --no-deps "$PACKAGE_WHEEL"
201
+ "$VENV_DIR/bin/python" -m pip check
202
+ "$VENV_DIR/bin/unlimited-ocr-rdna4" doctor --device "${UNLIMITED_OCR_DEVICE:-0}"
203
+ printf 'bootstrap=PASS venv=%s\n' "$VENV_DIR"
scripts/check-validation.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import copy
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from unlimited_ocr_rdna4.constants import MODEL_REVISION, VERIFIED_HIP_VERSION, VERIFIED_PACKAGE_VERSIONS
11
+
12
+
13
+ def _load_json(path: Path) -> dict:
14
+ value = json.loads(path.read_text(encoding="utf-8"))
15
+ if not isinstance(value, dict):
16
+ raise AssertionError(f"expected a JSON object: {path}")
17
+ return value
18
+
19
+
20
+ def _sha256(path: Path) -> str:
21
+ return hashlib.sha256(path.read_bytes()).hexdigest()
22
+
23
+
24
+ def _check_output(path: Path) -> None:
25
+ text = path.read_text(encoding="utf-8")
26
+ ordered = (
27
+ "Unlimited OCR RDNA4 Smoke Test",
28
+ "Items",
29
+ "Document scan",
30
+ "Table extraction",
31
+ "Total",
32
+ "Verification notes",
33
+ "RDNA4-OCR-PASS",
34
+ )
35
+ positions = [text.index(marker) for marker in ordered]
36
+ assert positions == sorted(positions), f"reading order mismatch in {path}"
37
+ assert "€48.50" in text, f"euro table total missing from {path}"
38
+ assert "mc}^2" in text or "mc^2" in text, f"formula missing from {path}"
39
+ assert "<table" in text or "| Item" in text, f"structured table missing from {path}"
40
+
41
+
42
+ def _check_run(payload: dict, *, expected_pages: int) -> None:
43
+ assert payload["schema_version"] == 1
44
+ assert payload["status"] == "ok"
45
+ assert payload["model_revision"] == MODEL_REVISION
46
+ assert payload["architecture"] == "gfx1201"
47
+ assert payload["hardware_verified"] is True
48
+ assert payload["software_verified"] is True
49
+ assert payload["pages_processed"] == expected_pages
50
+ assert payload["pages_total"] == expected_pages
51
+ assert 0 < payload["peak_allocated_gib"] < 16
52
+
53
+
54
+ def main() -> None:
55
+ parser = argparse.ArgumentParser(description="Validate machine-readable RDNA 4 smoke evidence")
56
+ parser.add_argument("--doctor", type=Path, required=True)
57
+ parser.add_argument("--image-run", type=Path, action="append", required=True)
58
+ parser.add_argument("--pdf-run", type=Path, required=True)
59
+ parser.add_argument("--image-output", type=Path, action="append", required=True)
60
+ parser.add_argument("--pdf-output", type=Path, required=True)
61
+ parser.add_argument("--expected-image-sha256")
62
+ parser.add_argument("--expected-pdf-sha256")
63
+ parser.add_argument("--network-isolated", action="store_true")
64
+ parser.add_argument("--gpu-processes", type=Path)
65
+ parser.add_argument("--evidence-output", type=Path)
66
+ args = parser.parse_args()
67
+
68
+ assert len(args.image_run) == 2, "exactly two image-run JSON files are required"
69
+ assert len(args.image_output) == 2, "exactly two image outputs are required"
70
+ doctor = _load_json(args.doctor)
71
+ assert doctor["schema_version"] == 1
72
+ assert doctor["status"] == "ok"
73
+ assert doctor["runtime_issues"] == []
74
+ runtime = doctor["runtime"]
75
+ assert runtime["visible_devices"] == 1
76
+ assert runtime["accepted_architecture"] is True
77
+ assert runtime["hardware_verified"] is True
78
+ assert runtime["software_verified"] is True
79
+ assert runtime["hip_version"] == VERIFIED_HIP_VERSION
80
+ assert runtime["package_versions"] == VERIFIED_PACKAGE_VERSIONS
81
+ assert doctor["model"]["prepared"] is True
82
+ assert doctor["model"]["revision"] == MODEL_REVISION
83
+
84
+ image_runs = [_load_json(run_path) for run_path in args.image_run]
85
+ pdf_run = _load_json(args.pdf_run)
86
+ for image_run in image_runs:
87
+ _check_run(image_run, expected_pages=1)
88
+ _check_run(pdf_run, expected_pages=1)
89
+ for output_path in (*args.image_output, args.pdf_output):
90
+ _check_output(output_path)
91
+
92
+ image_hashes = [_sha256(path) for path in args.image_output]
93
+ assert image_hashes[0] == image_hashes[1], "fresh-process image outputs are not byte-identical"
94
+ pdf_hash = _sha256(args.pdf_output)
95
+ if args.expected_image_sha256:
96
+ assert image_hashes[0] == args.expected_image_sha256
97
+ if args.expected_pdf_sha256:
98
+ assert pdf_hash == args.expected_pdf_sha256
99
+ if args.network_isolated:
100
+ assert args.network_isolated is True
101
+ processes = _load_json_array(args.gpu_processes) if args.gpu_processes else None
102
+ if processes is not None:
103
+ names = []
104
+ for gpu in processes:
105
+ for process in gpu.get("process_list", []):
106
+ process_info = process.get("process_info")
107
+ if isinstance(process_info, dict) and isinstance(process_info.get("name"), str):
108
+ names.append(process_info["name"])
109
+ assert not any("python" in name.lower() for name in names), f"OCR Python process still owns VRAM: {names}"
110
+
111
+ summary = {
112
+ "schema_version": 1,
113
+ "validation": "PASS",
114
+ "network_isolated": args.network_isolated,
115
+ "image_sha256": image_hashes[0],
116
+ "pdf_sha256": pdf_hash,
117
+ }
118
+ if args.evidence_output:
119
+ doctor_evidence = copy.deepcopy(doctor)
120
+ doctor_evidence["model"]["model_dir"] = "<prepared-model>"
121
+ image_evidence = copy.deepcopy(image_runs)
122
+ for index, image_run in enumerate(image_evidence, start=1):
123
+ image_run["input"] = "tests/fixtures/rdna4-smoke.png"
124
+ image_run["output"] = f"<temporary-output>/smoke-image-{index}.md"
125
+ pdf_evidence = copy.deepcopy(pdf_run)
126
+ pdf_evidence["input"] = "tests/fixtures/rdna4-smoke.pdf"
127
+ pdf_evidence["output"] = "<temporary-output>/smoke-pdf.md"
128
+ evidence = {
129
+ **summary,
130
+ "fixtures": {
131
+ "image_sha256": "f5099e17be868abfb4213dbdab220deac82a2db93ba87ab22f03219178246972",
132
+ "pdf_sha256": "cdd2b484d0ac90bd98b489dd97565a65eb17246f713359524cddd41d78cc10cb",
133
+ },
134
+ "doctor": doctor_evidence,
135
+ "image_runs": image_evidence,
136
+ "pdf_run": pdf_evidence,
137
+ "gpu_processes_after": processes,
138
+ }
139
+ args.evidence_output.parent.mkdir(parents=True, exist_ok=True)
140
+ args.evidence_output.write_text(json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8")
141
+ print(json.dumps(summary, sort_keys=True))
142
+
143
+
144
+ def _load_json_array(path: Path) -> list[dict]:
145
+ value = json.loads(path.read_text(encoding="utf-8"))
146
+ if not isinstance(value, list):
147
+ raise AssertionError(f"expected a JSON array: {path}")
148
+ return value
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
scripts/make-smoke-fixture.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ from PIL import Image, ImageDraw, ImageFont
8
+
9
+
10
+ def resolve_font_pair(regular_override: Path | None, bold_override: Path | None) -> tuple[Path, Path]:
11
+ if (regular_override is None) != (bold_override is None):
12
+ raise SystemExit("--regular-font and --bold-font must be supplied together")
13
+ if regular_override and bold_override:
14
+ if not regular_override.is_file() or not bold_override.is_file():
15
+ raise SystemExit("one or both explicit font paths do not exist")
16
+ return regular_override, bold_override
17
+
18
+ families = (
19
+ ("DejaVuSans.ttf", "DejaVuSans-Bold.ttf"),
20
+ ("LiberationSans-Regular.ttf", "LiberationSans-Bold.ttf"),
21
+ )
22
+ roots = (Path("/usr/share/fonts"), Path("/usr/local/share/fonts"))
23
+ for regular_name, bold_name in families:
24
+ for root in roots:
25
+ regular_matches = sorted(root.rglob(regular_name)) if root.is_dir() else []
26
+ bold_matches = sorted(root.rglob(bold_name)) if root.is_dir() else []
27
+ if regular_matches and bold_matches:
28
+ return regular_matches[0], bold_matches[0]
29
+ raise SystemExit("no DejaVu Sans or Liberation Sans font pair found; pass --regular-font and --bold-font")
30
+
31
+
32
+ def main() -> None:
33
+ parser = argparse.ArgumentParser(description="Generate a deterministic OCR validation page")
34
+ parser.add_argument("--output", type=Path, required=True)
35
+ parser.add_argument("--pdf-output", type=Path, help="also write the page as a 150-DPI PDF")
36
+ parser.add_argument("--regular-font", type=Path, help="regular TrueType/OpenType font override")
37
+ parser.add_argument("--bold-font", type=Path, help="bold TrueType/OpenType font override")
38
+ args = parser.parse_args()
39
+
40
+ regular_path, bold_path = resolve_font_pair(args.regular_font, args.bold_font)
41
+ image = Image.new("RGB", (1600, 2000), "white")
42
+ draw = ImageDraw.Draw(image)
43
+ title = ImageFont.truetype(str(bold_path), 68)
44
+ heading = ImageFont.truetype(str(bold_path), 38)
45
+ body = ImageFont.truetype(str(regular_path), 34)
46
+ small = ImageFont.truetype(str(regular_path), 28)
47
+
48
+ draw.text((120, 100), "Unlimited OCR RDNA4 Smoke Test", font=title, fill="black")
49
+ draw.line((120, 205, 1480, 205), fill="black", width=4)
50
+ draw.text((120, 265), "Invoice No. OCR-2026-0731", font=body, fill="black")
51
+ draw.text((120, 325), "Date: 31 July 2026", font=body, fill="black")
52
+ draw.text((120, 385), "GPU target: AMD gfx1201", font=body, fill="black")
53
+
54
+ draw.text((120, 505), "Items", font=heading, fill="black")
55
+ left, top, right, bottom = 120, 580, 1480, 1050
56
+ columns = (left, 930, 1120, right)
57
+ rows = (top, 690, 810, 930, bottom)
58
+ for x in columns:
59
+ draw.line((x, top, x, bottom), fill="black", width=3)
60
+ for y in rows:
61
+ draw.line((left, y, right, y), fill="black", width=3)
62
+ draw.text((145, 610), "Item", font=heading, fill="black")
63
+ draw.text((960, 610), "Qty", font=heading, fill="black")
64
+ draw.text((1150, 610), "Price", font=heading, fill="black")
65
+ draw.text((145, 725), "Document scan", font=body, fill="black")
66
+ draw.text((980, 725), "2", font=body, fill="black")
67
+ draw.text((1150, 725), "€19.50", font=body, fill="black")
68
+ draw.text((145, 845), "Table extraction", font=body, fill="black")
69
+ draw.text((980, 845), "1", font=body, fill="black")
70
+ draw.text((1150, 845), "€9.50", font=body, fill="black")
71
+ draw.text((145, 965), "Total", font=heading, fill="black")
72
+ draw.text((1150, 965), "€48.50", font=heading, fill="black")
73
+
74
+ draw.text((120, 1190), "Verification notes", font=heading, fill="black")
75
+ notes = [
76
+ "• AMD Radeon RX 9070 XT",
77
+ "• ROCm architecture: gfx1201",
78
+ "• Expected checksum: RDNA4-OCR-PASS",
79
+ "• Formula sample: E = mc²",
80
+ ]
81
+ for index, note in enumerate(notes):
82
+ draw.text((150, 1270 + index * 70), note, font=body, fill="black")
83
+
84
+ draw.line((120, 1780, 1480, 1780), fill="black", width=2)
85
+ draw.text((120, 1820), "Synthetic validation page; no private data.", font=small, fill="black")
86
+ args.output.parent.mkdir(parents=True, exist_ok=True)
87
+ image.save(args.output, format="PNG", optimize=True)
88
+ print(args.output)
89
+
90
+ if args.pdf_output:
91
+ args.pdf_output.parent.mkdir(parents=True, exist_ok=True)
92
+ image.save(args.pdf_output, format="PDF", resolution=150.0)
93
+ print(args.pdf_output)
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
scripts/validate-smoke.sh ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5
+ CLI=${CLI:-$ROOT/.venv/bin/unlimited-ocr-rdna4}
6
+ PYTHON=${PYTHON:-$ROOT/.venv/bin/python}
7
+ DEVICE=${UNLIMITED_OCR_DEVICE:-0}
8
+ MODEL_DIR=${UNLIMITED_OCR_MODEL_DIR:-}
9
+ AMD_SMI_GPU=${AMD_SMI_GPU:-}
10
+ NETWORK_ISOLATED=${VALIDATION_NETWORK_ISOLATED:-0}
11
+ EXPECTED_IMAGE_SHA256=${EXPECTED_IMAGE_SHA256:-13df40051aceb3bd14f75622697dda1f2a96515bb95c0483e0920cd4d41cbc01}
12
+ EXPECTED_PDF_SHA256=${EXPECTED_PDF_SHA256:-fb17a6396c3e73a025e388a09f73357960ca0bf95124e38179e06a2e64442438}
13
+ EVIDENCE_OUTPUT=${VALIDATION_EVIDENCE_OUTPUT:-}
14
+ CACHE_BASE=${XDG_CACHE_HOME:-${HOME}/.cache}/unlimited-ocr-rdna4
15
+ FIXTURE_IMAGE="$ROOT/tests/fixtures/rdna4-smoke.png"
16
+ FIXTURE_PDF="$ROOT/tests/fixtures/rdna4-smoke.pdf"
17
+ mkdir -p "$CACHE_BASE"
18
+ WORK_DIR=$(mktemp -d "$CACHE_BASE/validation.XXXXXX")
19
+ trap 'rm -rf -- "$WORK_DIR"' EXIT
20
+
21
+ printf '%s %s\n' \
22
+ 'f5099e17be868abfb4213dbdab220deac82a2db93ba87ab22f03219178246972' "$FIXTURE_IMAGE" \
23
+ 'cdd2b484d0ac90bd98b489dd97565a65eb17246f713359524cddd41d78cc10cb' "$FIXTURE_PDF" |
24
+ sha256sum --check --status
25
+
26
+ MODEL_ARGS=()
27
+ if [[ -n $MODEL_DIR ]]; then
28
+ MODEL_ARGS=(--model-dir "$MODEL_DIR")
29
+ fi
30
+
31
+ run_isolated() {
32
+ sudo -n systemd-run \
33
+ --quiet --wait --pipe --collect \
34
+ -p PrivateNetwork=yes \
35
+ -p "User=$(id -un)" \
36
+ -p "WorkingDirectory=$ROOT" \
37
+ -p "Environment=HOME=$HOME" \
38
+ -- "$@"
39
+ }
40
+
41
+ run_ocr() {
42
+ if [[ $NETWORK_ISOLATED == 1 ]]; then
43
+ run_isolated "$@"
44
+ else
45
+ "$@"
46
+ fi
47
+ }
48
+
49
+ if [[ $NETWORK_ISOLATED == 1 ]]; then
50
+ if run_isolated /usr/bin/python3 -c \
51
+ 'import socket; socket.create_connection(("1.1.1.1", 80), timeout=0.25)' \
52
+ >"$WORK_DIR/network-probe.out" 2>"$WORK_DIR/network-probe.err"; then
53
+ printf 'error: PrivateNetwork validation probe unexpectedly reached the network\n' >&2
54
+ exit 5
55
+ fi
56
+ fi
57
+
58
+ "$CLI" doctor --device "$DEVICE" --require-model --json "${MODEL_ARGS[@]}" >"$WORK_DIR/doctor.json"
59
+
60
+ run_ocr "$CLI" run \
61
+ --input "$FIXTURE_IMAGE" \
62
+ --output "$WORK_DIR/smoke-image-1.md" \
63
+ --device "$DEVICE" \
64
+ --json \
65
+ "${MODEL_ARGS[@]}" >"$WORK_DIR/image-1.json"
66
+
67
+ run_ocr "$CLI" run \
68
+ --input "$FIXTURE_IMAGE" \
69
+ --output "$WORK_DIR/smoke-image-2.md" \
70
+ --device "$DEVICE" \
71
+ --json \
72
+ "${MODEL_ARGS[@]}" >"$WORK_DIR/image-2.json"
73
+
74
+ run_ocr "$CLI" run \
75
+ --input "$FIXTURE_PDF" \
76
+ --output "$WORK_DIR/smoke-pdf.md" \
77
+ --device "$DEVICE" \
78
+ --dpi 150 \
79
+ --max-pages 1 \
80
+ --json \
81
+ "${MODEL_ARGS[@]}" >"$WORK_DIR/pdf.json"
82
+
83
+ if [[ -n $AMD_SMI_GPU ]]; then
84
+ amd-smi process -g "$AMD_SMI_GPU" --json >"$WORK_DIR/gpu-processes.json"
85
+ fi
86
+
87
+ CHECK_ARGS=(
88
+ --doctor "$WORK_DIR/doctor.json"
89
+ --image-run "$WORK_DIR/image-1.json"
90
+ --image-run "$WORK_DIR/image-2.json"
91
+ --pdf-run "$WORK_DIR/pdf.json"
92
+ --image-output "$WORK_DIR/smoke-image-1.md"
93
+ --image-output "$WORK_DIR/smoke-image-2.md"
94
+ --pdf-output "$WORK_DIR/smoke-pdf.md"
95
+ )
96
+ CHECK_ARGS+=(--expected-image-sha256 "$EXPECTED_IMAGE_SHA256")
97
+ CHECK_ARGS+=(--expected-pdf-sha256 "$EXPECTED_PDF_SHA256")
98
+ if [[ $NETWORK_ISOLATED == 1 ]]; then
99
+ CHECK_ARGS+=(--network-isolated)
100
+ fi
101
+ if [[ -n $AMD_SMI_GPU ]]; then
102
+ CHECK_ARGS+=(--gpu-processes "$WORK_DIR/gpu-processes.json")
103
+ fi
104
+ if [[ -n $EVIDENCE_OUTPUT ]]; then
105
+ CHECK_ARGS+=(--evidence-output "$EVIDENCE_OUTPUT")
106
+ fi
107
+
108
+ "$PYTHON" "$ROOT/scripts/check-validation.py" "${CHECK_ARGS[@]}"
src/unlimited_ocr_rdna4/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Unlimited-OCR runtime for AMD RDNA 4 GPUs."""
2
+
3
+ from .constants import VERSION
4
+
5
+ __all__ = ["VERSION"]
6
+ __version__ = VERSION
src/unlimited_ocr_rdna4/__main__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
src/unlimited_ocr_rdna4/cli.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ import traceback
7
+ from pathlib import Path
8
+
9
+ from .constants import MODEL_REVISION, VERSION
10
+ from .errors import AppError, ModelIntegrityError
11
+ from .infer import RunOptions, default_output_path, run_inference
12
+ from .model_store import prepare_model, verify_model
13
+ from .paths import default_cache_dir, default_model_dir
14
+ from .runtime import inspect_runtime, runtime_issues
15
+
16
+
17
+ def bounded_int(minimum: int, maximum: int):
18
+ def parse(value: str) -> int:
19
+ number = int(value)
20
+ if not minimum <= number <= maximum:
21
+ raise argparse.ArgumentTypeError(f"must be between {minimum} and {maximum}")
22
+ return number
23
+
24
+ return parse
25
+
26
+
27
+ def _add_output_flags(parser: argparse.ArgumentParser, *, suppress_defaults: bool = False) -> None:
28
+ default = argparse.SUPPRESS if suppress_defaults else False
29
+ parser.add_argument("--json", action="store_true", default=default, help="emit structured JSON")
30
+ parser.add_argument("-q", "--quiet", action="store_true", default=default, help="suppress progress diagnostics")
31
+ parser.add_argument("--debug", action="store_true", default=default, help="show tracebacks for unexpected failures")
32
+ parser.add_argument(
33
+ "--no-color", action="store_true", default=default, help="disable color (currently the default)"
34
+ )
35
+
36
+
37
+ def build_parser() -> argparse.ArgumentParser:
38
+ parser = argparse.ArgumentParser(
39
+ prog="unlimited-ocr-rdna4",
40
+ description="Run Baidu Unlimited-OCR locally on a single AMD RDNA 4 GPU.",
41
+ epilog="Examples: unlimited-ocr-rdna4 prepare; unlimited-ocr-rdna4 run --input page.png",
42
+ )
43
+ _add_output_flags(parser)
44
+ parser.add_argument("--version", action="version", version=VERSION)
45
+ subparsers = parser.add_subparsers(dest="command")
46
+
47
+ prepare = subparsers.add_parser("prepare", help="download and verify the pinned Baidu model")
48
+ _add_output_flags(prepare, suppress_defaults=True)
49
+ prepare.add_argument("--model-dir", type=Path, default=default_model_dir(), help="prepared model destination")
50
+ prepare.add_argument("--cache-dir", type=Path, default=default_cache_dir(), help="persistent download cache")
51
+ prepare.add_argument("-n", "--dry-run", action="store_true", help="show the plan without downloading")
52
+
53
+ doctor = subparsers.add_parser("doctor", help="inspect ROCm, PyTorch, RDNA 4, and model readiness")
54
+ _add_output_flags(doctor, suppress_defaults=True)
55
+ doctor.add_argument("--device", help="ROCm ordinal or stable ROCr UUID; omit to inspect all visible GPUs")
56
+ doctor.add_argument("--model-dir", type=Path, default=default_model_dir(), help="prepared model directory")
57
+ doctor.add_argument("--require-model", action="store_true", help="fail when the prepared model is absent")
58
+
59
+ run = subparsers.add_parser("run", help="parse one image or PDF into Markdown")
60
+ _add_output_flags(run, suppress_defaults=True)
61
+ run.add_argument("--input", required=True, type=Path, help="input image or PDF")
62
+ run.add_argument("-o", "--output", type=Path, help="Markdown output path")
63
+ run.add_argument(
64
+ "--device",
65
+ default=None,
66
+ help="ROCm ordinal or stable ROCr UUID (default: UNLIMITED_OCR_DEVICE or 0)",
67
+ )
68
+ run.add_argument("--model-dir", type=Path, default=default_model_dir(), help="prepared model directory")
69
+ run.add_argument("--mode", choices=("gundam", "base"), default="gundam", help="single-page image profile")
70
+ run.add_argument("--max-length", type=bounded_int(512, 32768), default=4096, help="total sequence limit")
71
+ run.add_argument("--dpi", type=bounded_int(72, 400), default=200, help="PDF rendering resolution")
72
+ run.add_argument("--start-page", type=bounded_int(1, 100000), default=1, help="first PDF page, 1-based")
73
+ run.add_argument("--max-pages", type=bounded_int(1, 100), default=20, help="maximum PDF pages per run")
74
+ run.add_argument(
75
+ "--max-page-pixels",
76
+ type=bounded_int(1_000_000, 200_000_000),
77
+ default=60_000_000,
78
+ help="maximum rendered pixels for one PDF page",
79
+ )
80
+ run.add_argument(
81
+ "--max-total-pixels",
82
+ type=bounded_int(1_000_000, 2_000_000_000),
83
+ default=400_000_000,
84
+ help="maximum aggregate rendered pixels for a PDF run",
85
+ )
86
+ run.add_argument(
87
+ "--max-page-rendered-mib",
88
+ type=bounded_int(1, 2048),
89
+ default=512,
90
+ help="maximum temporary size of one rendered PDF page",
91
+ )
92
+ run.add_argument(
93
+ "--max-rendered-mib",
94
+ type=bounded_int(1, 8192),
95
+ default=2048,
96
+ help="maximum aggregate size of rendered PDF pages",
97
+ )
98
+ run.add_argument("--prompt", default="<image>document parsing.", help="model prompt")
99
+ run.add_argument("-f", "--force", action="store_true", help="replace an existing output file")
100
+ return parser
101
+
102
+
103
+ def _emit(payload: dict, *, as_json: bool, quiet: bool = False) -> None:
104
+ if as_json:
105
+ print(json.dumps(payload, ensure_ascii=False, sort_keys=True))
106
+ return
107
+ if quiet:
108
+ primary = payload.get("output") or payload.get("model_dir") or payload.get("status")
109
+ if primary is not None:
110
+ print(primary)
111
+ return
112
+ for key, value in payload.items():
113
+ if isinstance(value, (dict, list, tuple)):
114
+ print(f"{key}={json.dumps(value, ensure_ascii=False, sort_keys=True)}")
115
+ else:
116
+ print(f"{key}={value}")
117
+
118
+
119
+ def _command_prepare(args: argparse.Namespace) -> int:
120
+ if not args.quiet:
121
+ action = "Would prepare" if args.dry_run else "Preparing"
122
+ print(f"{action} {MODEL_REVISION} at {args.model_dir}", file=sys.stderr, flush=True)
123
+ status = prepare_model(args.model_dir, args.cache_dir, dry_run=args.dry_run)
124
+ payload = {"schema_version": 1, **status.__dict__, "status": "dry-run" if args.dry_run else "ok"}
125
+ _emit(payload, as_json=args.json, quiet=args.quiet)
126
+ return 0
127
+
128
+
129
+ def _command_doctor(args: argparse.Namespace) -> int:
130
+ runtime = inspect_runtime(device=args.device, require_single=False, validate=False)
131
+ issues = runtime_issues(runtime, require_single=False)
132
+ model_payload: dict[str, object]
133
+ try:
134
+ model = verify_model(args.model_dir.expanduser().resolve(), full_weight_hash=False)
135
+ model_payload = {"prepared": True, "model_dir": model.model_dir, "revision": model.revision}
136
+ except ModelIntegrityError as exc:
137
+ if args.require_model:
138
+ raise
139
+ model_payload = {"prepared": False, "model_dir": str(args.model_dir), "detail": str(exc)}
140
+ payload = {
141
+ "schema_version": 1,
142
+ "status": "ok" if not issues else "error",
143
+ "runtime": runtime.to_dict(),
144
+ "runtime_issues": issues,
145
+ "model": model_payload,
146
+ }
147
+ _emit(payload, as_json=args.json, quiet=args.quiet)
148
+ return 0 if not issues else 3
149
+
150
+
151
+ def _command_run(args: argparse.Namespace) -> int:
152
+ import os
153
+
154
+ device = args.device or os.environ.get("UNLIMITED_OCR_DEVICE")
155
+ visibility_is_preconfigured = any(
156
+ name in os.environ for name in ("ROCR_VISIBLE_DEVICES", "HIP_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES")
157
+ )
158
+ if device is None and not visibility_is_preconfigured:
159
+ device = "0"
160
+ runtime = inspect_runtime(device=device, require_single=True)
161
+ input_path = args.input.expanduser()
162
+ output = args.output.expanduser() if args.output else default_output_path(input_path)
163
+ if not args.quiet:
164
+ print(f"Loading pinned model on RDNA 4 device {device}; inference is offline", file=sys.stderr, flush=True)
165
+ result = run_inference(
166
+ RunOptions(
167
+ input_path=input_path,
168
+ output_path=output,
169
+ model_dir=args.model_dir,
170
+ mode=args.mode,
171
+ max_length=args.max_length,
172
+ dpi=args.dpi,
173
+ start_page=args.start_page,
174
+ max_pages=args.max_pages,
175
+ max_page_pixels=args.max_page_pixels,
176
+ max_total_pixels=args.max_total_pixels,
177
+ max_page_rendered_bytes=args.max_page_rendered_mib * 1024**2,
178
+ max_rendered_bytes=args.max_rendered_mib * 1024**2,
179
+ prompt=args.prompt,
180
+ force=args.force,
181
+ quiet=args.quiet,
182
+ ),
183
+ runtime,
184
+ )
185
+ for warning in result.warnings:
186
+ print(f"warning: {warning}", file=sys.stderr)
187
+ payload = {"status": "ok", **result.to_dict()}
188
+ _emit(payload, as_json=args.json, quiet=args.quiet)
189
+ return 0
190
+
191
+
192
+ def run(argv: list[str] | None = None) -> int:
193
+ parser = build_parser()
194
+ args = parser.parse_args(argv)
195
+ if not args.command:
196
+ parser.print_help(sys.stderr)
197
+ return 2
198
+ try:
199
+ if args.command == "prepare":
200
+ return _command_prepare(args)
201
+ if args.command == "doctor":
202
+ return _command_doctor(args)
203
+ if args.command == "run":
204
+ return _command_run(args)
205
+ parser.error(f"unknown command: {args.command}")
206
+ except KeyboardInterrupt:
207
+ print("interrupted", file=sys.stderr)
208
+ return 130
209
+ except AppError as exc:
210
+ if args.json:
211
+ print(
212
+ json.dumps({"schema_version": 1, "status": "error", "error": str(exc), "exit_code": exc.exit_code}),
213
+ file=sys.stderr,
214
+ )
215
+ else:
216
+ print(f"error: {exc}", file=sys.stderr)
217
+ return exc.exit_code
218
+ except Exception as exc:
219
+ if args.debug:
220
+ traceback.print_exc()
221
+ else:
222
+ print(f"error: unexpected failure: {exc}; rerun with --debug", file=sys.stderr)
223
+ return 1
224
+ return 1
225
+
226
+
227
+ def main() -> None:
228
+ raise SystemExit(run())
src/unlimited_ocr_rdna4/constants.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ MODEL_REPO = "baidu/Unlimited-OCR"
4
+ MODEL_REVISION = "07dea832e22aefee32ad281d4b80551282e1c168"
5
+ MODEL_WEIGHT_FILE = "model-00001-of-000001.safetensors"
6
+ MODEL_WEIGHT_BYTES = 6_672_547_120
7
+ MODEL_WEIGHT_SHA256 = "2bc48a7a110061ea58fff65d3169367eebe3aee371ca6968dc2219c1b2855fc6"
8
+ UPSTREAM_MODEL_CODE_SHA256 = "268bdcbe12cf37bf5a2debb53faf542e56570958a5d9f3314aab3cab2cf6cb48"
9
+ PATCHED_MODEL_CODE_SHA256 = "67d722bc082cf2524948ac75ba1cf014fa0bffe2bfbd2e59a9b8511d3654366c"
10
+
11
+ MODEL_MANIFEST_FILE = "rdna4-manifest.json"
12
+ MODEL_UPSTREAM_BACKUP_FILE = "modeling_unlimitedocr.py.upstream"
13
+
14
+ # Every behavior-defining file loaded by Transformers is pinned. The weight is
15
+ # listed separately because callers may choose whether to return its full hash,
16
+ # but verification always hashes it before trust_remote_code=True is used.
17
+ MODEL_PAYLOAD_SHA256 = {
18
+ "LICENSE": "d985048c6d69429d685fdbe7557340aa0897c0fd8dc038299b148b9c75dc3383",
19
+ "README.md": "4f573db255db5262bfe2ea92e459bfc11635a456f210e3a96289bb9605077a55",
20
+ "config.json": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9",
21
+ "configuration_deepseek_v2.py": "b8470dd616ba8745fce6e27b093aef73a098863cc891b2477dcf9326a36000f7",
22
+ "conversation.py": "ec7b6ce89bcda643de1f43269ffa66a7b2e65dc3ed30e427958f776546b4ba03",
23
+ "deepencoder.py": "0ae2fb6d1e5ae8cf100fc32f854830acd08c821a0a1f23a94a76588c222ddcf2",
24
+ MODEL_WEIGHT_FILE: MODEL_WEIGHT_SHA256,
25
+ "model.safetensors.index.json": "354be1f2dcfb72ebb385e25465522ce5413a77c36f3b35fec088a3162a11af99",
26
+ "modeling_deepseekv2.py": "74e36e6bd0ba7bc565ef76464a99baa8e6bccb710ae9c1007b54ac30b855fa4c",
27
+ "modeling_unlimitedocr.py": PATCHED_MODEL_CODE_SHA256,
28
+ MODEL_UPSTREAM_BACKUP_FILE: UPSTREAM_MODEL_CODE_SHA256,
29
+ "processor_config.json": "92588cffb1d7032ec83d0a06c3a5171b41df5cbf432d68765441139a57899328",
30
+ "special_tokens_map.json": "ab4bd57ce17d62e39e0a39e739de1e407484f090f0b2c7e391312bca7a5b061a",
31
+ "tokenizer.json": "a02f8fd5228c90256bb4f6554c34a579d48f909e5beb232dc4afad870b55a8b4",
32
+ "tokenizer_config.json": "a0cbe8464049da1f891b7a12676de06af4cb54c130995d42f71adc1c30c6e9f3",
33
+ }
34
+
35
+ MODEL_DOWNLOAD_SHA256 = {
36
+ **MODEL_PAYLOAD_SHA256,
37
+ "modeling_unlimitedocr.py": UPSTREAM_MODEL_CODE_SHA256,
38
+ }
39
+ MODEL_DOWNLOAD_SHA256.pop(MODEL_UPSTREAM_BACKUP_FILE)
40
+
41
+ SUPPORTED_ARCHITECTURES = frozenset({"gfx1200", "gfx1201"})
42
+ HARDWARE_VERIFIED_ARCHITECTURES = frozenset({"gfx1201"})
43
+ VERIFIED_TORCH_VERSION = "2.9.1+rocm7.2.1.gitff65f5bc"
44
+ VERIFIED_HIP_VERSION = "7.2.53211-e1a6bc5663"
45
+ VERIFIED_ROCM_VERSION = "7.2.1"
46
+ VERIFIED_PACKAGE_VERSIONS = {
47
+ "numpy": "1.26.4",
48
+ "pillow": "12.1.1",
49
+ "pypdfium2": "5.12.1",
50
+ "torch": "2.9.1+rocm7.2.1.lw.gitff65f5bc",
51
+ "torchvision": "0.24.0+rocm7.2.1.gitb919bd0c",
52
+ "transformers": "4.57.1",
53
+ "triton": "3.5.1+rocm7.2.1.gita272dfa8",
54
+ }
55
+ VERSION = "0.1.0"
src/unlimited_ocr_rdna4/errors.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+
4
+ class AppError(Exception):
5
+ exit_code = 1
6
+
7
+
8
+ class RuntimeEnvironmentError(AppError):
9
+ exit_code = 3
10
+
11
+
12
+ class ModelIntegrityError(AppError):
13
+ exit_code = 4
14
+
15
+
16
+ class InferenceError(AppError):
17
+ exit_code = 5
src/unlimited_ocr_rdna4/infer.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import errno
5
+ import os
6
+ import stat
7
+ import sys
8
+ import tempfile
9
+ import time
10
+ from dataclasses import asdict, dataclass
11
+ from pathlib import Path
12
+ from typing import Any, TextIO
13
+
14
+ from .constants import MODEL_REVISION
15
+ from .errors import InferenceError
16
+ from .model_store import verify_model
17
+ from .paths import default_cache_dir
18
+ from .pdf import PDFPageRenderer
19
+ from .postprocess import clean_model_output, repetition_warning
20
+ from .runtime import RuntimeInfo
21
+
22
+ SUPPORTED_IMAGE_SUFFIXES = frozenset({".bmp", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp"})
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class RunOptions:
27
+ input_path: Path
28
+ output_path: Path
29
+ model_dir: Path
30
+ mode: str = "gundam"
31
+ max_length: int = 4096
32
+ dpi: int = 200
33
+ start_page: int = 1
34
+ max_pages: int = 20
35
+ max_page_pixels: int = 60_000_000
36
+ max_total_pixels: int = 400_000_000
37
+ max_page_rendered_bytes: int = 512 * 1024**2
38
+ max_rendered_bytes: int = 2 * 1024**3
39
+ prompt: str = "<image>document parsing."
40
+ force: bool = False
41
+ quiet: bool = False
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class RunResult:
46
+ schema_version: int
47
+ input: str
48
+ output: str
49
+ model_revision: str
50
+ pages_processed: int
51
+ pages_total: int
52
+ mode: str
53
+ model_load_seconds: float
54
+ inference_seconds: float
55
+ page_seconds: tuple[float, ...]
56
+ peak_allocated_gib: float
57
+ torch_version: str
58
+ hip_version: str | None
59
+ gpu: str
60
+ architecture: str | None
61
+ hardware_verified: bool
62
+ software_verified: bool
63
+ warnings: tuple[str, ...]
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ return asdict(self)
67
+
68
+
69
+ def default_output_path(input_path: Path) -> Path:
70
+ return input_path.with_name(f"{input_path.name}.ocr.md")
71
+
72
+
73
+ def _log_stream(quiet: bool) -> contextlib.AbstractContextManager[TextIO]:
74
+ if quiet:
75
+ return open(os.devnull, "w", encoding="utf-8")
76
+ return contextlib.nullcontext(sys.stderr)
77
+
78
+
79
+ def _load_model(model_dir: Path, log: TextIO):
80
+ import torch
81
+ from transformers import AutoModel, AutoTokenizer
82
+
83
+ with contextlib.redirect_stdout(log):
84
+ tokenizer = AutoTokenizer.from_pretrained(
85
+ model_dir,
86
+ trust_remote_code=True,
87
+ local_files_only=True,
88
+ )
89
+ model = (
90
+ AutoModel.from_pretrained(
91
+ model_dir,
92
+ trust_remote_code=True,
93
+ local_files_only=True,
94
+ use_safetensors=True,
95
+ dtype=torch.bfloat16,
96
+ )
97
+ .eval()
98
+ .cuda()
99
+ )
100
+ torch.cuda.synchronize()
101
+ return model, tokenizer
102
+
103
+
104
+ def _validated_output_path(input_path: Path, requested: Path, *, force: bool) -> Path:
105
+ unresolved = requested.expanduser().absolute()
106
+ unresolved.parent.mkdir(parents=True, exist_ok=True)
107
+ output_path = unresolved.parent.resolve(strict=True) / unresolved.name
108
+ if output_path == input_path:
109
+ raise InferenceError("input and output must be different files")
110
+ if output_path.is_symlink():
111
+ raise InferenceError(f"output must not be a symlink: {output_path}")
112
+ if output_path.exists():
113
+ if not stat.S_ISREG(output_path.lstat().st_mode):
114
+ raise InferenceError(f"output exists and is not a regular file: {output_path}")
115
+ if os.path.samefile(input_path, output_path):
116
+ raise InferenceError("input and output resolve to the same file")
117
+ if not force:
118
+ raise InferenceError(f"output already exists: {output_path}; pass --force to replace it")
119
+ return output_path
120
+
121
+
122
+ def _publish_text(output_path: Path, content: str, *, force: bool) -> None:
123
+ descriptor, temporary_name = tempfile.mkstemp(prefix=f".{output_path.name}.", suffix=".tmp", dir=output_path.parent)
124
+ temporary = Path(temporary_name)
125
+ try:
126
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
127
+ handle.write(content.rstrip() + "\n")
128
+ handle.flush()
129
+ os.fsync(handle.fileno())
130
+ if force:
131
+ os.replace(temporary, output_path)
132
+ else:
133
+ try:
134
+ os.link(temporary, output_path, follow_symlinks=False)
135
+ except FileExistsError as exc:
136
+ raise InferenceError(
137
+ f"output was created while inference was running: {output_path}; pass --force to replace it"
138
+ ) from exc
139
+ temporary.unlink()
140
+ directory_fd = os.open(output_path.parent, os.O_RDONLY | os.O_DIRECTORY)
141
+ try:
142
+ os.fsync(directory_fd)
143
+ finally:
144
+ os.close(directory_fd)
145
+ except OSError as exc:
146
+ if isinstance(exc, InferenceError):
147
+ raise
148
+ if exc.errno in {errno.EISDIR, errno.ENOTDIR}:
149
+ raise InferenceError(f"output path changed while inference was running: {output_path}") from exc
150
+ raise
151
+ finally:
152
+ temporary.unlink(missing_ok=True)
153
+
154
+
155
+ def run_inference(options: RunOptions, runtime: RuntimeInfo) -> RunResult:
156
+ input_path = options.input_path.expanduser().resolve(strict=True)
157
+ if not stat.S_ISREG(input_path.stat().st_mode):
158
+ raise InferenceError(f"input must be a regular file: {input_path}")
159
+ output_path = _validated_output_path(input_path, options.output_path, force=options.force)
160
+ model_dir = options.model_dir.expanduser().resolve()
161
+
162
+ suffix = input_path.suffix.lower()
163
+ if suffix != ".pdf" and suffix not in SUPPORTED_IMAGE_SUFFIXES:
164
+ supported = ", ".join(sorted(SUPPORTED_IMAGE_SUFFIXES | {".pdf"}))
165
+ raise InferenceError(f"unsupported input extension {suffix or '(none)'}; supported: {supported}")
166
+ if options.start_page < 1:
167
+ raise InferenceError("--start-page must be at least 1")
168
+
169
+ verify_model(model_dir)
170
+ cache_dir = default_cache_dir()
171
+ work_root = cache_dir / "tmp"
172
+ work_root.mkdir(parents=True, exist_ok=True)
173
+ os.environ["HF_HOME"] = str(cache_dir / "huggingface")
174
+ os.environ["HF_HUB_OFFLINE"] = "1"
175
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
176
+
177
+ import torch
178
+
179
+ with _log_stream(options.quiet) as log:
180
+ torch.cuda.reset_peak_memory_stats()
181
+ load_started = time.monotonic()
182
+ model, tokenizer = _load_model(model_dir, log)
183
+ model_load_seconds = time.monotonic() - load_started
184
+
185
+ if options.mode == "gundam":
186
+ base_size, image_size, crop_mode = 1024, 640, True
187
+ else:
188
+ base_size, image_size, crop_mode = 1024, 1024, False
189
+
190
+ page_seconds: list[float] = []
191
+ warnings: list[str] = []
192
+
193
+ def infer_image(image_path: Path) -> str:
194
+ started = time.monotonic()
195
+ try:
196
+ with contextlib.redirect_stdout(log), torch.inference_mode():
197
+ text = model.infer(
198
+ tokenizer,
199
+ prompt=options.prompt,
200
+ image_file=str(image_path),
201
+ output_path=str(work_dir),
202
+ base_size=base_size,
203
+ image_size=image_size,
204
+ crop_mode=crop_mode,
205
+ max_length=options.max_length,
206
+ no_repeat_ngram_size=35,
207
+ ngram_window=128,
208
+ temperature=0.0,
209
+ save_results=False,
210
+ eval_mode=True,
211
+ )
212
+ torch.cuda.synchronize()
213
+ except ValueError as exc:
214
+ if "max_length" in str(exc) and "Input length" in str(exc):
215
+ raise InferenceError(
216
+ f"{exc} Increase --max-length; 4096 is the tested single-page default."
217
+ ) from exc
218
+ raise InferenceError(f"generation failed: {exc}") from exc
219
+ except torch.OutOfMemoryError as exc:
220
+ raise InferenceError(
221
+ "GPU out of memory. Stop other GPU workloads, use --mode gundam, or reduce PDF page scope."
222
+ ) from exc
223
+ elapsed = time.monotonic() - started
224
+ page_seconds.append(elapsed)
225
+ cleaned = clean_model_output(text)
226
+ warning = repetition_warning(cleaned)
227
+ if warning:
228
+ warnings.append(f"{image_path.name}: {warning}")
229
+ return cleaned
230
+
231
+ with tempfile.TemporaryDirectory(prefix="run-", dir=work_root) as temporary:
232
+ work_dir = Path(temporary)
233
+ inference_started = time.monotonic()
234
+ if suffix == ".pdf":
235
+ with PDFPageRenderer(
236
+ input_path,
237
+ work_dir,
238
+ dpi=options.dpi,
239
+ start_page=options.start_page,
240
+ max_pages=options.max_pages,
241
+ max_page_pixels=options.max_page_pixels,
242
+ max_total_pixels=options.max_total_pixels,
243
+ max_page_rendered_bytes=options.max_page_rendered_bytes,
244
+ max_rendered_bytes=options.max_rendered_bytes,
245
+ ) as renderer:
246
+ sections = []
247
+ for page_number in renderer.page_numbers:
248
+ image_path = renderer.render(page_number)
249
+ try:
250
+ sections.append(f"<!-- page {page_number} -->\n\n{infer_image(image_path)}")
251
+ finally:
252
+ image_path.unlink(missing_ok=True)
253
+ pages_processed = len(sections)
254
+ pages_total = renderer.total_pages
255
+ content = "\n\n---\n\n".join(sections)
256
+ if options.start_page - 1 + pages_processed < pages_total:
257
+ warnings.append(
258
+ f"processed {pages_processed} page(s) starting at {options.start_page}; "
259
+ f"PDF contains {pages_total} page(s)"
260
+ )
261
+ else:
262
+ content = infer_image(input_path)
263
+ pages_processed = pages_total = 1
264
+ inference_seconds = time.monotonic() - inference_started
265
+
266
+ _publish_text(output_path, content, force=options.force)
267
+
268
+ device = runtime.devices[0]
269
+ return RunResult(
270
+ schema_version=1,
271
+ input=str(input_path),
272
+ output=str(output_path),
273
+ model_revision=MODEL_REVISION,
274
+ pages_processed=pages_processed,
275
+ pages_total=pages_total,
276
+ mode=options.mode,
277
+ model_load_seconds=round(model_load_seconds, 3),
278
+ inference_seconds=round(inference_seconds, 3),
279
+ page_seconds=tuple(round(value, 3) for value in page_seconds),
280
+ peak_allocated_gib=round(torch.cuda.max_memory_allocated() / (1024**3), 3),
281
+ torch_version=runtime.torch_version,
282
+ hip_version=runtime.hip_version,
283
+ gpu=device.name,
284
+ architecture=device.architecture,
285
+ hardware_verified=runtime.hardware_verified,
286
+ software_verified=runtime.software_verified,
287
+ warnings=tuple([*runtime.warnings, *warnings]),
288
+ )
src/unlimited_ocr_rdna4/model_store.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import shutil
7
+ import stat
8
+ import tempfile
9
+ from dataclasses import asdict, dataclass
10
+ from fcntl import LOCK_EX, flock
11
+ from pathlib import Path
12
+ from typing import BinaryIO
13
+
14
+ from .constants import (
15
+ MODEL_DOWNLOAD_SHA256,
16
+ MODEL_MANIFEST_FILE,
17
+ MODEL_PAYLOAD_SHA256,
18
+ MODEL_REPO,
19
+ MODEL_REVISION,
20
+ MODEL_UPSTREAM_BACKUP_FILE,
21
+ MODEL_WEIGHT_BYTES,
22
+ MODEL_WEIGHT_FILE,
23
+ MODEL_WEIGHT_SHA256,
24
+ PATCHED_MODEL_CODE_SHA256,
25
+ UPSTREAM_MODEL_CODE_SHA256,
26
+ VERIFIED_ROCM_VERSION,
27
+ VERIFIED_TORCH_VERSION,
28
+ )
29
+ from .errors import ModelIntegrityError
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ModelStatus:
34
+ model_dir: str
35
+ revision: str
36
+ prepared: bool
37
+ weight_bytes: int
38
+ weight_sha256: str | None
39
+ model_code_sha256: str
40
+ reused: bool = False
41
+
42
+
43
+ def _regular_file(path: Path) -> bool:
44
+ try:
45
+ return stat.S_ISREG(path.lstat().st_mode)
46
+ except FileNotFoundError:
47
+ return False
48
+
49
+
50
+ def sha256_file(path: Path) -> str:
51
+ digest = hashlib.sha256()
52
+ with path.open("rb") as handle:
53
+ for chunk in iter(lambda: handle.read(4 * 1024 * 1024), b""):
54
+ digest.update(chunk)
55
+ return digest.hexdigest()
56
+
57
+
58
+ def sha256_text(text: str) -> str:
59
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
60
+
61
+
62
+ def _replace_exact(text: str, old: str, new: str, expected: int) -> str:
63
+ found = text.count(old)
64
+ if found != expected:
65
+ raise ModelIntegrityError(f"model patch context mismatch: expected {expected} occurrence(s), found {found}")
66
+ return text.replace(old, new)
67
+
68
+
69
+ def patch_model_source_text(text: str) -> str:
70
+ current_hash = sha256_text(text)
71
+ if current_hash == PATCHED_MODEL_CODE_SHA256:
72
+ return text
73
+ if current_hash != UPSTREAM_MODEL_CODE_SHA256:
74
+ raise ModelIntegrityError(
75
+ "refusing to patch unknown model code; expected pinned Baidu revision "
76
+ f"{MODEL_REVISION}, got SHA-256 {current_hash}"
77
+ )
78
+
79
+ text = _replace_exact(
80
+ text,
81
+ "from .modeling_deepseekv2 import DeepseekV2Model, DeepseekV2ForCausalLM",
82
+ "import ast\n\nfrom .modeling_deepseekv2 import DeepseekV2Model, DeepseekV2ForCausalLM",
83
+ 1,
84
+ )
85
+ replacements = (
86
+ ("cor_list = eval(ref_text[2])", "cor_list = ast.literal_eval(ref_text[2])"),
87
+ ("lines = eval(outputs)['Line']['line']", "lines = ast.literal_eval(outputs)['Line']['line']"),
88
+ (
89
+ "line_type = eval(outputs)['Line']['line_type']",
90
+ "line_type = ast.literal_eval(outputs)['Line']['line_type']",
91
+ ),
92
+ (
93
+ "endpoints = eval(outputs)['Line']['line_endpoint']",
94
+ "endpoints = ast.literal_eval(outputs)['Line']['line_endpoint']",
95
+ ),
96
+ ("p0 = eval(line.split(' -- ')[0])", "p0 = ast.literal_eval(line.split(' -- ')[0])"),
97
+ ("p1 = eval(line.split(' -- ')[-1])", "p1 = ast.literal_eval(line.split(' -- ')[-1])"),
98
+ ("(x, y) = eval(endpoint.split(': ')[1])", "(x, y) = ast.literal_eval(endpoint.split(': ')[1])"),
99
+ )
100
+ for old, new in replacements:
101
+ text = _replace_exact(text, old, new, 1)
102
+
103
+ text = _replace_exact(
104
+ text,
105
+ "images_seq_mask[idx].unsqueeze(-1).cuda()",
106
+ "images_seq_mask[idx].unsqueeze(-1).to(inputs_embeds.device)",
107
+ 1,
108
+ )
109
+ text = _replace_exact(
110
+ text,
111
+ " input_ids=input_ids.unsqueeze(0).cuda(),\n",
112
+ " input_ids=input_ids.unsqueeze(0).cuda(),\n"
113
+ " attention_mask=torch.ones_like(input_ids.unsqueeze(0), device='cuda'),\n",
114
+ 3,
115
+ )
116
+ text = _replace_exact(
117
+ text,
118
+ " eos_token_id=tokenizer.eos_token_id,\n",
119
+ " eos_token_id=tokenizer.eos_token_id,\n pad_token_id=tokenizer.eos_token_id,\n",
120
+ 3,
121
+ )
122
+
123
+ patched_hash = sha256_text(text)
124
+ if PATCHED_MODEL_CODE_SHA256 != "__TO_BE_FILLED__" and patched_hash != PATCHED_MODEL_CODE_SHA256:
125
+ raise ModelIntegrityError(f"patched model code hash mismatch: {patched_hash}")
126
+ return text
127
+
128
+
129
+ def patch_model_file(model_dir: Path) -> str:
130
+ source = model_dir / "modeling_unlimitedocr.py"
131
+ text = source.read_text(encoding="utf-8")
132
+ patched = patch_model_source_text(text)
133
+ backup = model_dir / MODEL_UPSTREAM_BACKUP_FILE
134
+ if not backup.exists() and sha256_text(text) == UPSTREAM_MODEL_CODE_SHA256:
135
+ shutil.copy2(source, backup)
136
+ temporary = source.with_suffix(".py.new")
137
+ temporary.write_text(patched, encoding="utf-8")
138
+ os.replace(temporary, source)
139
+ return sha256_text(patched)
140
+
141
+
142
+ def _manifest_payload() -> dict[str, object]:
143
+ return {
144
+ "schema_version": 1,
145
+ "source": MODEL_REPO,
146
+ "revision": MODEL_REVISION,
147
+ "files": dict(sorted(MODEL_PAYLOAD_SHA256.items())),
148
+ "verified_rocm": VERIFIED_ROCM_VERSION,
149
+ "verified_torch": VERIFIED_TORCH_VERSION,
150
+ }
151
+
152
+
153
+ def _write_manifest(model_dir: Path) -> None:
154
+ manifest = model_dir / MODEL_MANIFEST_FILE
155
+ descriptor, temporary_name = tempfile.mkstemp(prefix=f".{MODEL_MANIFEST_FILE}.", dir=model_dir)
156
+ try:
157
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
158
+ json.dump(_manifest_payload(), handle, indent=2, sort_keys=True)
159
+ handle.write("\n")
160
+ handle.flush()
161
+ os.fsync(handle.fileno())
162
+ os.replace(temporary_name, manifest)
163
+ finally:
164
+ Path(temporary_name).unlink(missing_ok=True)
165
+
166
+
167
+ def _verify_payload(model_dir: Path, expected_hashes: dict[str, str]) -> dict[str, str]:
168
+ if model_dir.is_symlink() or not model_dir.is_dir():
169
+ raise ModelIntegrityError(f"model path must be a real directory, not a symlink: {model_dir}")
170
+
171
+ missing = [name for name in expected_hashes if not _regular_file(model_dir / name)]
172
+ if missing:
173
+ raise ModelIntegrityError(f"model is incomplete at {model_dir}; missing regular files: {', '.join(missing)}")
174
+
175
+ weight = model_dir / MODEL_WEIGHT_FILE
176
+ weight_bytes = weight.stat().st_size
177
+ if weight_bytes != MODEL_WEIGHT_BYTES:
178
+ raise ModelIntegrityError(f"weight size mismatch: expected {MODEL_WEIGHT_BYTES}, got {weight_bytes}")
179
+
180
+ actual_hashes: dict[str, str] = {}
181
+ for name, expected in expected_hashes.items():
182
+ actual = sha256_file(model_dir / name)
183
+ if actual != expected:
184
+ raise ModelIntegrityError(f"model file SHA-256 mismatch for {name}: expected {expected}, got {actual}")
185
+ actual_hashes[name] = actual
186
+ return actual_hashes
187
+
188
+
189
+ def _verify_exact_layout(model_dir: Path) -> None:
190
+ if model_dir.is_symlink() or not model_dir.is_dir():
191
+ raise ModelIntegrityError(f"model path must be a real directory, not a symlink: {model_dir}")
192
+ expected = set(MODEL_PAYLOAD_SHA256) | {MODEL_MANIFEST_FILE}
193
+ actual = {entry.name for entry in model_dir.iterdir()}
194
+ unexpected = sorted(actual - expected)
195
+ missing = sorted(expected - actual)
196
+ if unexpected or missing:
197
+ details = []
198
+ if missing:
199
+ details.append(f"missing: {', '.join(missing)}")
200
+ if unexpected:
201
+ details.append(f"unexpected: {', '.join(unexpected)}")
202
+ raise ModelIntegrityError(f"model layout mismatch at {model_dir}; {'; '.join(details)}")
203
+
204
+
205
+ def verify_model(model_dir: Path, *, full_weight_hash: bool = True) -> ModelStatus:
206
+ del full_weight_hash # Kept for API compatibility; security verification is always complete.
207
+ _verify_exact_layout(model_dir)
208
+ hashes = _verify_payload(model_dir, MODEL_PAYLOAD_SHA256)
209
+
210
+ manifest_path = model_dir / MODEL_MANIFEST_FILE
211
+ if not _regular_file(manifest_path):
212
+ raise ModelIntegrityError(f"model manifest is missing or not a regular file: {manifest_path}")
213
+ try:
214
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
215
+ except (OSError, json.JSONDecodeError) as exc:
216
+ raise ModelIntegrityError(f"invalid model manifest at {manifest_path}: {exc}") from exc
217
+ if manifest != _manifest_payload():
218
+ raise ModelIntegrityError(f"model manifest does not match the pinned runtime contract: {manifest_path}")
219
+
220
+ code_hash = hashes["modeling_unlimitedocr.py"]
221
+ weight_hash = hashes[MODEL_WEIGHT_FILE]
222
+ weight_bytes = (model_dir / MODEL_WEIGHT_FILE).stat().st_size
223
+
224
+ return ModelStatus(
225
+ model_dir=str(model_dir),
226
+ revision=MODEL_REVISION,
227
+ prepared=True,
228
+ weight_bytes=weight_bytes,
229
+ weight_sha256=weight_hash,
230
+ model_code_sha256=code_hash,
231
+ )
232
+
233
+
234
+ def _remove_snapshot_metadata(model_dir: Path) -> None:
235
+ metadata = model_dir / ".cache"
236
+ if metadata.is_symlink():
237
+ raise ModelIntegrityError(f"refusing symlinked snapshot metadata: {metadata}")
238
+ if metadata.exists():
239
+ shutil.rmtree(metadata)
240
+
241
+
242
+ def _upgrade_existing_model(model_dir: Path) -> ModelStatus:
243
+ allowed = set(MODEL_PAYLOAD_SHA256) | {MODEL_MANIFEST_FILE, ".cache"}
244
+ actual = {entry.name for entry in model_dir.iterdir()}
245
+ unexpected = sorted(actual - allowed)
246
+ if unexpected:
247
+ raise ModelIntegrityError(f"refusing unexpected entries in prepared model: {', '.join(unexpected)}")
248
+ _verify_payload(model_dir, MODEL_PAYLOAD_SHA256)
249
+ _remove_snapshot_metadata(model_dir)
250
+ _write_manifest(model_dir)
251
+ return verify_model(model_dir)
252
+
253
+
254
+ def _lock_handle(path: Path) -> BinaryIO:
255
+ descriptor = os.open(path, os.O_RDWR | os.O_CREAT | os.O_CLOEXEC | os.O_NOFOLLOW, 0o600)
256
+ handle = os.fdopen(descriptor, "a+b")
257
+ flock(handle.fileno(), LOCK_EX)
258
+ return handle
259
+
260
+
261
+ def prepare_model(model_dir: Path, cache_dir: Path, *, dry_run: bool = False) -> ModelStatus:
262
+ model_dir = model_dir.expanduser().absolute()
263
+ cache_dir = cache_dir.expanduser().resolve()
264
+ if dry_run:
265
+ return ModelStatus(
266
+ model_dir=str(model_dir),
267
+ revision=MODEL_REVISION,
268
+ prepared=False,
269
+ weight_bytes=MODEL_WEIGHT_BYTES,
270
+ weight_sha256=MODEL_WEIGHT_SHA256,
271
+ model_code_sha256=PATCHED_MODEL_CODE_SHA256,
272
+ )
273
+
274
+ if model_dir.is_symlink():
275
+ raise ModelIntegrityError(f"model destination must not be a symlink: {model_dir}")
276
+ model_dir.parent.mkdir(parents=True, exist_ok=True)
277
+ cache_dir.mkdir(parents=True, exist_ok=True)
278
+
279
+ try:
280
+ from huggingface_hub import snapshot_download
281
+ except ImportError as exc:
282
+ raise ModelIntegrityError("huggingface-hub is missing; install the package dependencies") from exc
283
+
284
+ lock_path = model_dir.parent / f".{model_dir.name}.prepare.lock"
285
+ with _lock_handle(lock_path):
286
+ if model_dir.exists():
287
+ if not model_dir.is_dir():
288
+ raise ModelIntegrityError(f"model destination exists and is not a directory: {model_dir}")
289
+ status = _upgrade_existing_model(model_dir)
290
+ return ModelStatus(**{**asdict(status), "reused": True})
291
+
292
+ partial = Path(tempfile.mkdtemp(prefix=f".{model_dir.name}.partial-", dir=model_dir.parent))
293
+ try:
294
+ snapshot_download(
295
+ repo_id=MODEL_REPO,
296
+ revision=MODEL_REVISION,
297
+ local_dir=partial,
298
+ cache_dir=cache_dir / "huggingface",
299
+ allow_patterns=sorted(MODEL_DOWNLOAD_SHA256),
300
+ )
301
+ _verify_payload(partial, MODEL_DOWNLOAD_SHA256)
302
+ patch_model_file(partial)
303
+ _remove_snapshot_metadata(partial)
304
+ _write_manifest(partial)
305
+ status = verify_model(partial)
306
+ partial.rename(model_dir)
307
+ except Exception:
308
+ shutil.rmtree(partial, ignore_errors=True)
309
+ raise
310
+ return ModelStatus(**{**asdict(status), "model_dir": str(model_dir)})
src/unlimited_ocr_rdna4/paths.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from .constants import MODEL_REVISION
7
+
8
+
9
+ def data_home() -> Path:
10
+ value = os.environ.get("XDG_DATA_HOME")
11
+ return Path(value).expanduser() if value else Path.home() / ".local" / "share"
12
+
13
+
14
+ def cache_home() -> Path:
15
+ value = os.environ.get("XDG_CACHE_HOME")
16
+ return Path(value).expanduser() if value else Path.home() / ".cache"
17
+
18
+
19
+ def default_model_dir() -> Path:
20
+ override = os.environ.get("UNLIMITED_OCR_MODEL_DIR")
21
+ if override:
22
+ return Path(override).expanduser()
23
+ return data_home() / "unlimited-ocr-rdna4" / "models" / MODEL_REVISION
24
+
25
+
26
+ def default_cache_dir() -> Path:
27
+ return cache_home() / "unlimited-ocr-rdna4"
src/unlimited_ocr_rdna4/pdf.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+ from typing import Any
7
+
8
+ from .errors import InferenceError
9
+
10
+
11
+ class PDFPageRenderer:
12
+ """Bounded, page-at-a-time PDFium renderer."""
13
+
14
+ def __init__(
15
+ self,
16
+ pdf_path: Path,
17
+ work_dir: Path,
18
+ *,
19
+ dpi: int,
20
+ start_page: int,
21
+ max_pages: int,
22
+ max_page_pixels: int,
23
+ max_total_pixels: int,
24
+ max_page_rendered_bytes: int,
25
+ max_rendered_bytes: int,
26
+ ) -> None:
27
+ self.pdf_path = pdf_path
28
+ self.work_dir = work_dir
29
+ self.dpi = dpi
30
+ self.start_page = start_page
31
+ self.max_pages = max_pages
32
+ self.max_page_pixels = max_page_pixels
33
+ self.max_total_pixels = max_total_pixels
34
+ self.max_page_rendered_bytes = max_page_rendered_bytes
35
+ self.max_rendered_bytes = max_rendered_bytes
36
+ self._pdfium: ModuleType | None = None
37
+ self._document: Any = None
38
+ self.total_pages = 0
39
+ self._rendered_pixels = 0
40
+ self._rendered_bytes = 0
41
+
42
+ def __enter__(self) -> PDFPageRenderer:
43
+ try:
44
+ import pypdfium2 as pdfium
45
+ except ImportError as exc:
46
+ raise InferenceError("pypdfium2 is missing; reinstall unlimited-ocr-rdna4") from exc
47
+
48
+ self._pdfium = pdfium
49
+ try:
50
+ self._document = pdfium.PdfDocument(str(self.pdf_path))
51
+ self.total_pages = len(self._document)
52
+ except Exception as exc:
53
+ raise InferenceError(f"could not open PDF {self.pdf_path}: {exc}") from exc
54
+
55
+ if self.total_pages < 1:
56
+ self.close()
57
+ raise InferenceError(f"PDF has no pages: {self.pdf_path}")
58
+ if self.start_page > self.total_pages:
59
+ self.close()
60
+ raise InferenceError(f"start page {self.start_page} exceeds PDF page count {self.total_pages}")
61
+ return self
62
+
63
+ def __exit__(self, _exc_type: object, _exc: object, _traceback: object) -> None:
64
+ self.close()
65
+
66
+ @property
67
+ def page_numbers(self) -> range:
68
+ last_page = min(self.total_pages, self.start_page - 1 + self.max_pages)
69
+ return range(self.start_page, last_page + 1)
70
+
71
+ def render(self, page_number: int) -> Path:
72
+ if self._document is None:
73
+ raise InferenceError("PDF renderer is not open")
74
+ page = None
75
+ bitmap = None
76
+ image = None
77
+ output = self.work_dir / f"page-{page_number:06d}.png"
78
+ try:
79
+ page = self._document[page_number - 1]
80
+ width_points, height_points = page.get_size()
81
+ scale = self.dpi / 72
82
+ width_pixels = math.ceil(width_points * scale)
83
+ height_pixels = math.ceil(height_points * scale)
84
+ pixels = width_pixels * height_pixels
85
+ if pixels <= 0 or pixels > self.max_page_pixels:
86
+ raise InferenceError(
87
+ f"PDF page {page_number} would render {pixels:,} pixels; limit is {self.max_page_pixels:,}"
88
+ )
89
+ if self._rendered_pixels + pixels > self.max_total_pixels:
90
+ raise InferenceError(
91
+ f"PDF rendered-pixel total would exceed {self.max_total_pixels:,} on page {page_number}"
92
+ )
93
+
94
+ bitmap = page.render(scale=scale)
95
+ image = bitmap.to_pil()
96
+ image.save(output, format="PNG", optimize=True)
97
+ rendered_bytes = output.stat().st_size
98
+ if rendered_bytes > self.max_page_rendered_bytes:
99
+ output.unlink(missing_ok=True)
100
+ raise InferenceError(
101
+ f"PDF page {page_number} rendered to {rendered_bytes:,} bytes; "
102
+ f"limit is {self.max_page_rendered_bytes:,}"
103
+ )
104
+ if self._rendered_bytes + rendered_bytes > self.max_rendered_bytes:
105
+ output.unlink(missing_ok=True)
106
+ raise InferenceError(
107
+ f"PDF rendered-byte total would exceed {self.max_rendered_bytes:,} on page {page_number}"
108
+ )
109
+ self._rendered_pixels += pixels
110
+ self._rendered_bytes += rendered_bytes
111
+ return output
112
+ except InferenceError:
113
+ raise
114
+ except Exception as exc:
115
+ output.unlink(missing_ok=True)
116
+ raise InferenceError(f"could not render PDF page {page_number}: {exc}") from exc
117
+ finally:
118
+ if image is not None:
119
+ image.close()
120
+ if bitmap is not None:
121
+ bitmap.close()
122
+ if page is not None:
123
+ page.close()
124
+
125
+ def close(self) -> None:
126
+ if self._document is not None:
127
+ self._document.close()
128
+ self._document = None
src/unlimited_ocr_rdna4/postprocess.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ EOS_STOP = "<|end▁of▁sentence|>"
6
+ _NONTEXT_LINE_RE = re.compile(r"(?m)^[ \t]*(?:\[\s*Non(?:[ -]?Text)\s*\]|NonText)[ \t]*(?:\n|$)")
7
+
8
+
9
+ def _replace_complete_tags(text: str, opening: str, closing: str, *, preserve_content: bool) -> str:
10
+ """Replace only complete exact tag pairs; malformed input is preserved."""
11
+ chunks: list[str] = []
12
+ cursor = 0
13
+ while True:
14
+ start = text.find(opening, cursor)
15
+ if start < 0:
16
+ chunks.append(text[cursor:])
17
+ break
18
+ end = text.find(closing, start + len(opening))
19
+ if end < 0:
20
+ chunks.append(text[cursor:])
21
+ break
22
+ chunks.append(text[cursor:start])
23
+ if preserve_content:
24
+ chunks.append(text[start + len(opening) : end])
25
+ cursor = end + len(closing)
26
+ return "".join(chunks)
27
+
28
+
29
+ def clean_model_output(text: str) -> str:
30
+ """Remove exact model sentinels while preserving recognized content verbatim."""
31
+ if text.endswith(EOS_STOP):
32
+ text = text[: -len(EOS_STOP)]
33
+ text = _replace_complete_tags(text, "<|ref|>", "<|/ref|>", preserve_content=True)
34
+ text = _replace_complete_tags(text, "<|det|>", "<|/det|>", preserve_content=False)
35
+ text = _NONTEXT_LINE_RE.sub("", text)
36
+ return text.strip()
37
+
38
+
39
+ def repetition_warning(text: str) -> str | None:
40
+ """Return a warning for obvious terminal repetition loops."""
41
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
42
+ for index in range(2, len(lines)):
43
+ if lines[index] == lines[index - 1] == lines[index - 2] and len(lines[index]) >= 8:
44
+ return f"repeated line detected three times: {lines[index][:80]!r}"
45
+
46
+ compact = re.sub(r"\s+", " ", text).strip()
47
+ for width in (64, 48, 32):
48
+ if len(compact) >= width * 3:
49
+ tail = compact[-width:]
50
+ if compact[-width * 3 :] == tail * 3:
51
+ return f"repeated {width}-character suffix detected"
52
+ return None
src/unlimited_ocr_rdna4/runtime.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from dataclasses import asdict, dataclass
6
+ from importlib.metadata import PackageNotFoundError, version
7
+ from typing import Any
8
+
9
+ from .constants import (
10
+ HARDWARE_VERIFIED_ARCHITECTURES,
11
+ SUPPORTED_ARCHITECTURES,
12
+ VERIFIED_HIP_VERSION,
13
+ VERIFIED_PACKAGE_VERSIONS,
14
+ )
15
+ from .errors import RuntimeEnvironmentError
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DeviceInfo:
20
+ index: int
21
+ name: str
22
+ architecture: str | None
23
+ uuid: str | None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class RuntimeInfo:
28
+ torch_version: str
29
+ hip_version: str | None
30
+ cuda_available: bool
31
+ bf16_supported: bool
32
+ visible_devices: int
33
+ devices: tuple[DeviceInfo, ...]
34
+ package_versions: dict[str, str | None]
35
+ accepted_architecture: bool
36
+ hardware_verified: bool
37
+ software_verified: bool
38
+ warnings: tuple[str, ...]
39
+
40
+ def to_dict(self) -> dict[str, Any]:
41
+ return asdict(self)
42
+
43
+
44
+ def select_device(device: str | None) -> None:
45
+ if device is None:
46
+ return
47
+ if "torch" in sys.modules:
48
+ raise RuntimeEnvironmentError("GPU selection must happen before importing PyTorch")
49
+ existing_rocr = os.environ.get("ROCR_VISIBLE_DEVICES")
50
+ if existing_rocr is not None and existing_rocr != device:
51
+ raise RuntimeEnvironmentError(
52
+ f"--device {device!r} conflicts with existing ROCR_VISIBLE_DEVICES={existing_rocr!r}; unset one explicitly"
53
+ )
54
+ conflicts = [name for name in ("HIP_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES") if name in os.environ]
55
+ if conflicts:
56
+ raise RuntimeEnvironmentError(
57
+ f"--device cannot be combined with existing {', '.join(conflicts)}; "
58
+ "unset the conflicting visibility variable"
59
+ )
60
+ os.environ["ROCR_VISIBLE_DEVICES"] = device
61
+
62
+
63
+ def _installed_versions() -> dict[str, str | None]:
64
+ installed: dict[str, str | None] = {}
65
+ for package in VERIFIED_PACKAGE_VERSIONS:
66
+ try:
67
+ installed[package] = version(package)
68
+ except PackageNotFoundError:
69
+ installed[package] = None
70
+ return installed
71
+
72
+
73
+ def runtime_issues(info: RuntimeInfo, *, require_single: bool) -> tuple[str, ...]:
74
+ issues: list[str] = []
75
+ if info.hip_version is None or not info.cuda_available:
76
+ issues.append("PyTorch does not see a ROCm GPU; a CUDA-only wheel is not sufficient")
77
+ if require_single and info.visible_devices != 1:
78
+ issues.append(f"expected exactly one visible GPU, found {info.visible_devices}; pass --device INDEX_OR_UUID")
79
+ if not info.accepted_architecture:
80
+ architectures = ", ".join(device.architecture or "unknown" for device in info.devices) or "none"
81
+ issues.append(f"expected RDNA 4 (gfx1200/gfx1201), found: {architectures}")
82
+ if info.cuda_available and not info.bf16_supported:
83
+ issues.append("the selected GPU/runtime does not report BF16 support")
84
+ return tuple(issues)
85
+
86
+
87
+ def inspect_runtime(*, device: str | None = None, require_single: bool = False, validate: bool = True) -> RuntimeInfo:
88
+ select_device(device)
89
+ try:
90
+ import torch
91
+ except ImportError as exc:
92
+ raise RuntimeEnvironmentError(
93
+ "ROCm PyTorch is not installed. Run scripts/bootstrap-rocm.sh from the repository checkout."
94
+ ) from exc
95
+
96
+ hip_version = getattr(torch.version, "hip", None)
97
+ cuda_available = bool(torch.cuda.is_available())
98
+ count = torch.cuda.device_count() if cuda_available else 0
99
+ devices: list[DeviceInfo] = []
100
+ for index in range(count):
101
+ properties = torch.cuda.get_device_properties(index)
102
+ devices.append(
103
+ DeviceInfo(
104
+ index=index,
105
+ name=properties.name,
106
+ architecture=getattr(properties, "gcnArchName", None),
107
+ uuid=str(getattr(properties, "uuid", "")) or None,
108
+ )
109
+ )
110
+
111
+ accepted_architecture = bool(devices) and all(device.architecture in SUPPORTED_ARCHITECTURES for device in devices)
112
+ hardware_verified = bool(devices) and all(
113
+ device.architecture in HARDWARE_VERIFIED_ARCHITECTURES for device in devices
114
+ )
115
+ package_versions = _installed_versions()
116
+ software_verified = hip_version == VERIFIED_HIP_VERSION and all(
117
+ package_versions[name] == expected for name, expected in VERIFIED_PACKAGE_VERSIONS.items()
118
+ )
119
+ warnings: list[str] = []
120
+ if accepted_architecture and not hardware_verified:
121
+ warnings.append(
122
+ "gfx1200 is accepted by the RDNA 4 guard but has not completed this project's GPU acceptance gate"
123
+ )
124
+ if not software_verified:
125
+ warnings.append("the installed software stack differs from the exact validated versions")
126
+ info = RuntimeInfo(
127
+ torch_version=torch.__version__,
128
+ hip_version=hip_version,
129
+ cuda_available=cuda_available,
130
+ bf16_supported=bool(torch.cuda.is_bf16_supported()) if cuda_available else False,
131
+ visible_devices=count,
132
+ devices=tuple(devices),
133
+ package_versions=package_versions,
134
+ accepted_architecture=accepted_architecture,
135
+ hardware_verified=hardware_verified,
136
+ software_verified=software_verified,
137
+ warnings=tuple(warnings),
138
+ )
139
+ issues = runtime_issues(info, require_single=require_single)
140
+ if validate and issues:
141
+ raise RuntimeEnvironmentError("; ".join(issues))
142
+ return info
tests/fixtures/rdna4-smoke.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cdd2b484d0ac90bd98b489dd97565a65eb17246f713359524cddd41d78cc10cb
3
+ size 152141
tests/fixtures/rdna4-smoke.png ADDED

Git LFS Details

  • SHA256: f5099e17be868abfb4213dbdab220deac82a2db93ba87ab22f03219178246972
  • Pointer size: 131 Bytes
  • Size of remote file: 115 kB
tests/test_bootstrap.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ ROOT = Path(__file__).resolve().parents[1]
4
+
5
+
6
+ def test_bootstrap_requires_exact_rocm_version() -> None:
7
+ script = (ROOT / "scripts/bootstrap-rocm.sh").read_text(encoding="utf-8")
8
+ assert '[[ $HOST_ROCM != "$ROCM_RELEASE" ]]' in script
9
+ assert "[[ $HOST_ROCM != 7.2.1* ]]" not in script
10
+
11
+
12
+ def test_bootstrap_uses_hash_lock_and_local_wheel() -> None:
13
+ script = (ROOT / "scripts/bootstrap-rocm.sh").read_text(encoding="utf-8")
14
+ assert "--require-hashes --only-binary=:all:" in script
15
+ assert "pip wheel --no-build-isolation --no-deps" in script
16
+ assert "pip install --force-reinstall --no-deps" in script
17
+ assert "pip install --upgrade" not in script
tests/test_cli.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from unlimited_ocr_rdna4.cli import run
5
+ from unlimited_ocr_rdna4.infer import default_output_path
6
+
7
+
8
+ def test_prepare_dry_run_json_after_subcommand(capsys, tmp_path) -> None:
9
+ code = run(
10
+ [
11
+ "prepare",
12
+ "--model-dir",
13
+ str(tmp_path / "model"),
14
+ "--cache-dir",
15
+ str(tmp_path / "cache"),
16
+ "--dry-run",
17
+ "--json",
18
+ ]
19
+ )
20
+ assert code == 0
21
+ payload = json.loads(capsys.readouterr().out)
22
+ assert payload["schema_version"] == 1
23
+ assert payload["status"] == "dry-run"
24
+ assert payload["prepared"] is False
25
+
26
+
27
+ def test_no_command_returns_usage_error(capsys) -> None:
28
+ assert run([]) == 2
29
+ assert "usage:" in capsys.readouterr().err
30
+
31
+
32
+ def test_default_output_path() -> None:
33
+ assert default_output_path(Path("invoice.pdf")) == Path("invoice.pdf.ocr.md")
tests/test_infer_output.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import pytest
4
+
5
+ from unlimited_ocr_rdna4.errors import InferenceError
6
+ from unlimited_ocr_rdna4.infer import _publish_text, _validated_output_path
7
+
8
+
9
+ def test_output_must_not_alias_input(tmp_path) -> None:
10
+ source = tmp_path / "page.png"
11
+ source.write_bytes(b"source")
12
+ alias = tmp_path / "alias.md"
13
+ os.link(source, alias)
14
+ with pytest.raises(InferenceError, match="same file"):
15
+ _validated_output_path(source.resolve(), alias, force=True)
16
+
17
+
18
+ def test_output_symlink_is_rejected_even_with_force(tmp_path) -> None:
19
+ source = tmp_path / "page.png"
20
+ source.write_bytes(b"source")
21
+ output = tmp_path / "output.md"
22
+ output.symlink_to(source)
23
+ with pytest.raises(InferenceError, match="symlink"):
24
+ _validated_output_path(source.resolve(), output, force=True)
25
+ assert source.read_bytes() == b"source"
26
+
27
+
28
+ def test_non_force_publish_never_overwrites(tmp_path) -> None:
29
+ output = tmp_path / "output.md"
30
+ output.write_text("existing\n", encoding="utf-8")
31
+ with pytest.raises(InferenceError, match="created while inference"):
32
+ _publish_text(output, "new", force=False)
33
+ assert output.read_text(encoding="utf-8") == "existing\n"
34
+
35
+
36
+ def test_force_publish_replaces_regular_file(tmp_path) -> None:
37
+ output = tmp_path / "output.md"
38
+ output.write_text("old\n", encoding="utf-8")
39
+ _publish_text(output, "new", force=True)
40
+ assert output.read_text(encoding="utf-8") == "new\n"
tests/test_model_store.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+
3
+ import pytest
4
+
5
+ import unlimited_ocr_rdna4.model_store as model_store
6
+ from unlimited_ocr_rdna4.errors import ModelIntegrityError
7
+
8
+
9
+ def _synthetic_upstream() -> str:
10
+ lines = [
11
+ "from .modeling_deepseekv2 import DeepseekV2Model, DeepseekV2ForCausalLM",
12
+ "cor_list = eval(ref_text[2])",
13
+ "lines = eval(outputs)['Line']['line']",
14
+ "line_type = eval(outputs)['Line']['line_type']",
15
+ "endpoints = eval(outputs)['Line']['line_endpoint']",
16
+ "p0 = eval(line.split(' -- ')[0])",
17
+ "p1 = eval(line.split(' -- ')[-1])",
18
+ "(x, y) = eval(endpoint.split(': ')[1])",
19
+ "images_seq_mask[idx].unsqueeze(-1).cuda()",
20
+ ]
21
+ for _ in range(3):
22
+ lines.extend(
23
+ [
24
+ " input_ids=input_ids.unsqueeze(0).cuda(),",
25
+ " eos_token_id=tokenizer.eos_token_id,",
26
+ ]
27
+ )
28
+ return "\n".join(lines) + "\n"
29
+
30
+
31
+ def test_patch_is_exact_and_idempotent(monkeypatch) -> None:
32
+ source = _synthetic_upstream()
33
+ monkeypatch.setattr(model_store, "UPSTREAM_MODEL_CODE_SHA256", model_store.sha256_text(source))
34
+ monkeypatch.setattr(model_store, "PATCHED_MODEL_CODE_SHA256", "__TO_BE_FILLED__")
35
+ patched = model_store.patch_model_source_text(source)
36
+ patched_hash = model_store.sha256_text(patched)
37
+ assert patched.count("ast.literal_eval(") == 7
38
+ assert patched.count("attention_mask=torch.ones_like") == 3
39
+ assert patched.count("pad_token_id=tokenizer.eos_token_id") == 3
40
+ assert ".to(inputs_embeds.device)" in patched
41
+
42
+ monkeypatch.setattr(model_store, "PATCHED_MODEL_CODE_SHA256", patched_hash)
43
+ assert model_store.patch_model_source_text(patched) == patched
44
+
45
+
46
+ def test_patch_rejects_unknown_source() -> None:
47
+ with pytest.raises(ModelIntegrityError, match="refusing to patch unknown model code"):
48
+ model_store.patch_model_source_text("unknown")
49
+
50
+
51
+ def test_verify_model_reports_missing_directory(tmp_path) -> None:
52
+ with pytest.raises(ModelIntegrityError, match="real directory"):
53
+ model_store.verify_model(tmp_path / "missing")
54
+
55
+
56
+ def test_verify_model_with_small_fixture(tmp_path, monkeypatch) -> None:
57
+ code = "patched model code\n"
58
+ weight = b"weights"
59
+ (tmp_path / "modeling_unlimitedocr.py").write_text(code, encoding="utf-8")
60
+ (tmp_path / "weight.bin").write_bytes(weight)
61
+ monkeypatch.setattr(model_store, "MODEL_WEIGHT_FILE", "weight.bin")
62
+ monkeypatch.setattr(model_store, "MODEL_WEIGHT_BYTES", len(weight))
63
+ monkeypatch.setattr(model_store, "MODEL_WEIGHT_SHA256", hashlib.sha256(weight).hexdigest())
64
+ monkeypatch.setattr(model_store, "PATCHED_MODEL_CODE_SHA256", hashlib.sha256(code.encode()).hexdigest())
65
+ monkeypatch.setattr(
66
+ model_store,
67
+ "MODEL_PAYLOAD_SHA256",
68
+ {
69
+ "modeling_unlimitedocr.py": hashlib.sha256(code.encode()).hexdigest(),
70
+ "weight.bin": hashlib.sha256(weight).hexdigest(),
71
+ },
72
+ )
73
+ model_store._write_manifest(tmp_path)
74
+ status = model_store.verify_model(tmp_path)
75
+ assert status.prepared
76
+ assert status.weight_sha256 == hashlib.sha256(weight).hexdigest()
77
+
78
+
79
+ def test_verify_model_rejects_unexpected_file(tmp_path, monkeypatch) -> None:
80
+ code = b"code"
81
+ weight = b"weight"
82
+ (tmp_path / "code.py").write_bytes(code)
83
+ (tmp_path / "weight.bin").write_bytes(weight)
84
+ monkeypatch.setattr(model_store, "MODEL_WEIGHT_FILE", "weight.bin")
85
+ monkeypatch.setattr(model_store, "MODEL_WEIGHT_BYTES", len(weight))
86
+ monkeypatch.setattr(
87
+ model_store,
88
+ "MODEL_PAYLOAD_SHA256",
89
+ {"code.py": hashlib.sha256(code).hexdigest(), "weight.bin": hashlib.sha256(weight).hexdigest()},
90
+ )
91
+ model_store._write_manifest(tmp_path)
92
+ (tmp_path / "configuration_surprise.py").write_text("raise SystemExit", encoding="utf-8")
93
+ with pytest.raises(ModelIntegrityError, match="unexpected"):
94
+ model_store.verify_model(tmp_path)
95
+
96
+
97
+ def test_verify_model_rejects_symlinked_payload(tmp_path, monkeypatch) -> None:
98
+ target = tmp_path / "target"
99
+ target.write_bytes(b"weight")
100
+ (tmp_path / "weight.bin").symlink_to(target)
101
+ monkeypatch.setattr(model_store, "MODEL_WEIGHT_FILE", "weight.bin")
102
+ monkeypatch.setattr(model_store, "MODEL_WEIGHT_BYTES", len(b"weight"))
103
+ monkeypatch.setattr(
104
+ model_store,
105
+ "MODEL_PAYLOAD_SHA256",
106
+ {"weight.bin": hashlib.sha256(b"weight").hexdigest(), "target": hashlib.sha256(b"weight").hexdigest()},
107
+ )
108
+ model_store._write_manifest(tmp_path)
109
+ with pytest.raises(ModelIntegrityError, match="missing regular files"):
110
+ model_store.verify_model(tmp_path)
tests/test_paths.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from unlimited_ocr_rdna4.constants import MODEL_REVISION
4
+ from unlimited_ocr_rdna4.paths import default_cache_dir, default_model_dir
5
+
6
+
7
+ def test_xdg_defaults(monkeypatch) -> None:
8
+ monkeypatch.delenv("UNLIMITED_OCR_MODEL_DIR", raising=False)
9
+ monkeypatch.setenv("XDG_DATA_HOME", "/data-home")
10
+ monkeypatch.setenv("XDG_CACHE_HOME", "/cache-home")
11
+ assert default_model_dir() == Path("/data-home/unlimited-ocr-rdna4/models") / MODEL_REVISION
12
+ assert default_cache_dir() == Path("/cache-home/unlimited-ocr-rdna4")
13
+
14
+
15
+ def test_model_dir_override(monkeypatch) -> None:
16
+ monkeypatch.setenv("UNLIMITED_OCR_MODEL_DIR", "/models/pinned")
17
+ assert default_model_dir() == Path("/models/pinned")
tests/test_pdf.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from PIL import Image
3
+
4
+ from unlimited_ocr_rdna4.errors import InferenceError
5
+ from unlimited_ocr_rdna4.pdf import PDFPageRenderer
6
+
7
+
8
+ def _make_pdf(path) -> None:
9
+ image = Image.new("RGB", (120, 80), "white")
10
+ try:
11
+ image.save(path, format="PDF", resolution=72)
12
+ finally:
13
+ image.close()
14
+
15
+
16
+ def test_pdf_renders_one_page_at_a_time(tmp_path) -> None:
17
+ pdf = tmp_path / "fixture.pdf"
18
+ _make_pdf(pdf)
19
+ with PDFPageRenderer(
20
+ pdf,
21
+ tmp_path,
22
+ dpi=72,
23
+ start_page=1,
24
+ max_pages=1,
25
+ max_page_pixels=20_000,
26
+ max_total_pixels=20_000,
27
+ max_page_rendered_bytes=1_000_000,
28
+ max_rendered_bytes=1_000_000,
29
+ ) as renderer:
30
+ assert tuple(renderer.page_numbers) == (1,)
31
+ rendered = renderer.render(1)
32
+ assert rendered.is_file()
33
+
34
+
35
+ def test_pdf_rejects_page_over_pixel_limit(tmp_path) -> None:
36
+ pdf = tmp_path / "fixture.pdf"
37
+ _make_pdf(pdf)
38
+ with (
39
+ PDFPageRenderer(
40
+ pdf,
41
+ tmp_path,
42
+ dpi=72,
43
+ start_page=1,
44
+ max_pages=1,
45
+ max_page_pixels=1_000,
46
+ max_total_pixels=20_000,
47
+ max_page_rendered_bytes=1_000_000,
48
+ max_rendered_bytes=1_000_000,
49
+ ) as renderer,
50
+ pytest.raises(InferenceError, match="would render"),
51
+ ):
52
+ renderer.render(1)
tests/test_postprocess.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from unlimited_ocr_rdna4.postprocess import EOS_STOP, clean_model_output, repetition_warning
2
+
3
+
4
+ def test_clean_model_output_preserves_unicode_and_content() -> None:
5
+ raw = (
6
+ "<|det|>header [1, 2, 3, 4]<|/det|>Überblick\n"
7
+ "<|det|>table [1, 2, 3, 4]<|/det|><table><tr><td>€48.50</td></tr></table>\n"
8
+ "[Non-Text]\n"
9
+ "\\coloneqq"
10
+ f"{EOS_STOP}"
11
+ )
12
+ assert clean_model_output(raw) == "Überblick\n<table><tr><td>€48.50</td></tr></table>\n\\coloneqq"
13
+
14
+
15
+ def test_clean_model_output_removes_combined_reference_tag() -> None:
16
+ raw = "<|ref|>title<|/ref|><|det|>[[1, 2, 3, 4]]<|/det|>Actual title"
17
+ assert clean_model_output(raw) == "titleActual title"
18
+
19
+
20
+ def test_clean_model_output_preserves_ordinary_words_and_tex() -> None:
21
+ raw = "NonTextile material\nA \\eqqcolon B\nembedded NonText label"
22
+ assert clean_model_output(raw) == raw
23
+
24
+
25
+ def test_clean_model_output_preserves_malformed_unclosed_tags() -> None:
26
+ raw = "before <|det|>coordinate and recognized content"
27
+ assert clean_model_output(raw) == raw
28
+
29
+
30
+ def test_repetition_warning_detects_three_lines() -> None:
31
+ text = "start\nrepeating output\nrepeating output\nrepeating output"
32
+ assert "repeated line" in (repetition_warning(text) or "")
33
+
34
+
35
+ def test_repetition_warning_accepts_normal_text() -> None:
36
+ assert repetition_warning("first\nsecond\nthird") is None
tests/test_runtime.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ from unlimited_ocr_rdna4.errors import RuntimeEnvironmentError
7
+ from unlimited_ocr_rdna4.runtime import DeviceInfo, RuntimeInfo, select_device
8
+
9
+
10
+ def test_runtime_info_serializes() -> None:
11
+ info = RuntimeInfo(
12
+ torch_version="2.9.1+rocm7.2.1",
13
+ hip_version="7.2",
14
+ cuda_available=True,
15
+ bf16_supported=True,
16
+ visible_devices=1,
17
+ devices=(DeviceInfo(0, "AMD Radeon RX 9070 XT", "gfx1201", "uuid"),),
18
+ package_versions={"torch": "different"},
19
+ accepted_architecture=True,
20
+ hardware_verified=True,
21
+ software_verified=False,
22
+ warnings=("different stack",),
23
+ )
24
+ payload = info.to_dict()
25
+ assert payload["devices"][0]["architecture"] == "gfx1201"
26
+ assert payload["hardware_verified"] is True
27
+ assert payload["software_verified"] is False
28
+
29
+
30
+ def test_device_selection_rejects_conflicting_visibility(monkeypatch) -> None:
31
+ monkeypatch.delitem(sys.modules, "torch", raising=False)
32
+ monkeypatch.setenv("HIP_VISIBLE_DEVICES", "2")
33
+ with pytest.raises(RuntimeEnvironmentError, match="HIP_VISIBLE_DEVICES"):
34
+ select_device("GPU-example")
35
+
36
+
37
+ def test_device_selection_preserves_matching_rocr_value(monkeypatch) -> None:
38
+ monkeypatch.delitem(sys.modules, "torch", raising=False)
39
+ monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising=False)
40
+ monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False)
41
+ monkeypatch.setenv("ROCR_VISIBLE_DEVICES", "GPU-example")
42
+ select_device("GPU-example")
43
+ assert os.environ["ROCR_VISIBLE_DEVICES"] == "GPU-example"