zero-vision 0.1.0
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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +163 -0
- package/dist/cdp/attach.js +121 -0
- package/dist/cdp/attach.js.map +1 -0
- package/dist/cdp/client.js +53 -0
- package/dist/cdp/client.js.map +1 -0
- package/dist/cdp/extract.js +197 -0
- package/dist/cdp/extract.js.map +1 -0
- package/dist/cli.js +223 -0
- package/dist/cli.js.map +1 -0
- package/dist/engines/apple-fm.js +103 -0
- package/dist/engines/apple-fm.js.map +1 -0
- package/dist/engines/apple-vision.js +89 -0
- package/dist/engines/apple-vision.js.map +1 -0
- package/dist/engines/cloud/index.js +41 -0
- package/dist/engines/cloud/index.js.map +1 -0
- package/dist/engines/index.js +71 -0
- package/dist/engines/index.js.map +1 -0
- package/dist/engines/local-vlm.js +46 -0
- package/dist/engines/local-vlm.js.map +1 -0
- package/dist/engines/tesseract.js +221 -0
- package/dist/engines/tesseract.js.map +1 -0
- package/dist/mcp.js +225 -0
- package/dist/mcp.js.map +1 -0
- package/dist/native.js +66 -0
- package/dist/native.js.map +1 -0
- package/dist/snap.js +30 -0
- package/dist/snap.js.map +1 -0
- package/dist/test/argv.test.js +22 -0
- package/dist/test/argv.test.js.map +1 -0
- package/dist/test/ax.test.js +28 -0
- package/dist/test/ax.test.js.map +1 -0
- package/dist/test/engines.test.js +81 -0
- package/dist/test/engines.test.js.map +1 -0
- package/dist/test/fetch.test.js +46 -0
- package/dist/test/fetch.test.js.map +1 -0
- package/dist/test/ocr.test.js +34 -0
- package/dist/test/ocr.test.js.map +1 -0
- package/dist/test/parity.test.js +33 -0
- package/dist/test/parity.test.js.map +1 -0
- package/dist/test/port.test.js +55 -0
- package/dist/test/port.test.js.map +1 -0
- package/dist/test/snap.test.js +13 -0
- package/dist/test/snap.test.js.map +1 -0
- package/dist/test/tesseract.test.js +17 -0
- package/dist/test/tesseract.test.js.map +1 -0
- package/docs/spec.md +549 -0
- package/package.json +40 -0
package/docs/spec.md
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# zero-vision
|
|
2
|
+
|
|
3
|
+
Status: accepted for implementation
|
|
4
|
+
Date: 2026-09-07
|
|
5
|
+
Audience: the engineer who builds this in TypeScript and Swift
|
|
6
|
+
|
|
7
|
+
**A local CLI and MCP server that reads a page, a screenshot, or a video as text. Default path never sends pixels off the machine.**
|
|
8
|
+
|
|
9
|
+
GitHub: `unfoundbox-crew/zero-vision`. npm package: `zero-vision` (unscoped `zrv` is taken — a 2020 React leftover). CLI / bin: `zrv`. Native helper: `zrv-native`. MCP stdio: `zrv mcp`.
|
|
10
|
+
|
|
11
|
+
v1 is macOS. Pixel engines need Apple silicon or a configured cloud key. CDP read works anywhere Node 22 can reach a Chrome debug port.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. What this is, and what it is not
|
|
16
|
+
|
|
17
|
+
Agents today screenshot a tab, ship a 1080p or 1440p PNG to a frontier model, and pay visual tokens plus seconds of latency to read text that the browser or the OS already knows.
|
|
18
|
+
|
|
19
|
+
Claude bills images as 28×28 patches: `ceil(w/28) * ceil(h/28)` visual tokens, capped at 1,568 on standard models and 4,784 on Claude 4.7+ ([Vision docs](https://platform.claude.com/docs/en/build-with-claude/vision)). A 1920×1080 screenshot is 1,560 tokens on the standard tier and 2,691 on the high-res tier, before any reasoning.
|
|
20
|
+
|
|
21
|
+
A 30-day forensic audit of 168 visual sessions in this fleet (`~/.agentworth/agentworth.db`, 2026-09-07) measured what those screenshots were *for*:
|
|
22
|
+
|
|
23
|
+
| Share | Sessions | Job |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| 65% | 110 | Pure OCR / verbatim text (error traces, tweets, "is this headline clipped") |
|
|
26
|
+
| 27% | 46 | MotionVector contact sheets and cut verification (3×3 sheets, slide text, title cards) |
|
|
27
|
+
| 8% | 12 | Genuine aesthetic / spatial reasoning |
|
|
28
|
+
| 0% | 0 | Porn |
|
|
29
|
+
|
|
30
|
+
Ninety-two percent of visual sessions were transcription. Eight percent needed a model that can see. The default engine is therefore a transcriber, not a VLM. The 8% gets an explicit `--engine`.
|
|
31
|
+
|
|
32
|
+
It is not a browser driver. It does not click, type, hover, or fill. Chrome DevTools MCP and Playwright MCP already do that with `uid` / `ref`.
|
|
33
|
+
|
|
34
|
+
It does not silently upload pixels. Cloud vision is an opt-in engine (`cloud-vlm`), never the default, never an automatic fallback.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. Rejected approaches
|
|
39
|
+
|
|
40
|
+
| Rejected | Do this instead | Why |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| One hardcoded OCR binary | Engine adapter. Four named engines, one result schema | The 8% will change engines. The 65% must not |
|
|
43
|
+
| Silent cloud fallback | `--engine cloud-vlm` or config. Default stays `apple-vision` | A router that uploads is the bug this tool exists to stop |
|
|
44
|
+
| `click(uid)` in v1 | Read only | chrome-devtools-mcp and Playwright MCP already click |
|
|
45
|
+
| Probe 9222 by default | Probe env, then 1948, then 9223. **9222 only with `--port` or `ZEROVISION_ALLOW_USER_CHROME=1`** | 9222 is often the human's daily Chrome |
|
|
46
|
+
| Puppeteer / Playwright | Raw `fetch` + `WebSocket` (Node 22) | Those libraries *are* the drivers we refuse to clone |
|
|
47
|
+
| ffmpeg required for video | AVFoundation in the Swift binary. ffmpeg optional | ffmpeg is a 50MB+ dep |
|
|
48
|
+
| Quote 15ms / 20ms OCR as a measured SLA | Warm-path **target**. Measure on `hello.png` before the README quotes a number | Apple does not publish 20ms. ANE concurrency is ~2 |
|
|
49
|
+
| Auto-route tree → OCR → cloud | Commands pick the producer. Engines pick the pixel backend | Silent fallback hides "the tree was empty" |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 3. Prior art
|
|
54
|
+
|
|
55
|
+
Compared 2026-09-07 against current docs. Chrome DevTools MCP tools checked against v1.8.0 `snapshot.ts`.
|
|
56
|
+
|
|
57
|
+
| Tool | What it is | Perception | Drive the browser? | Local OCR | Video text |
|
|
58
|
+
| --- | --- | --- | --- | --- | --- |
|
|
59
|
+
| [Playwright MCP](https://playwright.dev/mcp/introduction) | Microsoft MCP. Accessibility snapshot + `ref=eN` | AX tree, optional screenshot | Yes | No | No |
|
|
60
|
+
| [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) v1.8.0 | Google MCP. `take_snapshot`, `take_screenshot` | AX tree, screenshot, traces | Yes | No | No |
|
|
61
|
+
| [Stagehand](https://docs.stagehand.dev) v3/v4 | Browserbase SDK. `act` / `extract` / `observe` | AX + LLM | Yes | No | No |
|
|
62
|
+
| [Crawl4AI](https://docs.crawl4ai.com/core/simple-crawling/) | Self-hosted crawler | Rendered HTML | Crawl, not UI | No | No |
|
|
63
|
+
| [Browser-Use](https://github.com/browser-use/browser-use) | Python agent loop | Hybrid, still pays vision often | Yes | No | No |
|
|
64
|
+
| [ocrtool-mcp](https://github.com/ihugang/ocrtool-mcp) | Swift + Vision, MCP only | Image OCR | No | Yes | No |
|
|
65
|
+
|
|
66
|
+
**The hole:** none of these is attach-only Chrome text plus on-device OCR plus video/contact-sheet text plus a swappable VLM, behind one CLI. The 27% contact-sheet sessions are a still PNG (one image, many frames of text). That is `apple-vision` on a file, not a video pipeline and not a cloud VLM.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4. Architecture
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
zrv / zero-vision MCP
|
|
74
|
+
|
|
|
75
|
+
+------------------+------------------+
|
|
76
|
+
| |
|
|
77
|
+
CDP reader Pixel perceiver
|
|
78
|
+
(TypeScript) (engine adapter)
|
|
79
|
+
| |
|
|
80
|
+
localhost debug port +------------+------------+
|
|
81
|
+
attach, never launch | | |
|
|
82
|
+
v v v
|
|
83
|
+
apple-vision apple-fm local-vlm
|
|
84
|
+
(default) (fm+OCRTool) (MLX Qwen3-VL)
|
|
85
|
+
|
|
|
86
|
+
cloud-vlm (opt-in)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Two producers:
|
|
90
|
+
|
|
91
|
+
1. **CDP reader** — web pages already open (or a static URL fetched without a browser). Not an engine. Always local.
|
|
92
|
+
2. **Pixel perceiver** — screenshots, clipboard, local files, video keyframes, contact sheets. Speaks one interface. Four engines implement it.
|
|
93
|
+
|
|
94
|
+
A third outcome, not a producer: **`opaque`**. If a canvas or video covers ≥30% of the viewport, the reader sets `opaque: true`. It does not upload. `zrv --ocr-opaque` captures those rectangles and runs the **current pixel engine**.
|
|
95
|
+
|
|
96
|
+
Default path for a tab: CDP text. Default path for a file: `apple-vision`. No implicit cloud.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 5. Engine adapter
|
|
101
|
+
|
|
102
|
+
### 5.1 Interface
|
|
103
|
+
|
|
104
|
+
TypeScript owns routing. Each engine is a process or a function that accepts the same input and returns the same JSON. Adding a fifth engine is a new file that implements this, plus a name in the registry. No other package changes.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
type EngineId = "apple-vision" | "apple-fm" | "local-vlm" | "cloud-vlm";
|
|
108
|
+
type Task = "transcribe" | "describe";
|
|
109
|
+
|
|
110
|
+
interface PerceiveInput {
|
|
111
|
+
kind: "image" | "video" | "clipboard";
|
|
112
|
+
path?: string; // absolute file
|
|
113
|
+
bytes?: Buffer; // stdin / base64 decoded, cap 30 MB
|
|
114
|
+
task: Task; // default transcribe
|
|
115
|
+
lang?: string[];
|
|
116
|
+
level?: "accurate" | "fast"; // apple-vision only
|
|
117
|
+
video?: { mode: "scene" | "interval" | "all-idr"; interval?: number; maxFrames?: number };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface TextBlock {
|
|
121
|
+
text: string;
|
|
122
|
+
confidence?: number; // 0..1 when the engine has it
|
|
123
|
+
bbox?: { x: number; y: number; w: number; h: number }; // Vision-normalized, origin bottom-left
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface PerceptionResult {
|
|
127
|
+
ok: boolean;
|
|
128
|
+
engine: EngineId;
|
|
129
|
+
task: Task;
|
|
130
|
+
text: string; // always present on ok. Verbatim for transcribe, prose for describe
|
|
131
|
+
blocks?: TextBlock[]; // apple-vision. Others may omit
|
|
132
|
+
transcript?: { t: number; text: string }[]; // video only
|
|
133
|
+
ms: number;
|
|
134
|
+
tokens?: { input: number; output: number };
|
|
135
|
+
costUsd?: number;
|
|
136
|
+
model?: string; // fm / vlm / cloud model id
|
|
137
|
+
error?: string;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`transcribe` is the 65%+27% path. `describe` is the 8% path. Engines that cannot describe (`apple-vision`) reject `task: describe` with exit 1: `engine apple-vision cannot describe; use apple-fm, local-vlm, or cloud-vlm`.
|
|
142
|
+
|
|
143
|
+
Selection, first match wins:
|
|
144
|
+
|
|
145
|
+
1. `--engine <id>`
|
|
146
|
+
2. `ZEROVISION_ENGINE`
|
|
147
|
+
3. `~/.config/zero-vision/config.json` → `engine`
|
|
148
|
+
4. `apple-vision`
|
|
149
|
+
|
|
150
|
+
Config shape:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"engine": "apple-vision",
|
|
155
|
+
"engines": {
|
|
156
|
+
"local-vlm": {
|
|
157
|
+
"modelPath": "~/.cache/huggingface/hub/models--mlx-community--Qwen3-VL-8B-Instruct-4bit"
|
|
158
|
+
},
|
|
159
|
+
"cloud-vlm": {
|
|
160
|
+
"provider": "gemini",
|
|
161
|
+
"model": "gemini-3.1-flash-lite",
|
|
162
|
+
"apiKeyEnv": "GEMINI_API_KEY"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Do not read API keys from the config file. Env only.
|
|
169
|
+
|
|
170
|
+
### 5.2 `apple-vision` (default)
|
|
171
|
+
|
|
172
|
+
Compiled Swift CLI, spawned, JSON on stdout. Same packaging as before: optionalDependency `@zero-vision/darwin-arm64`.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
zrv-native ocr-image --input <path> --level accurate|fast --json
|
|
176
|
+
zrv-native ocr-clipboard --level accurate --json
|
|
177
|
+
zrv-native ocr-video --input <path> --mode scene --interval 2 --max-frames 60 --json
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- macOS 15+: `RecognizeTextRequest`. macOS 14: `VNRecognizeTextRequest` revision 3. Pin the revision.
|
|
181
|
+
- `.accurate` default, `.fast` for `--level fast`. Language correction on. `--lang` repeatable.
|
|
182
|
+
- Serialize requests. ANE stalls above ~2 concurrent (Apple DTS 2025). Process-wide lock of 1.
|
|
183
|
+
- `bbox` is Vision-normalized (origin bottom-left). `--bbox css` flips origin if a caller asks.
|
|
184
|
+
- Exit codes: 0 ok, 1 usage, 2 input missing, 3 Vision unavailable, 4 recognize failed, 5 unsupported video container.
|
|
185
|
+
|
|
186
|
+
**Latency:** warm-path **target** after process start is under 200ms `.accurate` on a 1280×800 printed-English screenshot. A 20ms figure is a target for `.fast` on a cropped region, not a measurement. Do not print either number in the README until `hello.png` is timed on a named machine.
|
|
187
|
+
|
|
188
|
+
Video: AVFoundation `AVAssetImageGenerator`, in-memory `CGImage`. Scene mode = 2 fps sample, keep if 64-bit dHash Hamming > 10, always first and last. Caps: 60 frames default, 200 hard, 180 seconds. Dedup by casefolded whitespace-collapsed text.
|
|
189
|
+
|
|
190
|
+
Contact sheets (27% of sessions): they are PNGs. `ocr-image` on the sheet is the path. Do not split a 3×3 grid in v1. The whole sheet is one transcription. `--grid 3x3` is a later engine option, not a new engine.
|
|
191
|
+
|
|
192
|
+
ffmpeg: only if AVFoundation cannot decode. Temp `.mp4`, OCR, delete. Guaranteed containers: `mp4`, `mov`, `m4v`.
|
|
193
|
+
|
|
194
|
+
### 5.3 `apple-fm`
|
|
195
|
+
|
|
196
|
+
On-device Foundation Model plus Vision `OCRTool`. Verified 2026-09-07: `/usr/bin/fm` is licensed, `fm available` reports `System model available`, and the CLI takes images:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
fm respond --image shot.png --text 'Transcribe all text verbatim.' --tool ocr --no-stream
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The adapter shells out to `fm`. It does not embed FoundationModels in the Swift OCR binary in v1 (two crash domains). Map stdout to `PerceptionResult.text`. `engine: "apple-fm"`, `model: "system"`. `blocks` omitted unless a later `fm` schema lands.
|
|
203
|
+
|
|
204
|
+
Requires macOS 27. If `fm` is missing or the license is unsigned, exit 3: `apple-fm unavailable`.
|
|
205
|
+
|
|
206
|
+
Use this when the user wants a sentence of understanding on top of OCR (clipped headline: "yes, the last two letters are cut") without leaving the machine. Still not Claude.
|
|
207
|
+
|
|
208
|
+
### 5.4 `local-vlm`
|
|
209
|
+
|
|
210
|
+
Opt-in. Default weights: the already-cached MLX 4-bit Qwen3-VL-8B at
|
|
211
|
+
|
|
212
|
+
```text
|
|
213
|
+
~/.cache/huggingface/hub/models--mlx-community--Qwen3-VL-8B-Instruct-4bit
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
(5.4 GB on disk, verified 2026-09-07). Do not download weights in v1. If the path is missing, exit 3 with the path we looked at.
|
|
217
|
+
|
|
218
|
+
Spawn via the SpacePilot / MLX runtime already on the machine (`llama-cpp` serves vision; `mlx-lm` is text-only — use a vision-capable runner). Timeout 60s. `task: describe` is the point of this engine. `task: transcribe` is allowed but the README must say `apple-vision` is the transcriber.
|
|
219
|
+
|
|
220
|
+
**Unflown on M1 Max.** SpacePilot lists `qwen2-5-vl-7b-instruct` as `runs_well` on paper and **unflown**. Do not quote tok/s. A local VLM is a privacy/latency play against a cloud round trip, not a quality play against Fable.
|
|
221
|
+
|
|
222
|
+
Fan rule: never load this model from a test suite on a laptop. One optional `ZEROVISION_LIVE_VLM=1` test, off by default.
|
|
223
|
+
|
|
224
|
+
### 5.5 `cloud-vlm`
|
|
225
|
+
|
|
226
|
+
Opt-in. Never probed, never default, never chosen because another engine failed.
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"provider": "gemini" | "anthropic" | "openai" | "custom",
|
|
231
|
+
"model": "gemini-3.1-flash-lite",
|
|
232
|
+
"baseUrl": "https://…", // custom only
|
|
233
|
+
"apiKeyEnv": "GEMINI_API_KEY"
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
OpenAI-compatible POST for `custom`. Provider SDKs otherwise, lazy-loaded so the default install still has one runtime dependency (`@modelcontextprotocol/sdk`). Cloud adapters live in `src/engines/cloud/` and pull their SDK only when that engine is selected.
|
|
238
|
+
|
|
239
|
+
Prices move. Do not hardcode "$0.05/Mtok". Print `costUsd` from the provider's usage field when present; otherwise omit.
|
|
240
|
+
|
|
241
|
+
Refuse to run if `apiKeyEnv` is unset. Refuse to run if the input is larger than 10 MB. Log `engine=cloud-vlm model=… bytes=…` to stderr, never the image bytes.
|
|
242
|
+
|
|
243
|
+
This is the 8% aesthetic path when the user wants frontier quality. Creative direction, layout judgment, humor. Not transcription.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
### 5.6 `tesseract` (Linux OCR)
|
|
248
|
+
|
|
249
|
+
Pure-npm OCR via tesseract.js — no apt, no system daemon. Default pixel
|
|
250
|
+
engine on Linux, same `PerceiveInput` (`task`, `lang`, `level`) as
|
|
251
|
+
`apple-vision`. `level: fast` downsamples long side to 1600px first.
|
|
252
|
+
|
|
253
|
+
When a system `tesseract` binary is on PATH, prefer it (same engine id,
|
|
254
|
+
faster, same output contract) and note `backend: "system"` vs `"wasm"`
|
|
255
|
+
in stderr. Either way the rank holds: local first, never cloud unless named.
|
|
256
|
+
|
|
257
|
+
Video on Linux is ffmpeg (optional dep): scene/interval keyframe extract,
|
|
258
|
+
then this engine per frame. Missing ffmpeg fails closed with the install
|
|
259
|
+
hint, mirroring the missing-native exit 3.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## 6. Native packaging (`apple-vision` binary)
|
|
264
|
+
|
|
265
|
+
Ship a **compiled executable**, not a dylib, not a `.node` addon, not `swift-sh`.
|
|
266
|
+
|
|
267
|
+
Node calls it with `spawn`, argv, and a timeout. JSON on stdout. Logs on stderr. One-shot per invocation.
|
|
268
|
+
|
|
269
|
+
```text
|
|
270
|
+
npm package zero-vision
|
|
271
|
+
optionalDependency @zero-vision/darwin-arm64
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Resolve, in order: `ZEROVISION_NATIVE`, that package's `bin/zrv-native`, `./native/.build/release/zrv-native` (dev).
|
|
275
|
+
|
|
276
|
+
If the binary is missing, CDP commands still run. `apple-vision` commands exit 3.
|
|
277
|
+
|
|
278
|
+
Rejected: dylib / N-API, swift-sh, in-process Vision (a crash must not kill MCP).
|
|
279
|
+
|
|
280
|
+
v1 ships `darwin-arm64` only.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 7. Snap (human CLI only)
|
|
285
|
+
|
|
286
|
+
`snap` is not an MCP tool. Interactive capture needs a TTY and a GUI session.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
zrv snap # screencapture -i -c -x image → clipboard
|
|
290
|
+
zrv snap --ocr # capture, run current engine, print text, pbcopy the text
|
|
291
|
+
zrv snap --save out.png
|
|
292
|
+
zrv snap --ocr --engine apple-fm
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`/usr/sbin/screencapture`: `-i` interactive, `-c` clipboard, `-x` silent, `-t png`. Temp files deleted unless `--save`. Escape → exit 130, print nothing.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 8. Browser CDP
|
|
300
|
+
|
|
301
|
+
Unchanged in spirit. Node 22 `fetch` + `WebSocket`. No puppeteer, no playwright.
|
|
302
|
+
|
|
303
|
+
1. `GET http://127.0.0.1:<port>/json/version` → `webSocketDebuggerUrl`
|
|
304
|
+
2. WebSocket to the browser target
|
|
305
|
+
3. `Target.setDiscoverTargets { discover: true }`
|
|
306
|
+
4. `Target.getTargets`
|
|
307
|
+
5. `Target.attachToTarget { targetId, flatten: true }`
|
|
308
|
+
6. Detach on exit
|
|
309
|
+
|
|
310
|
+
Do not launch Chrome. Nothing listening → exit 2.
|
|
311
|
+
|
|
312
|
+
Port probe: `--port` / `ZEROVISION_PORT`, then `AGENT_CHROME_PORT`, then `1948`, then `9223`. First `/json/version` 200 in 150ms wins. **Never 9222** unless `--port 9222` or `ZEROVISION_ALLOW_USER_CHROME=1`. Loopback only unless `--host`.
|
|
313
|
+
|
|
314
|
+
Tab pick: `type=page` (or tab whose child is a page). Drop `chrome://`, `devtools://`, `chrome-extension://`, `edge://`. Default = first non-internal page, title+URL on stderr. `--tab` is id or unique substring. `--url` matches prefix. `--url --navigate` creates a **new** tab, never navigates an existing one. Two matches → exit 1 with the list.
|
|
315
|
+
|
|
316
|
+
Wait: `readyState` / `Page.loadEventFired` (8s), `Accessibility.enable` + `loadComplete` (2s, proceed anyway), quiet 250ms / 5s wall. `--wait-ms`, `--wait-text`. Then `Accessibility.disable` if we enabled it.
|
|
317
|
+
|
|
318
|
+
Shadow DOM: AX flattens it. Default `zrv` text is AX-derived, not `innerText`. Same-origin iframes: `getFullAXTree` per `frameId`. OOPIF: `setAutoAttach`, skip on attach failure.
|
|
319
|
+
|
|
320
|
+
**Text / markdown / a11y YAML:** interesting-only walk. Skip `ignored`, roles `none`/`presentation`. Fold `InlineTextBox`. Cap 24,000 characters. YAML is Playwright-shaped with `uid=eN`. `--a11y --verbose` dumps raw CDP nodes.
|
|
321
|
+
|
|
322
|
+
Opaque probe: `canvas, video` coverage. Do **not** call `canvas.getContext('webgl')`. `--ocr-opaque` screenshots qualifying clips (CSS viewport `getBoundingClientRect`, scroll into view first) and runs the current pixel engine.
|
|
323
|
+
|
|
324
|
+
`--url --fetch`: HTTP GET, no JS. SPA shell (<200 chars of text, `div#root`) → exit 1 telling them to open it in debug Chrome.
|
|
325
|
+
|
|
326
|
+
WebMCP: if the page has tools, the user already has chrome-devtools-mcp `list_webmcp_tools` behind `--categoryExperimentalWebmcp`. zero-vision does not clone that. A skill, not this server, should try page tools before a screenshot.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## 9. CLI
|
|
331
|
+
|
|
332
|
+
One bin: `zrv`. `snap` is an argv[0] alias.
|
|
333
|
+
|
|
334
|
+
```text
|
|
335
|
+
zrv text of the chosen tab
|
|
336
|
+
zrv --md markdown
|
|
337
|
+
zrv --a11y interesting AX tree
|
|
338
|
+
zrv --url <url>
|
|
339
|
+
zrv --url <url> --navigate
|
|
340
|
+
zrv --url <url> --fetch
|
|
341
|
+
zrv --tab <id|substr>
|
|
342
|
+
zrv --tabs
|
|
343
|
+
zrv --ocr-opaque
|
|
344
|
+
zrv --engine apple-vision|apple-fm|local-vlm|cloud-vlm
|
|
345
|
+
zrv --wait-ms N
|
|
346
|
+
zrv --wait-text GLOB
|
|
347
|
+
zrv --scroll
|
|
348
|
+
zrv --json
|
|
349
|
+
zrv --port N
|
|
350
|
+
|
|
351
|
+
zrv ocr <file.png|jpg|heic|pdf|mp4|mov>
|
|
352
|
+
zrv ocr --clipboard
|
|
353
|
+
zrv ocr --task transcribe|describe
|
|
354
|
+
zrv ocr --engine local-vlm
|
|
355
|
+
zrv ocr --level fast
|
|
356
|
+
zrv ocr --lang en-US --lang ja-JP
|
|
357
|
+
|
|
358
|
+
zrv snap
|
|
359
|
+
zrv snap --ocr
|
|
360
|
+
zrv snap --save <path>
|
|
361
|
+
|
|
362
|
+
zrv mcp
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
PDF: v1 OCRs page 1.
|
|
366
|
+
|
|
367
|
+
`--json` for tab reads includes `engine` only when a pixel engine ran (`--ocr-opaque`). CDP reads use `"source": "cdp"`.
|
|
368
|
+
|
|
369
|
+
stderr is for humans. stdout is the payload. MCP must not print banners on stdout.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 10. MCP server
|
|
374
|
+
|
|
375
|
+
Bin `zrv-mcp` = `zrv mcp`. Transport: stdio. SDK: `@modelcontextprotocol/sdk`.
|
|
376
|
+
|
|
377
|
+
Five tools. No click. No screenshot-as-image.
|
|
378
|
+
|
|
379
|
+
`peek_tabs` — list attachable pages.
|
|
380
|
+
|
|
381
|
+
`peek_page` — `targetId` XOR `url` XOR default tab. `format: text|markdown`. `navigate`, `fetch`, `ocrOpaque`, `waitMs`, `engine`.
|
|
382
|
+
|
|
383
|
+
`peek_a11y` — same targeting, plus `verbose`.
|
|
384
|
+
|
|
385
|
+
`ocr_image` — exactly one of `path`, `base64`, `clipboard`. `engine`, `task` (default `transcribe`). Path absolute. Base64 cap 30 MB, temp file, unlink.
|
|
386
|
+
|
|
387
|
+
`ocr_video` — `path` absolute. `mode`, `interval`, `maxFrames`, `engine` (pixel engines that support video: `apple-vision` required in v1; others may refuse video).
|
|
388
|
+
|
|
389
|
+
Tool descriptions must say **prefer this over a screenshot when the goal is to read text**, and **use `apple-vision` unless the user asked to describe**.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## 11. Repository and build
|
|
394
|
+
|
|
395
|
+
Standalone repo, Apache-2.0. This spec is the design of record.
|
|
396
|
+
|
|
397
|
+
```text
|
|
398
|
+
zero-vision/
|
|
399
|
+
package.json
|
|
400
|
+
tsconfig.json
|
|
401
|
+
src/
|
|
402
|
+
cli.ts
|
|
403
|
+
mcp.ts
|
|
404
|
+
cdp/client.ts
|
|
405
|
+
cdp/attach.ts
|
|
406
|
+
cdp/extract.ts
|
|
407
|
+
engines/
|
|
408
|
+
index.ts registry, PerceiveInput / PerceptionResult
|
|
409
|
+
apple-vision.ts spawn zrv-native
|
|
410
|
+
apple-fm.ts spawn fm respond --image --tool ocr
|
|
411
|
+
local-vlm.ts spawn MLX / llama-cpp vision
|
|
412
|
+
cloud/
|
|
413
|
+
index.ts
|
|
414
|
+
gemini.ts
|
|
415
|
+
anthropic.ts
|
|
416
|
+
openai.ts
|
|
417
|
+
custom.ts
|
|
418
|
+
snap.ts
|
|
419
|
+
native/
|
|
420
|
+
Package.swift
|
|
421
|
+
Sources/zrv-native/
|
|
422
|
+
main.swift
|
|
423
|
+
ImageOCR.swift
|
|
424
|
+
VideoOCR.swift
|
|
425
|
+
Clipboard.swift
|
|
426
|
+
Tests/zrv-nativeTests/
|
|
427
|
+
test/
|
|
428
|
+
cdp/
|
|
429
|
+
engines/ mock each adapter against the unified schema
|
|
430
|
+
extract/
|
|
431
|
+
cli/
|
|
432
|
+
fixtures/
|
|
433
|
+
pages/tiny.html
|
|
434
|
+
pages/spa-shell.html
|
|
435
|
+
ax/tiny.json
|
|
436
|
+
ocr/hello.png
|
|
437
|
+
ocr/hello.expected.txt
|
|
438
|
+
ocr/contact-sheet.png # 3×3 title cards, the 27% case
|
|
439
|
+
video/slides.mp4
|
|
440
|
+
.github/workflows/ci.yml
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
`package.json` dependencies: `@modelcontextprotocol/sdk` only at install. Cloud SDKs are optional peerDependencies, imported only when `cloud-vlm` is selected.
|
|
444
|
+
|
|
445
|
+
Engines: `"node": ">=22"`.
|
|
446
|
+
|
|
447
|
+
CI: Ubuntu = TS + mock CDP + mock engines. macOS = plus `swift test` + `hello.png` contains `HELLO ZEROVISION` + contact-sheet fixture yields more than one title card. Skip `local-vlm` and `cloud-vlm` in CI.
|
|
448
|
+
|
|
449
|
+
Release: GitHub Actions builds the arm64 native binary, publishes `@zero-vision/darwin-arm64` then `zero-vision`. Restore the executable bit after `upload-artifact`.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## 12. Testing
|
|
454
|
+
|
|
455
|
+
| Layer | How |
|
|
456
|
+
| --- | --- |
|
|
457
|
+
| AX walk | Golden files from recorded `getFullAXTree` JSON |
|
|
458
|
+
| Port probe | Mock HTTP. Assert 9222 skipped |
|
|
459
|
+
| Engine schema | Each adapter, including mocks, round-trips `PerceptionResult` |
|
|
460
|
+
| apple-vision | `hello.png`. Skip if not darwin |
|
|
461
|
+
| Contact sheet | `contact-sheet.png` produces ≥3 distinct lines |
|
|
462
|
+
| Video | `slides.mp4`. Two timestamps, deduped |
|
|
463
|
+
| apple-fm | Mock `fm` stdout. Live `ZEROVISION_LIVE_FM=1` off by default |
|
|
464
|
+
| local-vlm / cloud-vlm | Mocks only in CI |
|
|
465
|
+
| Snap | Unit-test argv to screencapture, not interactive `-i` |
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 13. Security
|
|
470
|
+
|
|
471
|
+
- CDP is unauthenticated. Loopback only.
|
|
472
|
+
- Never default-attach to 9222.
|
|
473
|
+
- Never navigate unless `--navigate`.
|
|
474
|
+
- Never `Runtime.evaluate` user-supplied JS.
|
|
475
|
+
- OCR `path`: realpath, must be a file.
|
|
476
|
+
- MCP `base64`: size cap, `mkdtemp`, unlink in `finally`.
|
|
477
|
+
- `cloud-vlm` never runs without an env key. Never log image bytes.
|
|
478
|
+
- `snap` is CLI-only.
|
|
479
|
+
- Do not log page text to files.
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## 14. Non-goals (v1)
|
|
484
|
+
|
|
485
|
+
- Click, type, hover, fill
|
|
486
|
+
- Launching or killing Chrome
|
|
487
|
+
- Silent cloud fallback
|
|
488
|
+
- Downloading VLM weights
|
|
489
|
+
- Splitting contact-sheet grids
|
|
490
|
+
- Windows / Linux OCR
|
|
491
|
+
- Browser extension, stealth, crawl
|
|
492
|
+
- Shipping UIDs as a click API
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## 15. Key decisions
|
|
497
|
+
|
|
498
|
+
1. **Read tool, not a driver.**
|
|
499
|
+
2. **Engine adapter, one schema.** The 65% and the 8% do not share a backend. They share an interface.
|
|
500
|
+
3. **Default is `apple-vision`.** Matches 92% of audited visual sessions (transcription + contact sheets).
|
|
501
|
+
4. **Cloud is an engine, not a fallback.** Opt-in, keyed, logged.
|
|
502
|
+
5. **`fm` is the on-device understander.** Licensed and available on macOS 27. OCRTool is the transcriber inside that session.
|
|
503
|
+
6. **Local VLM is cached-weights-or-fail.** No surprise 15 GB download.
|
|
504
|
+
7. **Compiled Swift for Vision only.** `fm` and MLX stay out of that process.
|
|
505
|
+
8. **9222 is opt-in. AX-derived text, not `innerText`.**
|
|
506
|
+
9. **Contact sheets are images.** The 27% case is `ocr-image`, not `ocr-video`.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## 16. Implementation order
|
|
511
|
+
|
|
512
|
+
1. Engine interface + `apple-vision` + `hello.png`
|
|
513
|
+
2. CDP attach + `zrv --tabs` + AX text
|
|
514
|
+
3. Extract: markdown, a11y YAML, opaque probe
|
|
515
|
+
4. Contact-sheet fixture
|
|
516
|
+
5. CLI snap
|
|
517
|
+
6. MCP, `engine` on pixel tools
|
|
518
|
+
7. Video scene mode
|
|
519
|
+
8. `apple-fm` adapter (`fm respond --image --tool ocr`)
|
|
520
|
+
9. `local-vlm` adapter against the cached 4-bit Qwen3-VL
|
|
521
|
+
10. `cloud-vlm` adapter, last, behind config
|
|
522
|
+
11. Packaging
|
|
523
|
+
|
|
524
|
+
Do not start at MCP. Do not add click. Do not wire cloud before the default transcriber is boring.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## 17. Not confirmed
|
|
529
|
+
|
|
530
|
+
- End-to-end `apple-vision` latency on this M1 Max. Target ≠ measurement.
|
|
531
|
+
- Qwen3-VL-8B-4bit tok/s or TTFT on this machine. Cached, unflown.
|
|
532
|
+
- Whether `fm respond --tool ocr` returns structured blocks or only prose. Adapter treats stdout as `text` until a schema is verified.
|
|
533
|
+
- Whether `/json/list` order tracks focus in Chrome 144+.
|
|
534
|
+
- Current $ / MTok of any cloud Flash-class model. Read the provider on the day you wire `cloud-vlm`.
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## 18. Sources
|
|
539
|
+
|
|
540
|
+
- Fleet audit, 168 visual sessions, `~/.agentworth/agentworth.db`, 2026-09-07 (65 / 27 / 8 / 0)
|
|
541
|
+
- `fm respond --help`, `fm available` → System model available, 2026-09-07
|
|
542
|
+
- Claude Vision patch tokens: https://platform.claude.com/docs/en/build-with-claude/vision
|
|
543
|
+
- Chrome CDP Accessibility / Target domains
|
|
544
|
+
- Playwright MCP snapshots: https://playwright.dev/mcp/snapshots
|
|
545
|
+
- Apple `VNRecognizeTextRequest`; WWDC26 Foundation Models `OCRTool` (session 241)
|
|
546
|
+
- Apple DTS ANE concurrency: https://developer.apple.com/forums/thread/784672
|
|
547
|
+
- SpacePilot `check` / `models` on this M1 Max, 2026-09-07: Qwen3-VL-8B-4bit 5.4 GB cached, Qwen2.5-VL-7B recipe unflown
|
|
548
|
+
- Node 22: global `fetch`, global `WebSocket`
|
|
549
|
+
- `screencapture(1)`
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zero-vision",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read a page, a screenshot, or a video as text. Default path never sends pixels off the machine.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"bin": {
|
|
8
|
+
"zrv": "dist/cli.js",
|
|
9
|
+
"zrv-mcp": "dist/mcp.js",
|
|
10
|
+
"snap": "dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"docs/spec.md",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"NOTICE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"native": "swift build -c release --package-path native --product zrv-native",
|
|
25
|
+
"test": "tsc && node --test dist/test/*.js",
|
|
26
|
+
"zrv": "node dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+ssh://git@github.com/unfoundbox-crew/zero-vision.git"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
34
|
+
"tesseract.js": "^6.0.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.13.10",
|
|
38
|
+
"typescript": "^5.8.2"
|
|
39
|
+
}
|
|
40
|
+
}
|