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 +105 -349
- package/bin/optivision.js +159 -0
- package/package.json +14 -32
- package/js/bin/optivision-rag.js +0 -6
- package/js/lib/runner.js +0 -105
package/README.md
CHANGED
|
@@ -1,349 +1,105 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
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": "
|
|
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
|
|
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
|
}
|
package/js/bin/optivision-rag.js
DELETED
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
|
-
}
|