picturereader 1.0.2 → 2.0.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/src/guard.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Low-information image guard.
3
+ *
4
+ * Small local VLMs tend to hallucinate on blank / very simple images. Before
5
+ * sending an image to a VLM we can cheaply measure color diversity, dominant
6
+ * color coverage, edge density and brightness variance, then decide whether
7
+ * the image is too empty to be worth a VLM call.
8
+ *
9
+ * @module picturereader/guard
10
+ */
11
+
12
+ const SAMPLE = 64;
13
+
14
+ /**
15
+ * Calculate luminance (Rec.601).
16
+ * @param {number} r - red channel 0..255.
17
+ * @param {number} g - green channel 0..255.
18
+ * @param {number} b - blue channel 0..255.
19
+ * @returns {number} luminance 0..255.
20
+ */
21
+ function gray(r, g, b) {
22
+ return 0.299 * r + 0.587 * g + 0.114 * b;
23
+ }
24
+
25
+ /**
26
+ * Detect low-information images (blank, very simple, or unrendered).
27
+ *
28
+ * The guard checks four heuristics:
29
+ * 1. Color diversity: unique color buckets <= 8
30
+ * 2. Dominant color coverage: top color >= 90%
31
+ * 3. Dominant color with low edge density: top >= 60% AND edges < 8%
32
+ * 4. Low brightness variance: standard deviation < 8
33
+ *
34
+ * @param {Uint8ClampedArray|Buffer} rgba - RGBA pixel data.
35
+ * @param {number} width - image width in pixels.
36
+ * @param {number} height - image height in pixels.
37
+ * @returns {boolean} true when the image looks blank / very low-information.
38
+ */
39
+ export function isLowInformationImage(rgba, width, height) {
40
+ if (width <= 0 || height <= 0 || rgba.length < 4) return true;
41
+
42
+ // Downsample to SAMPLE x SAMPLE (nearest neighbor is fine for a guard).
43
+ const cells = [];
44
+ const cellSizeX = Math.max(1, Math.floor(width / SAMPLE));
45
+ const cellSizeY = Math.max(1, Math.floor(height / SAMPLE));
46
+ const gridW = Math.min(SAMPLE, width);
47
+ const gridH = Math.min(SAMPLE, height);
48
+
49
+ for (let gy = 0; gy < gridH; gy++) {
50
+ for (let gx = 0; gx < gridW; gx++) {
51
+ const px = Math.min(width - 1, gx * cellSizeX + Math.floor(cellSizeX / 2));
52
+ const py = Math.min(height - 1, gy * cellSizeY + Math.floor(cellSizeY / 2));
53
+ const i = (py * width + px) * 4;
54
+ cells.push([rgba[i], rgba[i + 1], rgba[i + 2], rgba[i + 3]]);
55
+ }
56
+ }
57
+
58
+ const buckets = new Map();
59
+ let total = 0;
60
+ let sum = 0;
61
+ let sumSq = 0;
62
+ let edgeCount = 0;
63
+ let edgePairs = 0;
64
+
65
+ for (let y = 0; y < gridH; y++) {
66
+ for (let x = 0; x < gridW; x++) {
67
+ const [r, g, b] = cells[y * gridW + x];
68
+ // Quantize to 3 bits per channel for bucketing
69
+ const key = ((r & 0xe0) << 10) | ((g & 0xe0) << 5) | (b & 0xe0);
70
+ buckets.set(key, (buckets.get(key) ?? 0) + 1);
71
+
72
+ const lum = gray(r, g, b);
73
+ sum += lum;
74
+ sumSq += lum * lum;
75
+ total++;
76
+
77
+ // Check horizontal edge (luminance difference > 20)
78
+ if (x + 1 < gridW) {
79
+ const [r2, g2, b2] = cells[y * gridW + x + 1];
80
+ const lum2 = gray(r2, g2, b2);
81
+ if (Math.abs(lum - lum2) > 20) edgeCount++;
82
+ edgePairs++;
83
+ }
84
+ }
85
+ }
86
+
87
+ const unique = buckets.size;
88
+ const top = Math.max(...buckets.values());
89
+ const topRatio = total > 0 ? top / total : 1;
90
+ const edgeRatio = edgePairs > 0 ? edgeCount / edgePairs : 0;
91
+ const mean = total > 0 ? sum / total : 0;
92
+ const variance = total > 0 ? Math.max(0, sumSq / total - mean * mean) : 0;
93
+ const stdDev = Math.sqrt(variance);
94
+
95
+ return (
96
+ unique <= 8 ||
97
+ topRatio >= 0.9 ||
98
+ (topRatio >= 0.6 && edgeRatio < 0.08) ||
99
+ stdDev < 8
100
+ );
101
+ }
package/src/index.js CHANGED
@@ -1,30 +1,32 @@
1
- /**
2
- * picturereader — pixel-to-text image reading for text-only DeepSeek Harness
3
- * models. One plugin row registers the `image_scan` tool: decode the image,
4
- * downscale it into a coarse cell grid, quantize colors against a small named
5
- * palette, and feed the rendered grids back into the conversation so DeepSeek
6
- * can describe layout, colors and rough shapes without a vision model.
7
- *
8
- * Mount with one row:
9
- *
10
- * ```yaml
11
- * - id: picturereader
12
- * name: 'picturereader'
13
- * ```
14
- * @module picturereader
15
- */
16
-
17
- import { createImageScanTool, createImageOcrTool, createImageSampleTool } from './tool.js';
18
-
19
- export const name = 'picturereader';
20
-
21
- /** Services required at runtime: the tool registry and the filesystem seam. */
22
- export const inject = ['tools', 'fs'];
23
-
24
- export function apply(ctx) {
25
- ctx.effect(() => {
26
- ctx.tools.register(createImageScanTool(ctx));
27
- ctx.tools.register(createImageOcrTool(ctx));
28
- ctx.tools.register(createImageSampleTool(ctx));
29
- });
30
- }
1
+ /**
2
+ * picturereader — pixel-to-text image reading for text-only DeepSeek Harness
3
+ * models. One plugin row registers the `image_scan` tool: decode the image,
4
+ * downscale it into a coarse cell grid, quantize colors against a small named
5
+ * palette, and feed the rendered grids back into the conversation so DeepSeek
6
+ * can describe layout, colors and rough shapes without a vision model.
7
+ *
8
+ * Mount with one row:
9
+ *
10
+ * ```yaml
11
+ * - id: picturereader
12
+ * name: 'picturereader'
13
+ * ```
14
+ * @module picturereader
15
+ */
16
+
17
+ import { createImageScanTool, createImageOcrTool, createImageSampleTool } from './tool.js';
18
+ import { createVisionAnalyzeTool } from './vision-analyze.js';
19
+
20
+ export const name = 'picturereader';
21
+
22
+ /** Services required at runtime: the tool registry and the filesystem seam. */
23
+ export const inject = ['tools', 'fs'];
24
+
25
+ export function apply(ctx) {
26
+ ctx.effect(() => {
27
+ ctx.tools.register(createImageScanTool(ctx));
28
+ ctx.tools.register(createImageOcrTool(ctx));
29
+ ctx.tools.register(createImageSampleTool(ctx));
30
+ ctx.tools.register(createVisionAnalyzeTool(ctx));
31
+ });
32
+ }