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.
- package/NOTICES.md +6 -30
- package/README.md +47 -13
- package/dist/api/extendscript/_helpers.js +1 -101
- package/dist/api/photoshop-api.js +23 -16
- package/dist/api/snippet-client.js +38 -2
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/core/server.js +60 -184
- package/dist/core/session.js +9 -10
- package/dist/core/tool-registry.js +4 -4
- package/dist/delivery/client.js +50 -1
- package/dist/delivery/provision.js +18 -1
- package/dist/delivery/signing.js +38 -1
- package/dist/delivery/store.js +157 -16
- package/dist/detection/detect-active-doc.js +42 -11
- package/dist/detection/detection-client.js +3 -3
- package/dist/detection/face-detector.js +2 -2
- package/dist/detection/geometry.js +14 -0
- package/dist/detection/landmark-detection-client.js +3 -3
- package/dist/detection/mesh-face.js +2 -3
- package/dist/detection/object-detector.js +2 -2
- package/dist/detection/runtime.js +14 -3
- package/dist/detection/sam-segmenter.js +69 -26
- package/dist/kernel/kernel.js +5 -9
- package/dist/kernel/module-lifecycle.js +187 -0
- package/dist/license/entitlement.js +13 -3
- package/dist/license/store.js +8 -1
- package/dist/modules/ce/index.js +1 -1
- package/dist/perception/facets.js +19 -1
- package/dist/perception/grounding-anchors.js +2 -12
- package/dist/perception/grounding-locate.js +1 -9
- package/dist/perception/grounding-review-crop.js +4 -29
- package/dist/perception/overlay.js +62 -0
- package/dist/perception/pixel-identity.js +41 -0
- package/dist/perception/region-precompute.js +96 -20
- package/dist/perception/scene-model.js +93 -50
- package/dist/perception/select-recipes.js +109 -68
- package/dist/perception/sky-ground-flood.js +0 -5
- package/dist/perception/sky-mask-transfer.js +73 -14
- package/dist/platform/connection.js +60 -43
- package/dist/platform/host-platform.js +16 -0
- package/dist/platform/macos-detector.js +41 -70
- package/dist/platform/macos-runner.js +94 -0
- package/dist/platform/script-queue.js +68 -0
- package/dist/platform/script-result.js +39 -0
- package/dist/platform/windows-detector.js +49 -66
- package/dist/platform/windows-runner.js +84 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +80 -132
- package/dist/tools/brush-tools.js +4 -15
- package/dist/tools/channel-compose-tools.js +4 -23
- package/dist/tools/detection-tools.js +59 -47
- package/dist/tools/document-tools.js +92 -127
- package/dist/tools/filter-tools.js +490 -731
- package/dist/tools/group-tools.js +75 -156
- package/dist/tools/guide-tools.js +52 -83
- package/dist/tools/history-tools.js +29 -63
- package/dist/tools/image-placement-tools.js +18 -37
- package/dist/tools/image-tools.js +23 -51
- package/dist/tools/layer-ordering-tools.js +2 -9
- package/dist/tools/layer-properties-tools.js +207 -395
- package/dist/tools/layer-tools.js +224 -190
- package/dist/tools/layer-transform-tools.js +41 -106
- package/dist/tools/metadata-tools.js +2 -9
- package/dist/tools/path-tools.js +4 -15
- package/dist/tools/portrait-tools.js +2 -5
- package/dist/tools/preview-tools.js +59 -72
- package/dist/tools/retouch-tools.js +33 -71
- package/dist/tools/scene-tools.js +123 -75
- package/dist/tools/selection-tools.js +386 -566
- package/dist/tools/shape-tools.js +2 -9
- package/dist/tools/text-tools.js +24 -57
- package/dist/tools/transform-canvas-tools.js +21 -54
- package/dist/tools/vector-mask-tools.js +2 -9
- package/dist/update/check.js +1 -1
- package/dist/utils/jpeg-quality.js +1 -0
- package/dist/utils/logger.js +55 -45
- package/dist/utils/operation-timeouts.js +1 -0
- package/dist/utils/run-script.js +7 -2
- package/dist/utils/session-log.js +134 -12
- package/dist/utils/template-storage.js +5 -6
- package/dist/utils/tool-helpers.js +43 -0
- package/dist/version.js +1 -1
- package/package.json +10 -3
- package/LICENSE +0 -81
- package/dist/platform/detector.js +0 -32
- package/dist/platform/macos-executor.js +0 -151
- package/dist/platform/windows-executor.js +0 -160
- /package/dist/platform/{script-executor.js → ports.js} +0 -0
package/dist/delivery/store.js
CHANGED
|
@@ -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
|
|
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 (!
|
|
53
|
+
if (!isInstalledModuleBase(parsed)) {
|
|
54
54
|
logger.warn(`installed.json for '${sku}' malformed — ignoring`);
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
|
-
const
|
|
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${
|
|
70
|
+
logger.warn(`module '${sku}' v${rec.version} install incomplete — ignoring pointer`);
|
|
60
71
|
return null;
|
|
61
72
|
}
|
|
62
|
-
return
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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.
|
|
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);
|
package/dist/kernel/kernel.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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) {
|