optivision-rag 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,349 +1,105 @@
1
- ---
2
- title: OptiVision RAG
3
- emoji: 🗜️
4
- colorFrom: indigo
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 6.24.0
8
- python_version: "3.11"
9
- app_file: app.py
10
- pinned: false
11
- license: mit
12
- short_description: Visual-token pruning and binary quantization for document retrieval
13
- ---
14
-
15
- # OptiVision RAG
16
-
17
- **Extreme token compression for vision-language document retrieval.**
18
-
19
- > The YAML block above is Hugging Face Space metadata. It is ignored by GitHub
20
- > and by every tool in this repo; see [docs/DEPLOY.md](docs/DEPLOY.md) for how
21
- > the Space is set up.
22
-
23
- ---
24
-
25
- ## The problem
26
-
27
- ColPali-style models search scanned documents *as images* — no OCR, so they work on
28
- old copies, seals, stamps and handwriting that text pipelines fail on. They do it by
29
- splitting each page into patches and keeping **one vector per patch**.
30
-
31
- That is also the problem. A single page becomes ~800–1000 vectors of 128 float32
32
- dimensions:
33
-
34
- ```
35
- 1 page = 1024 vectors x 128 dims x 4 bytes = 512 KB
36
- 1 million pages = 512 GB
37
- ```
38
-
39
- Half a terabyte of RAM-resident vector index for a corpus that a records office
40
- would consider small. The accuracy is excellent; the storage is what stops it
41
- being deployable.
42
-
43
- ## What this project does
44
-
45
- OptiVision RAG shrinks the **index**, not the model. The published checkpoint runs
46
- unmodified, the query path is untouched, and the retrieval is still late-interaction
47
- MaxSim — only the number and the size of the stored vectors change.
48
-
49
- ```
50
- page image
51
- │
52
- ├─ VLM encoder ─────────► ~1000 patch vectors (model unchanged)
53
- │
54
- ├─ 1. spatial pruning ──► drop patches on blank paper
55
- │
56
- ├─ 2. redundancy prune ─► collapse near-duplicate patches
57
- │
58
- ├─ 3. binary quantize ──► 128 floats (512 B) → 128 bits (16 B)
59
- │
60
- └─ index ───────────────► Qdrant MaxSim, or an exact numpy index
61
- ```
62
-
63
- **1. Spatial pruning.** Most of a document page is paper. Per patch we measure ink
64
- density (how much is darker than the estimated paper background) and edge energy
65
- (local contrast — glyph strokes, rules, stamp borders), and drop the cells that carry
66
- neither. The estimate runs on the pixels in microseconds, before the model, and needs
67
- no attention maps or second forward pass. The paper background is estimated per page
68
- (95th percentile) rather than assumed white, so a yellowed photocopy is not read as a
69
- fully inked page.
70
-
71
- **2. Redundancy pruning.** Spatial pruning removes patches with *no* content;
72
- this removes patches with *duplicate* content — the interior of a filled table cell,
73
- the middle of a thick rule. They look salient to a pixel detector but add nothing to a
74
- MaxSim score, because MaxSim already takes a max over near-identical vectors. A greedy
75
- single pass collapses each cluster to its renormalised centroid.
76
-
77
- **3. Binary quantization.** These vectors are L2-normalised and roughly zero-centred
78
- per dimension, so the sign bits keep the orthant and discard only the within-orthant
79
- position. Crucially, the distortion is nearly the same for every document vector, so
80
- it moves scores much more than it moves *ranking*. Queries stay in float32 and are
81
- scored against ±1 document codes (asymmetric scoring): a corpus has millions of
82
- vectors, a query has twenty, so query precision is the cheapest thing to keep.
83
-
84
- The three stages multiply — under E1, pruning ~3.5× × quantization 32× ≈ **100×+** off
85
- the index. On real ViDoRe pages pruning buys less, and the product lands in the 53-60× range; both
86
- figures are in *Results* below.
87
-
88
- ## Results
89
-
90
- We ran the ablation twice, and the two runs disagree. That disagreement is the
91
- result — read both tables before drawing a conclusion from either.
92
-
93
- ### E1 — ColSmol-256M, generated pages
94
-
95
- 60 pages / 72 queries on CPU. Full table and analysis in
96
- [docs/RESULTS.md](docs/RESULTS.md); reproduce with `make bench`.
97
-
98
- | variant | tok/pg | KB/pg | compression | nDCG@5 | retained | tau |
99
- |---|---|---|---|---|---|---|
100
- | baseline-float32 | 875.0 | 448.00 | 1.0x | 0.7823 | 100.0% | 1.000 |
101
- | spatial-only | 356.1 | 182.32 | 2.5x | 0.7602 | 97.2% | 0.935 |
102
- | spatial+redundancy | 246.8 | 126.34 | 3.5x | 0.7519 | 96.1% | 0.866 |
103
- | int8-only | 875.0 | 112.00 | 4.0x | 0.7877 | 100.7% | 0.972 |
104
- | **prune+int8** | **246.8** | **31.59** | **14.2x** | **0.7511** | **96.0%** | 0.864 |
105
- | binary-only | 875.0 | 14.00 | 32.0x | 0.6875 | 87.9% | 0.585 |
106
- | **optivision** | **246.8** | **3.95** | **113.5x** | **0.6782** | **86.7%** | 0.606 |
107
- | optivision-aggressive | 186.3 | 2.98 | 150.3x | 0.6680 | 85.4% | 0.602 |
108
-
109
- **Under this encoder, the two halves of the proposal do not contribute equally.**
110
- Pruning is nearly free — 3.5x fewer vectors for 3.9% of nDCG@5.
111
- Binary quantization is where the quality goes — 12.1% lost *without dropping a single
112
- token*, while int8 gives 4x for 0.5%. Pruning harder barely matters once binary is in
113
- play (keeping the top 10% of patches scores the same as keeping the top 50%).
114
-
115
- So under E1 the pipeline has two defensible operating points:
116
-
117
- - **smallest index** — prune + binary: **113.5x** at 86.7% of baseline nDCG@5
118
- - **best quality per byte** — prune + int8: **14.2x** at 96.0%, with a Kendall tau of
119
- 0.864 against 0.866 for pruning alone, so int8 adds no measurable ranking distortion on
120
- top of the pruning
121
-
122
- 448 KB/page becomes 3.95 KB/page: a million-page archive drops from ~448 GB to ~4 GB.
123
- At the quality-first setting it is ~32 GB.
124
-
125
- ### E2 — ColPali-v1.3, real ViDoRe pages
126
-
127
- **The reference model disagrees with every headline above.** The same ablation on
128
- ColPali-v1.3 over four ViDoRe splits (1,780 pages, 1,325 queries) reaches 53-60x at
129
- 94.6-103.4% of baseline nDCG@5 — less compression, far less loss. Two of the three findings above do not transfer:
130
- pruning buys only 1.7-1.9x on real pages rather than 3.5x, and binary quantization
131
- costs 0-3.7% rather than 12.1%. Full table and analysis in
132
- [docs/RESULTS.md](docs/RESULTS.md#the-same-table-on-colpali-3b-and-real-vidore-pages);
133
- raw reports in [reports/](reports/); reproduce with `bash scripts/run_bench_gpu.sh` on
134
- any free T4.
135
-
136
- The takeaway is not "E2 supersedes E1". It is that **the per-stage attribution is a
137
- property of the encoder and the corpus, not of the compression layer** — the same code
138
- paid its quality in different places under the two. A third run separates those two
139
- variables; see below. A compression ratio reported without naming the encoder and the
140
- corpus it was measured on is not enough information to act on, which is what the paper
141
- argues.
142
-
143
- ### E3 — ColPali-3B, generated pages
144
-
145
- The missing cell: the reference encoder over E1's own corpus, regenerated at seed 7 so
146
- the pages are the same ones. It splits the reversal in two.
147
-
148
- | | corpus fixed, encoder swapped | encoder fixed, corpus swapped |
149
- |---|---|---|
150
- | one-bit codec costs | E1 → E3: **12.1 → 1.6 points** | E3 → E2: 1.6 → 3.7 points |
151
- | pruning buys | E1 → E3: 3.55x → 4.20x | E3 → E2: **4.20x → 1.85x** |
152
-
153
- **The corpus sets what pruning buys.** Run `python scripts/compare_regimes.py` to
154
- print it from the benchmark files. E3 is also the best operating point in the project
155
- (98.7% retention at 134.5x) and the least representative one.
156
-
157
- The codec half is not an encoder property. Per query, a sign code adds the same ~3% of
158
- score noise under both encoders, and it flips only queries whose float margin over
159
- the best competitor is smaller than that noise — under 1% of 945 real ViDoRe queries
160
- above twice the noise, against a third of those below half.
161
- E1's precise queries are decided by three digits at a 0.05% margin, which is why the
162
- small encoder looks fragile; ColPali cannot read those digits at 448 px and never wins
163
- the queries it is credited with not losing (precise R@1 0.250, floor 0.200). Enlarge the
164
- code and its one-bit cost appears. See [RESULTS.md](docs/RESULTS.md#e3---colpali-3b-on-the-generated-corpus)
165
- and [REVIEW-2026-08-21.md](docs/REVIEW-2026-08-21.md); `scripts/tau_audit.py` explains why
166
- E1's tau of 0.585 and E2's 0.527 were never the same measurement.
167
-
168
- On dense pages, selecting tokens by embedding coverage beats pixel saliency below a 50%
169
- budget — and 256 random probe directions do as well as the designed selector
170
- ([RESULTS.md](docs/RESULTS.md#token-selection-on-dense-pages---the-stage-ii-controls)).
171
-
172
- ### Memory at query time
173
-
174
- This part holds under both experiments. A compressed index is worth nothing if
175
- searching it expands the vectors back to float32, which costs 32x the index and
176
- is what the obvious implementation does. Scoring here runs in blocks bounded by
177
- a memory budget, so peak RAM is set by that budget rather than by the size of
178
- the corpus — 239 MB at the 256 MB default, whether the index holds sixty pages
179
- or a million. See [docs/IMPROVEMENTS.md](docs/IMPROVEMENTS.md).
180
-
181
- ### Reading either table
182
-
183
- | column | what it means |
184
- |---|---|
185
- | `Tok/pg` | vectors actually stored per page |
186
- | `KB/pg` | index bytes per page |
187
- | `Compr.` | vs. the uncompressed float32 index |
188
- | `nDCG@5`, `R@1` | absolute retrieval quality against ground truth |
189
- | `Retain` | nDCG@5 as a fraction of the uncompressed baseline's |
190
- | `Tau` | Kendall tau against the **baseline's own ranking** |
191
-
192
- `Tau` is the honest measure of compression damage: it does not ask whether the model
193
- was right, only whether compressing changed its mind.
194
-
195
- ## Demo app
196
-
197
- A single-page Gradio demo (`app.py`) shows the compression happening on one
198
- uploaded document — built for the Stage-I presentation, not as a production RAG
199
- service.
200
-
201
- Double-click `run_demo.bat`, or:
202
-
203
- ```bash
204
- pip install -e ".[vlm,app]" && pip install gradio
205
- python app.py # http://127.0.0.1:7860
206
- ```
207
-
208
- Upload a PDF or scan, press **Compress Document**, and it reports the real token
209
- counts, the real byte sizes, and renders the pipeline's actual keep-mask over the
210
- page. Every figure is read back off the arrays the pipeline produced.
211
-
212
- It runs **locally**: Hugging Face now requires a PRO subscription to host any live
213
- Gradio Space, free CPU hardware included. `app.py` and `scripts/deploy_space.py`
214
- are Space-ready for whenever that changes — see [docs/DEPLOY.md](docs/DEPLOY.md).
215
-
216
- The demo refuses to run on the `synthetic` backend: showing a regression harness's
217
- output as a demonstration result would misrepresent the project.
218
-
219
- ## Install
220
-
221
- ```bash
222
- python -m venv .venv
223
- .venv/Scripts/activate # Windows; source .venv/bin/activate elsewhere
224
- pip install -e ".[vlm,bench,app,dev]"
225
- ```
226
-
227
- `torch` is CPU-only by default. For a GPU box install the CUDA build first
228
- (`pip install torch --index-url https://download.pytorch.org/whl/cu121`).
229
-
230
- ## Quick start
231
-
232
- ```bash
233
- # 1. build a corpus of scanned-looking documents with ground-truth queries
234
- optivision make-corpus data/corpus --docs 30 --pages 2
235
-
236
- # 2. index it (colsmol.yaml = real 256M model, runs on a CPU laptop)
237
- optivision index data/corpus/pdfs -c configs/colsmol.yaml
238
-
239
- # 3. search
240
- optivision search "renewal of vehicle insurance policy" -c configs/colsmol.yaml
241
-
242
- # 4. see exactly which patches were dropped
243
- optivision explain data/corpus/pdfs -c configs/colsmol.yaml --out reports/figures
244
-
245
- # 5. run the full ablation table
246
- # --cache stores the encode pass, so re-running to add or change a row
247
- # takes seconds instead of re-encoding the whole corpus
248
- optivision bench data/corpus/pdfs data/corpus/queries.json \
249
- -c configs/colsmol.yaml --out reports/colsmol --sweep \
250
- --cache data/cache/colsmol.npz
251
-
252
- # 6. demo UI
253
- streamlit run app/streamlit_app.py -- --config configs/colsmol.yaml
254
- ```
255
-
256
- Point step 2 at your own folder of PDFs or scans to index real documents.
257
-
258
- For the reference numbers — ColPali-v1.3 over ViDoRe splits — you need a GPU.
259
- Two paths, same config and same code:
260
- `notebooks/vidore_colpali_bench.ipynb` (Colab or Kaggle, interactive) and
261
- `scripts/run_bench_gpu.sh` (any Ubuntu + CUDA box, unattended, all four splits).
262
- See [docs/GPU_RUN.md](docs/GPU_RUN.md) — a rented GPU costs about $0.25 for the
263
- whole benchmark, which is less trouble than the free tiers' quotas.
264
-
265
- ## Configurations
266
-
267
- | config | encoder | needs | use for |
268
- |---|---|---|---|
269
- | `configs/synthetic.yaml` | hashed stand-in | nothing | tests, CI, first smoke run |
270
- | `configs/colsmol.yaml` | ColSmol-256M | ~0.5 GB download, CPU ok | **default** — real results on a laptop |
271
- | `configs/colqwen2.yaml`* | ColQwen2-2B | GPU | stronger quality |
272
- | `configs/colpali.yaml` | ColPali-v1.3 | GPU (~6 GB) | reference model from the paper |
273
- | `configs/qdrant.yaml` | ColSmol + Qdrant | optional server | deployment-shaped storage |
274
-
275
- \* generate with `optivision init-config configs/colqwen2.yaml --backend colqwen2`.
276
-
277
- The **synthetic** encoder deserves a warning: it makes the whole pipeline runnable
278
- with no downloads and its retrieval genuinely works, so it is a real correctness
279
- harness — but its hashed word vectors are near-orthogonal, which makes MaxSim behave
280
- like exact matching. Quality metrics there saturate at 1.0 and **must not be reported
281
- as results**. Use it to test plumbing; use ColSmol or ColPali for numbers.
282
-
283
- ## Layout
284
-
285
- ```
286
- src/optivision/
287
- types.py PageEncoding → PrunedPage → CompressedPage
288
- config.py every experimental knob, YAML-loadable
289
- encoders/
290
- colvlm.py ColPali / ColQwen2 / ColSmol via colpali-engine
291
- synthetic.py offline stand-in (tests only)
292
- pruning/
293
- saliency.py ink density + edge energy per patch
294
- spatial.py keep-mask construction, dilation, budgets
295
- redundancy.py greedy near-duplicate collapsing
296
- compression/
297
- binary.py bit packing, asymmetric/symmetric MaxSim
298
- index/
299
- numpy_index.py exact brute-force MaxSim (the reference)
300
- qdrant_index.py Qdrant multivector + binary quantization
301
- pipeline.py OptiVisionRAG.build() / .search()
302
- bench.py encode-once, replay-every-variant ablation harness
303
- corpus.py synthetic corpus generator + ViDoRe loader
304
- metrics.py nDCG / recall / MRR / Kendall tau / storage
305
- viz.py keep-mask and saliency figures
306
- app/streamlit_app.py demo UI
307
- docs/ architecture, results, viva notes
308
- notebooks/ Colab / Kaggle runner for the ColPali benchmark
309
- ```
310
-
311
- ## Testing
312
-
313
- ```bash
314
- pytest # 75 tests, no model download needed
315
- ruff check src tests app
316
- ```
317
-
318
- The suite covers saliency behaviour on blank/grey/inked pages, keep-mask budgets,
319
- redundancy clustering invariants, bit-packing round-trips, MaxSim segment maths on
320
- variable-length pages, index save/load, Qdrant multivector round-trips, and a full
321
- index→search→evaluate loop.
322
-
323
- ## Improvements
324
-
325
- [docs/IMPROVEMENTS.md](docs/IMPROVEMENTS.md) records five changes made after the
326
- pipeline was working — bounded query-time memory, an int8 quantizer that uses
327
- the range it pays for, a single-allocation decode path, a Qdrant stats fix, and
328
- one quadratic loop — each with the measurement behind it, plus what was
329
- reviewed and deliberately left alone.
330
-
331
- ## Honest limitations
332
-
333
- - **Encoder speed on CPU.** ColSmol-256M takes ~30 s/page on this laptop (no GPU).
334
- Pruning and quantization together take ~15 ms/page — the compression is free
335
- relative to encoding, but building a large index needs a GPU.
336
- - **The bundled corpus is generated**, not scanned. It has the right whitespace
337
- profile and gives exact ground truth, but real scans have noise, skew and bleed-through.
338
- `optivision fetch-vidore` pulls the real ViDoRe benchmark for that reason.
339
- - **Redundancy merging changes the vectors**, not just their count. It is nearly free
340
- under MaxSim but it is not lossless — the ablation reports both stages separately so
341
- the cost is visible.
342
- - **Qdrant local mode** is convenient but not a performance claim; latency numbers
343
- come from the exact numpy index so they are not confounded by ANN recall.
344
-
345
- ## References
346
-
347
- - Faysse et al., *ColPali: Efficient Document Retrieval with Vision Language Models* (2024)
348
- - Khattab & Zaharia, *ColBERT* (2020) — late interaction / MaxSim
349
- - Qdrant multivector + binary quantization documentation
1
+ # OptiVision RAG
2
+
3
+ **Extreme token compression for vision-language document retrieval.**
4
+
5
+ ColPali-style models search scanned documents as images, with no OCR. They store
6
+ one 128-dimensional float vector per image patch, so a single page costs about
7
+ 512 KB of index and a million pages cost about half a terabyte.
8
+
9
+ OptiVision RAG shrinks the **index**, not the model. The published checkpoint runs
10
+ unmodified and retrieval is still late-interaction MaxSim. Only the number and the
11
+ size of the stored vectors change:
12
+
13
+ ```
14
+ page image
15
+ ├─ VLM encoder ─────────► ~1000 patch vectors (model unchanged)
16
+ ├─ 1. spatial pruning ──► drop patches on blank paper
17
+ ├─ 2. redundancy prune ─► collapse near-duplicate patches
18
+ ├─ 3. binary quantize ──► 128 floats (512 B) -> 128 bits (16 B)
19
+ └─ index ───────────────► Qdrant MaxSim, or an exact numpy index
20
+ ```
21
+
22
+ | setting | encoder / corpus | compression | nDCG@5 retained |
23
+ |---|---|---|---|
24
+ | prune + binary | ColSmol-256M, generated pages | 113.5x | 86.7% |
25
+ | prune + int8 | ColSmol-256M, generated pages | 14.2x | 96.0% |
26
+ | prune + binary | ColPali-v1.3, ViDoRe (4 splits) | 53-60x | 94.6-103.4% |
27
+
28
+ Full results, the ablations and the analysis are in the
29
+ [repository](https://github.com/Daemon-VI/optivision-rag).
30
+
31
+ > **This npm package is a launcher.** OptiVision RAG is written in Python. The first
32
+ > time you run `npx optivision-rag`, it creates a private virtual environment in your
33
+ > user cache folder and installs the matching release from
34
+ > [PyPI](https://pypi.org/project/optivision-rag/). Later runs start straight away.
35
+ > You need **Python 3.10 or newer** on the machine.
36
+ >
37
+ > | variable | effect |
38
+ > |---|---|
39
+ > | `OPTIVISION_EXTRAS` | pip extras to install, default `corpus`. Use `vlm,corpus` for the real encoders. |
40
+ > | `OPTIVISION_PYTHON` | the Python interpreter to use |
41
+ > | `OPTIVISION_REINSTALL=1` | rebuild the environment |
42
+ > | `OPTIVISION_HOME` | where the environments are kept |
43
+ >
44
+ > Every `optivision` command below also works as `npx optivision-rag <command>`.
45
+
46
+ ## Install
47
+
48
+ Python 3.10 or newer.
49
+
50
+ ```bash
51
+ pip install optivision-rag # core pipeline + CLI
52
+ pip install "optivision-rag[corpus]" # + synthetic test-corpus generator
53
+ pip install "optivision-rag[vlm]" # + real encoders (torch, colpali-engine)
54
+ pip install "optivision-rag[vlm,corpus,bench]"
55
+ ```
56
+
57
+ Or run the CLI through npm without touching pip yourself (it still needs Python
58
+ 3.10+ on the machine):
59
+
60
+ ```bash
61
+ npx optivision-rag --help
62
+ ```
63
+
64
+ ## Quick start (offline, no model download)
65
+
66
+ The `synthetic` preset uses a deterministic stand-in encoder, so the whole pipeline
67
+ runs in seconds on any laptop. Use it to try the tool, not for real results.
68
+
69
+ ```bash
70
+ optivision make-corpus data/corpus --docs 10 --pages 2
71
+ optivision index data/corpus/pdfs -c synthetic
72
+ optivision search "renewal of vehicle insurance policy" -c synthetic
73
+ optivision stats -c synthetic
74
+ ```
75
+
76
+ ## With a real model
77
+
78
+ Install the `vlm` extra, then switch the preset. `colsmol` is a 256M-parameter
79
+ model that runs on a CPU laptop; `colpali` wants a GPU.
80
+
81
+ ```bash
82
+ optivision index path/to/your/pdfs -c colsmol
83
+ optivision search "fire safety audit memorandum" -c colsmol
84
+ optivision explain path/to/your/pdfs -c colsmol --out figures
85
+ ```
86
+
87
+ `-c` takes a bundled preset name (`synthetic`, `colsmol`, `colpali`, `qdrant`) or a
88
+ path to your own YAML file. `optivision init-config my.yaml` writes one pre-filled
89
+ with every default.
90
+
91
+ ## Python API
92
+
93
+ ```python
94
+ from optivision import Config, OptiVisionRAG
95
+
96
+ rag = OptiVisionRAG(Config.load("colsmol"))
97
+ report = rag.build("path/to/pdfs")
98
+ print(report.compression_ratio)
99
+ print(rag.search("office memorandum on fire safety audit").hits[0].ref.page_id)
100
+ ```
101
+
102
+ ## Authors
103
+
104
+ T. Rithik Krishna, Amgovath Navanitha and Badavath Akhila, Mahatma Gandhi Institute
105
+ of Technology, Hyderabad. MIT licensed.
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * npx launcher for the `optivision` Python CLI.
4
+ *
5
+ * On first run it creates a private virtual environment in the user cache
6
+ * directory and installs the PyPI release whose version matches this npm
7
+ * package. Every later run starts the CLI directly. All arguments are passed
8
+ * through unchanged.
9
+ *
10
+ * Environment variables:
11
+ * OPTIVISION_PYTHON Python interpreter to use (default: first of py -3 / python3 / python)
12
+ * OPTIVISION_EXTRAS pip extras to install, comma separated (default: "corpus"; "" for none)
13
+ * e.g. OPTIVISION_EXTRAS=vlm,corpus for the real encoders
14
+ * OPTIVISION_PIP_SPEC full pip requirement, overrides the version and extras
15
+ * (a local wheel path works, for testing)
16
+ * OPTIVISION_HOME where the environments are kept
17
+ * OPTIVISION_REINSTALL set to 1 to rebuild the environment
18
+ */
19
+ "use strict";
20
+
21
+ const { spawnSync, spawn } = require("child_process");
22
+ const fs = require("fs");
23
+ const os = require("os");
24
+ const path = require("path");
25
+
26
+ const PKG = require("../package.json");
27
+ const MIN_PY = [3, 10];
28
+ const IS_WIN = process.platform === "win32";
29
+
30
+ function log(msg) {
31
+ process.stderr.write(`optivision-rag: ${msg}\n`);
32
+ }
33
+
34
+ function fail(msg) {
35
+ log(msg);
36
+ process.exit(1);
37
+ }
38
+
39
+ function cacheRoot() {
40
+ if (process.env.OPTIVISION_HOME) return process.env.OPTIVISION_HOME;
41
+ if (IS_WIN) {
42
+ const local = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
43
+ return path.join(local, "optivision-rag");
44
+ }
45
+ if (process.platform === "darwin") {
46
+ return path.join(os.homedir(), "Library", "Caches", "optivision-rag");
47
+ }
48
+ return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), "optivision-rag");
49
+ }
50
+
51
+ function splitCmd(cmd) {
52
+ // "py -3" -> ["py", ["-3"]]; an existing path (even with spaces) stays whole.
53
+ if (fs.existsSync(cmd)) return [cmd, []];
54
+ const parts = cmd.trim().split(/\s+/);
55
+ return [parts[0], parts.slice(1)];
56
+ }
57
+
58
+ function probePython(cmd) {
59
+ const [exe, pre] = splitCmd(cmd);
60
+ const r = spawnSync(exe, [...pre, "-c", "import sys; print('%d.%d' % sys.version_info[:2])"], {
61
+ encoding: "utf8",
62
+ windowsHide: true,
63
+ });
64
+ if (r.status !== 0 || !r.stdout) return null;
65
+ const [maj, min] = r.stdout.trim().split(".").map(Number);
66
+ const ok = maj > MIN_PY[0] || (maj === MIN_PY[0] && min >= MIN_PY[1]);
67
+ return { exe, pre, version: `${maj}.${min}`, tooOld: !ok };
68
+ }
69
+
70
+ function findPython() {
71
+ const candidates = process.env.OPTIVISION_PYTHON
72
+ ? [process.env.OPTIVISION_PYTHON]
73
+ : IS_WIN
74
+ ? ["py -3", "python", "python3"]
75
+ : ["python3", "python"];
76
+ const tooOld = [];
77
+ for (const c of candidates) {
78
+ const p = probePython(c);
79
+ if (p && !p.tooOld) return p;
80
+ if (p) tooOld.push(`${c} (${p.version})`);
81
+ }
82
+ const need = MIN_PY.join(".");
83
+ if (tooOld.length) {
84
+ fail(`Python ${need}+ is required; found only ${tooOld.join(", ")}. Set OPTIVISION_PYTHON to a newer interpreter.`);
85
+ }
86
+ fail(
87
+ `Python ${need}+ is required but none was found on PATH. ` +
88
+ "Install it from https://www.python.org/downloads/ or set OPTIVISION_PYTHON.",
89
+ );
90
+ }
91
+
92
+ function requirement() {
93
+ if (process.env.OPTIVISION_PIP_SPEC) return process.env.OPTIVISION_PIP_SPEC;
94
+ const raw = process.env.OPTIVISION_EXTRAS === undefined ? "corpus" : process.env.OPTIVISION_EXTRAS;
95
+ const extras = raw
96
+ .split(",")
97
+ .map((s) => s.trim())
98
+ .filter(Boolean);
99
+ return `optivision-rag${extras.length ? `[${extras.join(",")}]` : ""}==${PKG.version}`;
100
+ }
101
+
102
+ function envDirFor(spec) {
103
+ const tag = spec.replace(/[^A-Za-z0-9.=_-]+/g, "_").slice(-60);
104
+ return path.join(cacheRoot(), `env-${PKG.version}-${tag}`);
105
+ }
106
+
107
+ function venvBin(dir, name) {
108
+ return IS_WIN ? path.join(dir, "Scripts", `${name}.exe`) : path.join(dir, "bin", name);
109
+ }
110
+
111
+ function runQuiet(exe, args) {
112
+ // Installer output goes to stderr so the CLI's stdout stays clean for pipes.
113
+ const r = spawnSync(exe, args, { stdio: ["ignore", process.stderr, process.stderr], windowsHide: true });
114
+ if (r.error) fail(`could not run ${exe}: ${r.error.message}`);
115
+ return r.status === 0;
116
+ }
117
+
118
+ function ensureInstalled() {
119
+ const spec = requirement();
120
+ const dir = envDirFor(spec);
121
+ const marker = path.join(dir, ".optivision-installed");
122
+ const cli = venvBin(dir, "optivision");
123
+
124
+ const ready = fs.existsSync(marker) && fs.existsSync(cli);
125
+ if (ready && process.env.OPTIVISION_REINSTALL !== "1") return cli;
126
+
127
+ const py = findPython();
128
+ fs.rmSync(dir, { recursive: true, force: true });
129
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
130
+ log(`first run: installing ${spec} with Python ${py.version} into ${dir}`);
131
+ if (!runQuiet(py.exe, [...py.pre, "-m", "venv", dir])) {
132
+ fs.rmSync(dir, { recursive: true, force: true });
133
+ fail("could not create a virtual environment (on Debian/Ubuntu: apt install python3-venv)");
134
+ }
135
+ const venvPy = venvBin(dir, "python");
136
+ const pipArgs = ["-m", "pip", "install", "--disable-pip-version-check", "--quiet", spec];
137
+ if (!runQuiet(venvPy, pipArgs)) {
138
+ fs.rmSync(dir, { recursive: true, force: true });
139
+ fail(`pip install ${spec} failed (see the output above)`);
140
+ }
141
+ fs.writeFileSync(marker, `${spec}\n`);
142
+ log("installed; later runs start immediately");
143
+ return cli;
144
+ }
145
+
146
+ function main() {
147
+ const cli = ensureInstalled();
148
+ const child = spawn(cli, process.argv.slice(2), { stdio: "inherit" });
149
+ for (const sig of ["SIGINT", "SIGTERM"]) {
150
+ process.on(sig, () => child.kill(sig));
151
+ }
152
+ child.on("error", (e) => fail(`could not start ${cli}: ${e.message}`));
153
+ child.on("exit", (code, signal) => {
154
+ if (signal) process.kill(process.pid, signal);
155
+ else process.exit(code === null ? 1 : code);
156
+ });
157
+ }
158
+
159
+ main();
package/package.json CHANGED
@@ -1,39 +1,21 @@
1
1
  {
2
2
  "name": "optivision-rag",
3
- "version": "0.1.0",
4
- "description": "Extreme token compression for vision-language document retrieval (ColPali-style). npx launcher for the Python CLI — installs and runs the matching optivision-rag PyPI release.",
5
- "type": "module",
3
+ "version": "0.1.1",
4
+ "description": "OptiVision RAG: extreme token compression for vision-language document retrieval (npx launcher for the Python CLI)",
5
+ "keywords": ["rag", "colpali", "vision-language", "retrieval", "quantization", "token-pruning", "qdrant", "cli"],
6
+ "homepage": "https://github.com/Daemon-VI/optivision-rag#readme",
7
+ "bugs": { "url": "https://github.com/Daemon-VI/optivision-rag/issues" },
8
+ "repository": { "type": "git", "url": "git+https://github.com/Daemon-VI/optivision-rag.git", "directory": "npm" },
9
+ "license": "MIT",
10
+ "author": "T. Rithik Krishna",
11
+ "contributors": ["Amgovath Navanitha", "Badavath Akhila"],
6
12
  "bin": {
7
- "optivision-rag": "js/bin/optivision-rag.js"
8
- },
9
- "files": [
10
- "js/bin/",
11
- "js/lib/"
12
- ],
13
- "engines": {
14
- "node": ">=18"
13
+ "optivision-rag": "bin/optivision.js",
14
+ "optivision": "bin/optivision.js"
15
15
  },
16
+ "files": ["bin/", "README.md", "LICENSE"],
17
+ "engines": { "node": ">=18" },
16
18
  "scripts": {
17
- "test": "node --test"
18
- },
19
- "keywords": [
20
- "rag",
21
- "retrieval",
22
- "vision-language",
23
- "colpali",
24
- "document-retrieval",
25
- "token-compression",
26
- "quantization",
27
- "cli"
28
- ],
29
- "author": "T. Rithik Krishna (https://github.com/Daemon-VI)",
30
- "license": "MIT",
31
- "repository": {
32
- "type": "git",
33
- "url": "git+https://github.com/Daemon-VI/optivision-rag.git"
34
- },
35
- "homepage": "https://github.com/Daemon-VI/optivision-rag#readme",
36
- "bugs": {
37
- "url": "https://github.com/Daemon-VI/optivision-rag/issues"
19
+ "test": "node test/smoke.js"
38
20
  }
39
21
  }
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- // `optivision-rag` on npm: a launcher for the Python CLI (src/optivision/cli.py). See
3
- // js/lib/runner.js for why this can't be a JS port.
4
- import { main } from "../lib/runner.js";
5
-
6
- process.exitCode = main(process.argv.slice(2));
package/js/lib/runner.js DELETED
@@ -1,105 +0,0 @@
1
- // The npm package ships no VLM code of its own — colpali-engine/transformers/torch have no
2
- // JS equivalent. This is a thin launcher: it finds (or installs) the matching version of the
3
- // real `optivision-rag` PyPI package and execs its CLI, forwarding argv and the exit code.
4
-
5
- import { spawnSync } from "node:child_process";
6
- import { createRequire } from "node:module";
7
-
8
- // Read from package.json rather than duplicating the version string here — the publish
9
- // workflow already checks package.json against the release tag.
10
- const PACKAGE_VERSION = createRequire(import.meta.url)("../../package.json").version;
11
-
12
- function run(cmd, args) {
13
- return spawnSync(cmd, args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
14
- }
15
-
16
- function findPython() {
17
- // `python`/`python3` resolve through PATH, so they respect whatever venv or version
18
- // manager the caller has active. `py -3` is the Windows Launcher's own registry lookup —
19
- // it ignores PATH entirely, so it only goes last, as a fallback for the vanilla
20
- // python.org install that never put python.exe on PATH.
21
- const candidates =
22
- process.platform === "win32" ? ["python", "python3", "py"] : ["python3", "python"];
23
- for (const cmd of candidates) {
24
- const args = cmd === "py" ? ["-3", "--version"] : ["--version"];
25
- const result = run(cmd, args);
26
- if (result.status === 0) {
27
- const match = /Python (\d+)\.(\d+)/.exec(result.stdout || result.stderr || "");
28
- if (match && (Number(match[1]) > 3 || Number(match[2]) >= 10)) {
29
- return cmd === "py" ? { cmd: "py", prefix: ["-3"] } : { cmd, prefix: [] };
30
- }
31
- }
32
- }
33
- return null;
34
- }
35
-
36
- function hasPipx() {
37
- const result = run(process.platform === "win32" ? "where" : "which", ["pipx"]);
38
- return result.status === 0;
39
- }
40
-
41
- // A matching install already on PATH is the fast path — no network call, and it is what
42
- // makes anyone who already `pip install`ed this exact release (or ships it in a Docker image)
43
- // see the launcher exec instantly.
44
- function installedVersion(python) {
45
- const check = spawnSync(
46
- python.cmd,
47
- [...python.prefix, "-c", "import optivision; print(optivision.__version__)"],
48
- { encoding: "utf8" },
49
- );
50
- return check.status === 0 ? check.stdout.trim() : null;
51
- }
52
-
53
- export function main(argv) {
54
- const python = findPython();
55
-
56
- if (python && installedVersion(python) === PACKAGE_VERSION) {
57
- const result = spawnSync(python.cmd, [...python.prefix, "-m", "optivision.cli", ...argv], {
58
- stdio: "inherit",
59
- });
60
- return result.status ?? 1;
61
- }
62
-
63
- if (hasPipx()) {
64
- const result = spawnSync("pipx", ["run", `optivision-rag==${PACKAGE_VERSION}`, ...argv], {
65
- stdio: "inherit",
66
- });
67
- if (result.error) {
68
- console.error(`optivision-rag: failed to run pipx (${result.error.message})`);
69
- return 1;
70
- }
71
- return result.status ?? 1;
72
- }
73
-
74
- if (!python) {
75
- console.error(
76
- "optivision-rag: no Python 3.10+ interpreter found on PATH.\n" +
77
- "Install Python (https://python.org/downloads) or pipx (https://pipx.pypa.io), then retry.",
78
- );
79
- return 1;
80
- }
81
-
82
- console.error(`optivision-rag: installing optivision-rag==${PACKAGE_VERSION} with pip (one-time)...`);
83
- const install = spawnSync(
84
- python.cmd,
85
- [
86
- ...python.prefix,
87
- "-m",
88
- "pip",
89
- "install",
90
- "--user",
91
- "--quiet",
92
- `optivision-rag==${PACKAGE_VERSION}`,
93
- ],
94
- { stdio: "inherit" },
95
- );
96
- if (install.status !== 0) {
97
- console.error("optivision-rag: pip install failed. Install pipx for an isolated run instead.");
98
- return install.status ?? 1;
99
- }
100
-
101
- const result = spawnSync(python.cmd, [...python.prefix, "-m", "optivision.cli", ...argv], {
102
- stdio: "inherit",
103
- });
104
- return result.status ?? 1;
105
- }