@myvillage/cli 2.16.0 → 2.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "2.16.0",
3
+ "version": "2.16.1",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  * datasets/models never pulls the heavy runtime in.
15
15
  */
16
16
  import { readFileSync, existsSync } from 'fs';
17
- import { join, dirname, extname } from 'path';
17
+ import { join, dirname, extname, basename } from 'path';
18
18
  import { fileURLToPath } from 'url';
19
19
  import { createRequire } from 'module';
20
20
 
@@ -27,6 +27,16 @@ function requirePkg(name) {
27
27
  export const BACKBONE_ID = 'mobilenet-v3-small';
28
28
  export const INPUT_SIZE = 224;
29
29
  export const EMBED_DIM = 1024;
30
+ /**
31
+ * Pictures up to this size are embedded exactly as lab-runtime embeds them
32
+ * (its MAX_MEGAPIXELS), so a shared model reads them the same way. Anything
33
+ * bigger, like a 48 MP phone photo, is box-shrunk to fit first: it keeps the
34
+ * float copy small and averages the detail the 224×224 resize would skip.
35
+ */
36
+ const MAX_DIRECT_PIXELS = 16 * 1000 * 1000;
37
+ /** jpeg-js's working memory cap; 512 MB stops short of 48 MP phone photos.
38
+ * Its 100 MP resolution cap still applies. */
39
+ const JPEG_MEMORY_MB = 2048;
30
40
 
31
41
  const BACKBONE_DIR = join(
32
42
  dirname(fileURLToPath(import.meta.url)),
@@ -99,18 +109,66 @@ function decodeImage(path) {
99
109
  if (!existsSync(path)) throw new Error(`Image not found: ${path}`);
100
110
  const buf = readFileSync(path);
101
111
  const ext = extname(path).toLowerCase();
102
- if (ext === '.png') {
103
- // Lazy require keeps these decoders out of the hot path for text/tabular.
104
- const { PNG } = requirePkg('pngjs');
105
- const png = PNG.sync.read(buf);
106
- return { width: png.width, height: png.height, data: png.data };
112
+ if (ext !== '.png' && ext !== '.jpg' && ext !== '.jpeg') {
113
+ throw new Error(`Unsupported image type "${ext}". Use .jpg, .jpeg, or .png.`);
107
114
  }
108
- if (ext === '.jpg' || ext === '.jpeg') {
115
+ try {
116
+ if (ext === '.png') {
117
+ // Lazy require keeps these decoders out of the hot path for text/tabular.
118
+ const { PNG } = requirePkg('pngjs');
119
+ const png = PNG.sync.read(buf);
120
+ return { width: png.width, height: png.height, data: png.data };
121
+ }
109
122
  const jpeg = requirePkg('jpeg-js');
110
- const img = jpeg.decode(buf, { useTArray: true, maxMemoryUsageInMB: 512 });
123
+ const img = jpeg.decode(buf, { useTArray: true, maxMemoryUsageInMB: JPEG_MEMORY_MB });
111
124
  return { width: img.width, height: img.height, data: img.data };
125
+ } catch (e) {
126
+ const name = basename(path);
127
+ if (/maxResolutionInMP|maxMemoryUsageInMB/.test(e?.message || '')) {
128
+ throw new Error(`The picture "${name}" is too big to read. Make it smaller (under 100 megapixels) and add it again.`);
129
+ }
130
+ throw new Error(`The picture "${name}" couldn't be read. Try a different JPEG or PNG.`);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * RGBA bytes → RGB floats in [0,1]. Pictures over MAX_DIRECT_PIXELS are
136
+ * averaged down by the smallest whole factor that fits (any leftover edge
137
+ * pixels are dropped); smaller ones are copied pixel for pixel.
138
+ */
139
+ function toRgb({ width, height, data }) {
140
+ let k = 1;
141
+ while (Math.floor(width / k) * Math.floor(height / k) > MAX_DIRECT_PIXELS) k++;
142
+ const w = Math.floor(width / k);
143
+ const h = Math.floor(height / k);
144
+ const rgb = new Float32Array(w * h * 3);
145
+ if (k === 1) {
146
+ for (let i = 0, j = 0; i < data.length; i += 4, j += 3) {
147
+ rgb[j] = data[i] / 255;
148
+ rgb[j + 1] = data[i + 1] / 255;
149
+ rgb[j + 2] = data[i + 2] / 255;
150
+ }
151
+ return { rgb, w, h };
152
+ }
153
+ const scale = 1 / (k * k * 255);
154
+ for (let y = 0; y < h; y++) {
155
+ for (let x = 0; x < w; x++) {
156
+ let r = 0, g = 0, b = 0;
157
+ for (let dy = 0; dy < k; dy++) {
158
+ let i = ((y * k + dy) * width + x * k) * 4;
159
+ for (let dx = 0; dx < k; dx++, i += 4) {
160
+ r += data[i];
161
+ g += data[i + 1];
162
+ b += data[i + 2];
163
+ }
164
+ }
165
+ const j = (y * w + x) * 3;
166
+ rgb[j] = r * scale;
167
+ rgb[j + 1] = g * scale;
168
+ rgb[j + 2] = b * scale;
169
+ }
112
170
  }
113
- throw new Error(`Unsupported image type "${ext}". Use .jpg, .jpeg, or .png.`);
171
+ return { rgb, w, h };
114
172
  }
115
173
 
116
174
  /**
@@ -120,17 +178,11 @@ function decodeImage(path) {
120
178
  export async function embedImage(path) {
121
179
  const tf = await import('@tensorflow/tfjs');
122
180
  const model = await getBackbone();
123
- const { width, height, data } = decodeImage(path);
181
+ const { rgb, w, h } = toRgb(decodeImage(path));
124
182
 
125
183
  return tf.tidy(() => {
126
- // RGBA → RGB float [0,1], shaped [h, w, 3], resized to the backbone input.
127
- const rgb = new Float32Array(width * height * 3);
128
- for (let i = 0, j = 0; i < data.length; i += 4, j += 3) {
129
- rgb[j] = data[i] / 255;
130
- rgb[j + 1] = data[i + 1] / 255;
131
- rgb[j + 2] = data[i + 2] / 255;
132
- }
133
- const img = tf.tensor3d(rgb, [height, width, 3]);
184
+ // RGB float [0,1], shaped [h, w, 3], resized to the backbone input.
185
+ const img = tf.tensor3d(rgb, [h, w, 3]);
134
186
  const resized = tf.image.resizeBilinear(img, [INPUT_SIZE, INPUT_SIZE]);
135
187
  const batched = resized.expandDims(0); // [1, 224, 224, 3]
136
188
  const out = model.predict(batched); // [1, 1024]