editmamei 0.23.0 → 1.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.
Files changed (90) hide show
  1. package/NOTICES.md +6 -30
  2. package/README.md +47 -13
  3. package/dist/api/extendscript/_helpers.js +1 -101
  4. package/dist/api/photoshop-api.js +23 -16
  5. package/dist/api/snippet-client.js +38 -2
  6. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  7. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  8. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  9. package/dist/core/server.js +60 -184
  10. package/dist/core/session.js +9 -10
  11. package/dist/core/tool-registry.js +4 -4
  12. package/dist/delivery/client.js +50 -1
  13. package/dist/delivery/provision.js +18 -1
  14. package/dist/delivery/signing.js +38 -1
  15. package/dist/delivery/store.js +157 -16
  16. package/dist/detection/detect-active-doc.js +42 -11
  17. package/dist/detection/detection-client.js +3 -3
  18. package/dist/detection/face-detector.js +2 -2
  19. package/dist/detection/geometry.js +14 -0
  20. package/dist/detection/landmark-detection-client.js +3 -3
  21. package/dist/detection/mesh-face.js +2 -3
  22. package/dist/detection/object-detector.js +2 -2
  23. package/dist/detection/runtime.js +14 -3
  24. package/dist/detection/sam-segmenter.js +69 -26
  25. package/dist/kernel/kernel.js +5 -9
  26. package/dist/kernel/module-lifecycle.js +187 -0
  27. package/dist/license/entitlement.js +13 -3
  28. package/dist/license/store.js +8 -1
  29. package/dist/modules/ce/index.js +1 -1
  30. package/dist/perception/facets.js +19 -1
  31. package/dist/perception/grounding-anchors.js +2 -12
  32. package/dist/perception/grounding-locate.js +1 -9
  33. package/dist/perception/grounding-review-crop.js +4 -29
  34. package/dist/perception/overlay.js +62 -0
  35. package/dist/perception/pixel-identity.js +41 -0
  36. package/dist/perception/region-precompute.js +96 -20
  37. package/dist/perception/scene-model.js +93 -50
  38. package/dist/perception/select-recipes.js +109 -68
  39. package/dist/perception/sky-ground-flood.js +0 -5
  40. package/dist/perception/sky-mask-transfer.js +73 -14
  41. package/dist/platform/connection.js +60 -43
  42. package/dist/platform/host-platform.js +16 -0
  43. package/dist/platform/macos-detector.js +41 -70
  44. package/dist/platform/macos-runner.js +94 -0
  45. package/dist/platform/script-queue.js +68 -0
  46. package/dist/platform/script-result.js +39 -0
  47. package/dist/platform/windows-detector.js +49 -66
  48. package/dist/platform/windows-runner.js +84 -0
  49. package/dist/skills/editmamei-skill.zip +0 -0
  50. package/dist/tools/adjustment-tools.js +80 -132
  51. package/dist/tools/brush-tools.js +4 -15
  52. package/dist/tools/channel-compose-tools.js +4 -23
  53. package/dist/tools/detection-tools.js +59 -47
  54. package/dist/tools/document-tools.js +92 -127
  55. package/dist/tools/filter-tools.js +490 -731
  56. package/dist/tools/group-tools.js +75 -156
  57. package/dist/tools/guide-tools.js +52 -83
  58. package/dist/tools/history-tools.js +29 -63
  59. package/dist/tools/image-placement-tools.js +18 -37
  60. package/dist/tools/image-tools.js +23 -51
  61. package/dist/tools/layer-ordering-tools.js +2 -9
  62. package/dist/tools/layer-properties-tools.js +207 -395
  63. package/dist/tools/layer-tools.js +224 -190
  64. package/dist/tools/layer-transform-tools.js +41 -106
  65. package/dist/tools/metadata-tools.js +2 -9
  66. package/dist/tools/path-tools.js +4 -15
  67. package/dist/tools/portrait-tools.js +2 -5
  68. package/dist/tools/preview-tools.js +59 -72
  69. package/dist/tools/retouch-tools.js +33 -71
  70. package/dist/tools/scene-tools.js +123 -75
  71. package/dist/tools/selection-tools.js +386 -566
  72. package/dist/tools/shape-tools.js +2 -9
  73. package/dist/tools/text-tools.js +24 -57
  74. package/dist/tools/transform-canvas-tools.js +21 -54
  75. package/dist/tools/vector-mask-tools.js +2 -9
  76. package/dist/update/check.js +1 -1
  77. package/dist/utils/jpeg-quality.js +1 -0
  78. package/dist/utils/logger.js +55 -45
  79. package/dist/utils/operation-timeouts.js +1 -0
  80. package/dist/utils/run-script.js +7 -2
  81. package/dist/utils/session-log.js +134 -12
  82. package/dist/utils/template-storage.js +5 -6
  83. package/dist/utils/tool-helpers.js +43 -0
  84. package/dist/version.js +1 -1
  85. package/package.json +10 -3
  86. package/LICENSE +0 -81
  87. package/dist/platform/detector.js +0 -32
  88. package/dist/platform/macos-executor.js +0 -151
  89. package/dist/platform/windows-executor.js +0 -160
  90. /package/dist/platform/{script-executor.js → ports.js} +0 -0
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdir
3
3
  import { settingsDir } from '../core/settings.js';
4
4
  import { installBundle } from './bundle.js';
5
5
  import { sha256Hex } from './crypto.js';
6
- import { verifyModuleSignature, MODULE_SIGNING_PUBLIC_KEYS } from './signing.js';
6
+ import { verifyModuleSignature, verifyModuleSignatureV2, digestsRootSha256Hex, isModuleFileDigestArray, MODULE_SIGNING_PUBLIC_KEYS, } from './signing.js';
7
7
  import { Logger } from '../utils/logger.js';
8
8
  const logger = new Logger('Modules');
9
9
  const INSTALLED_FILENAME = 'installed.json';
@@ -31,7 +31,7 @@ export function moduleBinDir(sku, version, opts = {}) {
31
31
  export function moduleArtifactPath(sku, version, opts = {}) {
32
32
  return join(installedModuleDir(sku, version, opts), MODULE_ARTIFACT);
33
33
  }
34
- function isInstalledModule(v) {
34
+ function isInstalledModuleBase(v) {
35
35
  if (typeof v !== 'object' || v === null)
36
36
  return false;
37
37
  const r = v;
@@ -50,22 +50,41 @@ export function readInstalledModule(sku, opts = {}) {
50
50
  return null;
51
51
  try {
52
52
  const parsed = JSON.parse(readFileSync(path, 'utf8'));
53
- if (!isInstalledModule(parsed)) {
53
+ if (!isInstalledModuleBase(parsed)) {
54
54
  logger.warn(`installed.json for '${sku}' malformed — ignoring`);
55
55
  return null;
56
56
  }
57
- const manifest = join(installedModuleDir(sku, parsed.version, opts), MODULE_MANIFEST);
57
+ const filesOk = parsed.files === undefined || isModuleFileDigestArray(parsed.files);
58
+ const sigV2Ok = parsed.sig_v2 === undefined || typeof parsed.sig_v2 === 'string';
59
+ const bothPresent = parsed.files !== undefined && parsed.sig_v2 !== undefined;
60
+ const bothAbsent = parsed.files === undefined && parsed.sig_v2 === undefined;
61
+ if (!filesOk || !sigV2Ok || !(bothPresent || bothAbsent)) {
62
+ logger.warn(`installed.json for '${sku}' carried malformed v2 fast-boot fields — dropping ` +
63
+ `them and falling back to the v1 path`);
64
+ delete parsed.files;
65
+ delete parsed.sig_v2;
66
+ }
67
+ const rec = parsed;
68
+ const manifest = join(installedModuleDir(sku, rec.version, opts), MODULE_MANIFEST);
58
69
  if (!existsSync(manifest)) {
59
- logger.warn(`module '${sku}' v${parsed.version} install incomplete — ignoring pointer`);
70
+ logger.warn(`module '${sku}' v${rec.version} install incomplete — ignoring pointer`);
60
71
  return null;
61
72
  }
62
- return parsed;
73
+ return rec;
63
74
  }
64
75
  catch (err) {
65
76
  logger.warn(`installed.json for '${sku}' unreadable: ${err instanceof Error ? err.message : String(err)}`);
66
77
  return null;
67
78
  }
68
79
  }
80
+ function removeQuietly(path) {
81
+ try {
82
+ rmSync(path, { recursive: true, force: true });
83
+ }
84
+ catch (err) {
85
+ logger.warn(`could not remove superseded directory ${path}: ${err instanceof Error ? err.message : String(err)} (harmless; the current tree is unaffected)`);
86
+ }
87
+ }
69
88
  function writeFileAtomic(path, data) {
70
89
  const dir = dirname(path);
71
90
  mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -73,6 +92,9 @@ function writeFileAtomic(path, data) {
73
92
  writeFileSync(tmp, data, { mode: 0o600 });
74
93
  renameSync(tmp, path);
75
94
  }
95
+ function writeInstalledModule(rec, opts = {}) {
96
+ writeFileAtomic(installedPath(rec.sku, opts), JSON.stringify(rec, null, 2) + '\n');
97
+ }
76
98
  export function installModule(rec, blob, opts = {}) {
77
99
  const finalDir = installedModuleDir(rec.sku, rec.version, opts);
78
100
  const skuDir = dirname(finalDir);
@@ -87,19 +109,37 @@ export function installModule(rec, blob, opts = {}) {
87
109
  throw err;
88
110
  }
89
111
  if (opts.force && existsSync(finalDir)) {
90
- rmSync(finalDir, { recursive: true, force: true });
91
- }
92
- try {
93
- renameSync(tmpDir, finalDir);
112
+ const oldDir = join(skuDir, `${TMP_PREFIX}old-${rec.version}-${process.pid}`);
113
+ rmSync(oldDir, { recursive: true, force: true });
114
+ renameSync(finalDir, oldDir);
115
+ try {
116
+ renameSync(tmpDir, finalDir);
117
+ rmSync(oldDir, { recursive: true, force: true });
118
+ }
119
+ catch (err) {
120
+ rmSync(tmpDir, { recursive: true, force: true });
121
+ if (existsSync(finalDir)) {
122
+ rmSync(oldDir, { recursive: true, force: true });
123
+ }
124
+ else {
125
+ renameSync(oldDir, finalDir);
126
+ throw err;
127
+ }
128
+ }
94
129
  }
95
- catch (err) {
96
- rmSync(tmpDir, { recursive: true, force: true });
97
- if (!existsSync(finalDir))
98
- throw err;
130
+ else {
131
+ try {
132
+ renameSync(tmpDir, finalDir);
133
+ }
134
+ catch (err) {
135
+ rmSync(tmpDir, { recursive: true, force: true });
136
+ if (!existsSync(finalDir))
137
+ throw err;
138
+ }
99
139
  }
100
140
  const now = opts.now ?? Date.now;
101
141
  const installed = { ...rec, installed_at: new Date(now()).toISOString() };
102
- writeFileAtomic(installedPath(rec.sku, opts), JSON.stringify(installed, null, 2) + '\n');
142
+ writeInstalledModule(installed, opts);
103
143
  pruneOldModuleVersions(rec.sku, rec.version, opts);
104
144
  return installed;
105
145
  }
@@ -139,12 +179,72 @@ export function pruneOldModuleVersions(sku, keepVersion, opts = {}) {
139
179
  }
140
180
  }
141
181
  }
182
+ function verifyFastPath(sku, version, dir, artifactSha256Hex, files, sigV2, pubKeys) {
183
+ const digestsRoot = digestsRootSha256Hex(files);
184
+ if (!verifyModuleSignatureV2(sku, version, artifactSha256Hex, digestsRoot, sigV2, pubKeys)) {
185
+ return { ok: false, reason: 'sig_v2 did not verify against the pinned key' };
186
+ }
187
+ const expected = new Set(files.map((f) => f.path));
188
+ expected.add(MODULE_ARTIFACT);
189
+ let onDisk;
190
+ try {
191
+ onDisk = listFilesRecursive(dir);
192
+ }
193
+ catch (err) {
194
+ return {
195
+ ok: false,
196
+ reason: `tree enumeration failed: ${err instanceof Error ? err.message : String(err)}`,
197
+ };
198
+ }
199
+ const onDiskSet = new Set(onDisk);
200
+ for (const p of expected) {
201
+ if (!onDiskSet.has(p))
202
+ return { ok: false, reason: `missing file '${p}'` };
203
+ }
204
+ for (const p of onDisk) {
205
+ if (!expected.has(p))
206
+ return { ok: false, reason: `unexpected file '${p}'` };
207
+ }
208
+ for (const f of files) {
209
+ let bytes;
210
+ try {
211
+ bytes = readFileSync(join(dir, f.path));
212
+ }
213
+ catch (err) {
214
+ return {
215
+ ok: false,
216
+ reason: `could not read '${f.path}': ${err instanceof Error ? err.message : String(err)}`,
217
+ };
218
+ }
219
+ if (sha256Hex(bytes) !== f.sha256) {
220
+ return { ok: false, reason: `content mismatch on '${f.path}'` };
221
+ }
222
+ }
223
+ return { ok: true };
224
+ }
225
+ function listFilesRecursive(dir) {
226
+ const out = [];
227
+ const walk = (sub) => {
228
+ for (const entry of readdirSync(join(dir, sub), { withFileTypes: true })) {
229
+ const relPath = sub ? `${sub}/${entry.name}` : entry.name;
230
+ if (entry.isDirectory()) {
231
+ walk(relPath);
232
+ }
233
+ else {
234
+ out.push(relPath);
235
+ }
236
+ }
237
+ };
238
+ walk('');
239
+ return out;
240
+ }
142
241
  export function loadVerifiedModule(sku, opts = {}, pubKeys = MODULE_SIGNING_PUBLIC_KEYS) {
143
242
  const rec = readInstalledModule(sku, opts);
144
243
  if (!rec)
145
244
  return null;
146
245
  const version = rec.version;
147
246
  const dir = installedModuleDir(sku, version, opts);
247
+ const skuDir = dirname(dir);
148
248
  try {
149
249
  const blob = readFileSync(moduleArtifactPath(sku, version, opts));
150
250
  const digest = sha256Hex(blob);
@@ -156,12 +256,53 @@ export function loadVerifiedModule(sku, opts = {}, pubKeys = MODULE_SIGNING_PUBL
156
256
  logger.warn(`module '${sku}' v${version} signature did not verify at boot — refusing to load`);
157
257
  return null;
158
258
  }
159
- installBundle(blob, rec.content_key, dir);
259
+ if (rec.files && rec.sig_v2) {
260
+ const fast = verifyFastPath(sku, version, dir, digest, rec.files, rec.sig_v2, pubKeys);
261
+ if (fast.ok) {
262
+ return {
263
+ version,
264
+ dir,
265
+ handlersPath: moduleHandlersPath(sku, version, opts),
266
+ binDir: moduleBinDir(sku, version, opts),
267
+ regenerated: false,
268
+ };
269
+ }
270
+ logger.warn(`module '${sku}' v${version} fast-path check failed (${fast.reason}) — ` +
271
+ `regenerating from the verified artifact`);
272
+ }
273
+ const regenDir = join(skuDir, `${TMP_PREFIX}regen-${version}-${process.pid}`);
274
+ rmSync(regenDir, { recursive: true, force: true });
275
+ try {
276
+ installBundle(blob, rec.content_key, regenDir);
277
+ writeFileSync(join(regenDir, MODULE_ARTIFACT), Buffer.from(blob), { mode: 0o600 });
278
+ }
279
+ catch (err) {
280
+ rmSync(regenDir, { recursive: true, force: true });
281
+ throw err;
282
+ }
283
+ const oldDir = join(skuDir, `${TMP_PREFIX}regen-old-${version}-${process.pid}`);
284
+ rmSync(oldDir, { recursive: true, force: true });
285
+ renameSync(dir, oldDir);
286
+ try {
287
+ renameSync(regenDir, dir);
288
+ removeQuietly(oldDir);
289
+ }
290
+ catch (err) {
291
+ rmSync(regenDir, { recursive: true, force: true });
292
+ if (existsSync(dir)) {
293
+ removeQuietly(oldDir);
294
+ }
295
+ else {
296
+ renameSync(oldDir, dir);
297
+ throw err;
298
+ }
299
+ }
160
300
  return {
161
301
  version,
162
302
  dir,
163
303
  handlersPath: moduleHandlersPath(sku, version, opts),
164
304
  binDir: moduleBinDir(sku, version, opts),
305
+ regenerated: true,
165
306
  };
166
307
  }
167
308
  catch (err) {
@@ -3,8 +3,13 @@ import { getContextInfo } from '../api/extendscript/_helpers.js';
3
3
  import { jsLit, jsNum } from '../utils/jsx.js';
4
4
  import { runScript } from '../utils/run-script.js';
5
5
  import { TempDir, userOwnedTempRoot } from '../utils/temp.js';
6
+ import { decodeJpegBuffer } from './runtime.js';
6
7
  import { mapDetectionToDoc, } from './detection-client.js';
7
- export async function detectActiveDoc(connection, client, opts) {
8
+ import { Logger } from '../utils/logger.js';
9
+ const logger = new Logger('detect-active-doc');
10
+ export async function detectActiveDoc(connection, client, opts, deps = {}) {
11
+ const readFileFn = deps.readFile ?? readFile;
12
+ const decodeFn = deps.decode ?? decodeJpegBuffer;
8
13
  const maxDimension = opts.maxDimension ?? 1024;
9
14
  const dir = process.platform === 'darwin'
10
15
  ? await TempDir.createWithRoot(userOwnedTempRoot(), 'editmamei-detect-')
@@ -13,7 +18,7 @@ export async function detectActiveDoc(connection, client, opts) {
13
18
  const tempPath = dir.path('detect.jpg');
14
19
  const script = `
15
20
  ${getContextInfo}
16
- if (app.documents.length === 0) { throw new Error('No active document'); }
21
+ if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
17
22
  for (var __pi = app.documents.length - 1; __pi >= 0; __pi--) {
18
23
  try {
19
24
  var __pd = app.documents[__pi];
@@ -62,21 +67,47 @@ export async function detectActiveDoc(connection, client, opts) {
62
67
  const exp = (await runScript(connection, script));
63
68
  const docWidth = exp.doc_width ?? 0;
64
69
  const docHeight = exp.doc_height ?? 0;
65
- const raw = await client.detect(tempPath, {
66
- faces: opts.faces,
67
- objects: opts.objects,
68
- faceThreshold: opts.faceThreshold,
69
- objectThreshold: opts.objectThreshold,
70
- maxObjects: opts.maxObjects,
71
- });
72
70
  let exportBytes = Buffer.alloc(0);
71
+ let decoded;
73
72
  try {
74
- exportBytes = await readFile(tempPath);
73
+ exportBytes = await readFileFn(tempPath);
75
74
  }
76
75
  catch {
77
76
  }
77
+ if (exportBytes.length > 0) {
78
+ try {
79
+ decoded = decodeFn(exportBytes);
80
+ }
81
+ catch {
82
+ }
83
+ }
84
+ let doDetect = true;
85
+ if (opts.shouldDetect) {
86
+ try {
87
+ doDetect = opts.shouldDetect({ decoded, context: exp.context, docWidth, docHeight });
88
+ }
89
+ catch (err) {
90
+ const msg = err instanceof Error ? err.message : String(err);
91
+ logger.debug(`shouldDetect threw (${msg}) — degrading to detect normally`);
92
+ doDetect = true;
93
+ }
94
+ }
95
+ const raw = doDetect
96
+ ? await client.detect(tempPath, {
97
+ faces: opts.faces,
98
+ objects: opts.objects,
99
+ faceThreshold: opts.faceThreshold,
100
+ objectThreshold: opts.objectThreshold,
101
+ maxObjects: opts.maxObjects,
102
+ }, decoded)
103
+ : {
104
+ image: decoded
105
+ ? { width: decoded.width, height: decoded.height }
106
+ : { width: 0, height: 0 },
107
+ backends: {},
108
+ };
78
109
  const result = mapDetectionToDoc(raw, docWidth, docHeight);
79
- return { docWidth, docHeight, result, raw, context: exp.context, exportBytes };
110
+ return { docWidth, docHeight, result, raw, context: exp.context, exportBytes, decoded };
80
111
  }
81
112
  finally {
82
113
  await dir.cleanup();
@@ -1,13 +1,13 @@
1
1
  import { detectFaces } from './face-detector.js';
2
2
  import { detectObjects } from './object-detector.js';
3
3
  export class OnnxDetectionClient {
4
- async detect(imagePath, opts) {
4
+ async detect(imagePath, opts, decoded) {
5
5
  const wantFaces = opts.faces ?? false;
6
6
  const wantObjects = opts.objects ?? false;
7
7
  const [faceRes, objRes] = await Promise.all([
8
- wantFaces ? detectFaces(imagePath, opts.faceThreshold) : Promise.resolve(null),
8
+ wantFaces ? detectFaces(imagePath, opts.faceThreshold, decoded) : Promise.resolve(null),
9
9
  wantObjects
10
- ? detectObjects(imagePath, { threshold: opts.objectThreshold, maxObjects: opts.maxObjects })
10
+ ? detectObjects(imagePath, { threshold: opts.objectThreshold, maxObjects: opts.maxObjects }, decoded)
11
11
  : Promise.resolve(null),
12
12
  ]);
13
13
  const dims = faceRes ?? objRes;
@@ -5,8 +5,8 @@ const IN_W = 320;
5
5
  const IN_H = 240;
6
6
  const DEFAULT_THRESHOLD = 0.7;
7
7
  const IOU_THRESHOLD = 0.4;
8
- export async function detectFaces(imagePath, threshold = DEFAULT_THRESHOLD) {
9
- const img = decodeJpeg(imagePath);
8
+ export async function detectFaces(imagePath, threshold = DEFAULT_THRESHOLD, decoded) {
9
+ const img = decoded ?? decodeJpeg(imagePath);
10
10
  const session = await loadModel(resolveModelPath(MODEL_FILE));
11
11
  const input = resizeToCHW(img, IN_W, IN_H, (v) => (v - 127) / 128);
12
12
  const feeds = { input: new ort.Tensor('float32', input, [1, 3, IN_H, IN_W]) };
@@ -7,6 +7,20 @@ export function iou(a, b) {
7
7
  const ua = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter;
8
8
  return inter / (ua + 1e-9);
9
9
  }
10
+ const cx = (b) => (b[0] + b[2]) / 2;
11
+ const cy = (b) => (b[1] + b[3]) / 2;
12
+ const boxArea = (b) => (b[2] - b[0]) * (b[3] - b[1]);
13
+ export function orderBySpatialPick(items, pick) {
14
+ const cmp = {
15
+ leftmost: (a, b) => cx(a.bbox) - cx(b.bbox),
16
+ rightmost: (a, b) => cx(b.bbox) - cx(a.bbox),
17
+ topmost: (a, b) => cy(a.bbox) - cy(b.bbox),
18
+ bottommost: (a, b) => cy(b.bbox) - cy(a.bbox),
19
+ largest: (a, b) => boxArea(b.bbox) - boxArea(a.bbox),
20
+ smallest: (a, b) => boxArea(a.bbox) - boxArea(b.bbox),
21
+ };
22
+ return [...items].sort(cmp[pick]);
23
+ }
10
24
  export function greedyNms(items, iouThreshold, sameGroup) {
11
25
  const keep = [];
12
26
  for (const item of items) {
@@ -14,11 +14,11 @@ export class OnnxLandmarkDetectionClient {
14
14
  this.detectLandmarks = deps.detectLandmarks ?? realDetectLandmarks;
15
15
  this.decodeJpeg = deps.decodeJpeg ?? realDecodeJpeg;
16
16
  }
17
- async detect(imagePath, opts) {
18
- const result = await this.base.detect(imagePath, opts);
17
+ async detect(imagePath, opts, decoded) {
18
+ const result = await this.base.detect(imagePath, opts, decoded);
19
19
  if (!opts.faces || !result.faces || result.faces.length === 0)
20
20
  return result;
21
- const img = this.decodeJpeg(imagePath);
21
+ const img = decoded ?? this.decodeJpeg(imagePath);
22
22
  const minScore = this.opts.minScore ?? 0.5;
23
23
  const lmOpts = { margin: this.opts.margin };
24
24
  const correctEyes = this.opts.correctEyes ?? true;
@@ -1,3 +1,4 @@
1
+ import { orderBySpatialPick } from './geometry.js';
1
2
  export function meshFaces(faces) {
2
3
  const out = [];
3
4
  for (const f of faces) {
@@ -21,11 +22,9 @@ export function pickFace(faces, which) {
21
22
  case 'best':
22
23
  return [...faces].sort((a, b) => b.score - a.score || area(b) - area(a))[0];
23
24
  case 'leftmost':
24
- return byLeft[0];
25
25
  case 'rightmost':
26
- return byLeft[byLeft.length - 1];
27
26
  case 'largest':
28
- return [...faces].sort((a, b) => area(b) - area(a))[0];
27
+ return orderBySpatialPick(faces, which)[0];
29
28
  default: {
30
29
  const idx = Number.parseInt(which, 10);
31
30
  if (String(idx) === which.trim() && idx >= 0 && idx < byLeft.length)
@@ -90,10 +90,10 @@ export const COCO_LABELS = [
90
90
  'toothbrush',
91
91
  ];
92
92
  const sigmoid = (x) => 1 / (1 + Math.exp(-x));
93
- export async function detectObjects(imagePath, opts = {}) {
93
+ export async function detectObjects(imagePath, opts = {}, decoded) {
94
94
  const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
95
95
  const maxObjects = opts.maxObjects ?? DEFAULT_MAX;
96
- const img = decodeJpeg(imagePath);
96
+ const img = decoded ?? decodeJpeg(imagePath);
97
97
  const session = await loadModel(resolveModelPath(MODEL_FILE));
98
98
  const input = resizeToCHW(img, IN_SIZE, IN_SIZE, (v) => v / 255);
99
99
  const feeds = { pixel_values: new ort.Tensor('float32', input, [1, 3, IN_SIZE, IN_SIZE]) };
@@ -57,14 +57,25 @@ export function resolveModelPath(filename) {
57
57
  }
58
58
  return join(hostModelsDir(), filename);
59
59
  }
60
+ function decodeJpegBytes(bytes) {
61
+ const img = decode(bytes, { useTArray: true, formatAsRGBA: true });
62
+ return { width: img.width, height: img.height, data: img.data };
63
+ }
64
+ export function decodeJpegBuffer(bytes) {
65
+ try {
66
+ return decodeJpegBytes(bytes);
67
+ }
68
+ catch (err) {
69
+ const msg = err instanceof Error ? err.message : String(err);
70
+ throw new Error(`failed to decode JPEG buffer: ${msg}`);
71
+ }
72
+ }
60
73
  export function decodeJpeg(path) {
61
- let img;
62
74
  try {
63
- img = decode(readFileSync(path), { useTArray: true, formatAsRGBA: true });
75
+ return decodeJpegBytes(readFileSync(path));
64
76
  }
65
77
  catch (err) {
66
78
  const msg = err instanceof Error ? err.message : String(err);
67
79
  throw new Error(`failed to decode JPEG at ${path}: ${msg}`);
68
80
  }
69
- return { width: img.width, height: img.height, data: img.data };
70
81
  }
@@ -37,40 +37,83 @@ export function thresholdMask(maskData, n) {
37
37
  mask[i] = maskData[i] > 0 ? 1 : 0;
38
38
  return mask;
39
39
  }
40
+ const HASH_SAMPLE_BYTES = 4096;
41
+ const HASH_STRIDE_SAMPLES = 1024;
42
+ function fnv1a(data, start, end, seed) {
43
+ let h = seed;
44
+ for (let i = start; i < end; i++) {
45
+ h = Math.imul(h ^ data[i], 0x01000193) >>> 0;
46
+ }
47
+ return h;
48
+ }
49
+ function computeImageKey(img) {
50
+ const { width, height, data } = img;
51
+ const n = data.length;
52
+ let h = fnv1a(data, 0, Math.min(HASH_SAMPLE_BYTES, n), 0x811c9dc5);
53
+ h = fnv1a(data, Math.max(0, n - HASH_SAMPLE_BYTES), n, h);
54
+ const stride = Math.max(1, Math.floor(n / HASH_STRIDE_SAMPLES));
55
+ for (let i = 0; i < n; i += stride) {
56
+ h = Math.imul(h ^ data[i], 0x01000193) >>> 0;
57
+ }
58
+ return { width, height, byteLength: n, hash: h };
59
+ }
60
+ function sameImageKey(a, b) {
61
+ return (a.width === b.width &&
62
+ a.height === b.height &&
63
+ a.byteLength === b.byteLength &&
64
+ a.hash === b.hash);
65
+ }
66
+ async function realRunEncoder(img) {
67
+ const session = await loadModel(resolveModelPath(MODEL_ENCODER));
68
+ const input = new ort.Tensor('float32', toEncoderInput(img), [img.height, img.width, 3]);
69
+ const out = await session.run({ input_image: input });
70
+ return out.image_embeddings;
71
+ }
72
+ async function realRunDecoder(embedding, coords, labels, imgW, imgH) {
73
+ const session = await loadModel(resolveModelPath(MODEL_DECODER));
74
+ const nPts = labels.length;
75
+ const out = await session.run({
76
+ image_embeddings: embedding,
77
+ point_coords: new ort.Tensor('float32', coords, [1, nPts, 2]),
78
+ point_labels: new ort.Tensor('float32', labels, [1, nPts]),
79
+ mask_input: new ort.Tensor('float32', new Float32Array(256 * 256), [1, 1, 256, 256]),
80
+ has_mask_input: new ort.Tensor('float32', Float32Array.from([0]), [1]),
81
+ orig_im_size: new ort.Tensor('float32', Float32Array.from([imgH, imgW]), [2]),
82
+ });
83
+ const masks = out.masks;
84
+ const [, , mh, mw] = masks.dims;
85
+ return {
86
+ mask: thresholdMask(masks.data, mh * mw),
87
+ width: mw,
88
+ height: mh,
89
+ iou: out.iou_predictions.data[0] ?? 0,
90
+ };
91
+ }
40
92
  export class OnnxSamSegmenter {
41
- async encodeImage(img) {
42
- const session = await loadModel(resolveModelPath(MODEL_ENCODER));
43
- const input = new ort.Tensor('float32', toEncoderInput(img), [img.height, img.width, 3]);
44
- const out = await session.run({ input_image: input });
45
- return out.image_embeddings;
93
+ runEncoderImpl;
94
+ runDecoderImpl;
95
+ cachedKey = null;
96
+ cachedEmbedding = null;
97
+ constructor(deps = {}) {
98
+ this.runEncoderImpl = deps.runEncoder ?? realRunEncoder;
99
+ this.runDecoderImpl = deps.runDecoder ?? realRunDecoder;
46
100
  }
47
- async runDecoder(embedding, coords, labels, imgW, imgH) {
48
- const session = await loadModel(resolveModelPath(MODEL_DECODER));
49
- const nPts = labels.length;
50
- const out = await session.run({
51
- image_embeddings: embedding,
52
- point_coords: new ort.Tensor('float32', coords, [1, nPts, 2]),
53
- point_labels: new ort.Tensor('float32', labels, [1, nPts]),
54
- mask_input: new ort.Tensor('float32', new Float32Array(256 * 256), [1, 1, 256, 256]),
55
- has_mask_input: new ort.Tensor('float32', Float32Array.from([0]), [1]),
56
- orig_im_size: new ort.Tensor('float32', Float32Array.from([imgH, imgW]), [2]),
57
- });
58
- const masks = out.masks;
59
- const [, , mh, mw] = masks.dims;
60
- return {
61
- mask: thresholdMask(masks.data, mh * mw),
62
- width: mw,
63
- height: mh,
64
- iou: out.iou_predictions.data[0] ?? 0,
65
- };
101
+ async encodeImage(img) {
102
+ const key = computeImageKey(img);
103
+ if (this.cachedKey && sameImageKey(this.cachedKey, key))
104
+ return this.cachedEmbedding;
105
+ const embedding = await this.runEncoderImpl(img);
106
+ this.cachedKey = key;
107
+ this.cachedEmbedding = embedding;
108
+ return embedding;
66
109
  }
67
110
  async segmentBox(embedding, box, imgW, imgH) {
68
111
  const { coords, labels } = boxToPromptCoords(box, imgW, imgH);
69
- return this.runDecoder(embedding, coords, labels, imgW, imgH);
112
+ return this.runDecoderImpl(embedding, coords, labels, imgW, imgH);
70
113
  }
71
114
  async segmentPointsAt(embedding, fg, bg, imgW, imgH) {
72
115
  const { coords, labels } = pointsToPromptCoords(fg, bg, imgW, imgH);
73
- return this.runDecoder(embedding, coords, labels, imgW, imgH);
116
+ return this.runDecoderImpl(embedding, coords, labels, imgW, imgH);
74
117
  }
75
118
  async segment(img, box) {
76
119
  const embedding = await this.encodeImage(img);
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  import { CompositeSnippetClient } from '../api/snippet-client.js';
2
3
  import { Logger } from '../utils/logger.js';
3
4
  import { runScript } from '../utils/run-script.js';
@@ -10,7 +11,7 @@ export class Kernel {
10
11
  detection;
11
12
  sessionId;
12
13
  logger;
13
- invokeDepth = 0;
14
+ invokeDepth = new AsyncLocalStorage();
14
15
  static MAX_INVOKE_DEPTH = 8;
15
16
  constructor(deps) {
16
17
  this.registry = deps.registry;
@@ -43,16 +44,11 @@ export class Kernel {
43
44
  };
44
45
  }
45
46
  async invokeTool(name, args) {
46
- if (this.invokeDepth >= Kernel.MAX_INVOKE_DEPTH) {
47
+ const depth = this.invokeDepth.getStore() ?? 0;
48
+ if (depth >= Kernel.MAX_INVOKE_DEPTH) {
47
49
  throw new Error(`invokeTool recursion limit (${Kernel.MAX_INVOKE_DEPTH}) exceeded invoking '${name}'`);
48
50
  }
49
- this.invokeDepth++;
50
- try {
51
- return await this.registry.execute(name, args);
52
- }
53
- finally {
54
- this.invokeDepth--;
55
- }
51
+ return this.invokeDepth.run(depth + 1, () => this.registry.execute(name, args));
56
52
  }
57
53
  abiTooNew(manifest) {
58
54
  if (manifest.abi > KERNEL_ABI) {