editmamei 0.22.4 → 0.24.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/README.md +8 -6
- package/dist/api/extendscript/_helpers.js +97 -13
- package/dist/api/photoshop-api.js +4 -3
- 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 +56 -180
- 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 +149 -16
- package/dist/detection/detect-active-doc.js +41 -10
- 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 +181 -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 +85 -17
- package/dist/perception/scene-model.js +93 -50
- package/dist/perception/select-recipes.js +107 -66
- package/dist/perception/sky-ground-flood.js +0 -5
- package/dist/perception/sky-mask-transfer.js +72 -13
- package/dist/platform/connection.js +42 -11
- package/dist/platform/executor-queue.js +68 -0
- package/dist/platform/macos-detector.js +2 -1
- package/dist/platform/macos-executor.js +18 -67
- package/dist/platform/run-child.js +6 -3
- package/dist/platform/windows-detector.js +14 -7
- package/dist/platform/windows-executor.js +9 -64
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +78 -130
- package/dist/tools/brush-tools.js +3 -14
- package/dist/tools/channel-compose-tools.js +4 -23
- package/dist/tools/detection-tools.js +59 -47
- package/dist/tools/document-tools.js +121 -131
- package/dist/tools/filter-tools.js +489 -730
- package/dist/tools/group-tools.js +90 -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 +245 -384
- package/dist/tools/layer-tools.js +115 -181
- package/dist/tools/layer-transform-tools.js +41 -106
- package/dist/tools/metadata-tools.js +2 -9
- package/dist/tools/path-tools.js +3 -14
- package/dist/tools/portrait-tools.js +2 -5
- package/dist/tools/preview-tools.js +53 -65
- package/dist/tools/retouch-tools.js +32 -70
- package/dist/tools/scene-tools.js +94 -73
- package/dist/tools/selection-tools.js +306 -563
- package/dist/tools/shape-tools.js +18 -10
- 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/utils/jpeg-quality.js +1 -0
- package/dist/utils/logger.js +3 -0
- package/dist/utils/operation-timeouts.js +7 -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 +37 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/delivery/client.js
CHANGED
|
@@ -11,6 +11,7 @@ export class DeliveryError extends Error {
|
|
|
11
11
|
this.name = 'DeliveryError';
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
14
15
|
const USER_AGENT = 'editmamei-delivery-client/1';
|
|
15
16
|
const HEADER_KEY = 'x-editmamei-license-key';
|
|
16
17
|
const defaultFetch = (url, init) => fetch(url, init);
|
|
@@ -23,6 +24,7 @@ export class DeliveryClient {
|
|
|
23
24
|
retry;
|
|
24
25
|
sleep;
|
|
25
26
|
minIntervalMs;
|
|
27
|
+
maxArtifactBytes;
|
|
26
28
|
lastRequestAt = 0;
|
|
27
29
|
constructor(opts = {}) {
|
|
28
30
|
this.cfg = opts.config ?? resolveDeliveryConfig();
|
|
@@ -30,6 +32,7 @@ export class DeliveryClient {
|
|
|
30
32
|
this.retry = { ...DEFAULT_RETRY, ...opts.retry };
|
|
31
33
|
this.sleep = opts.sleep ?? defaultSleep;
|
|
32
34
|
this.minIntervalMs = opts.minRequestIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
35
|
+
this.maxArtifactBytes = opts.maxArtifactBytes ?? MAX_ARTIFACT_BYTES;
|
|
33
36
|
if (!this.cfg.baseUrl) {
|
|
34
37
|
throw new DeliveryError('Delivery endpoint is not configured. Set EDITMAMEI_DELIVERY_URL ' +
|
|
35
38
|
'(e.g. http://localhost:8787 for a local wrangler dev Worker).', 0, 'not_configured');
|
|
@@ -52,9 +55,55 @@ export class DeliveryClient {
|
|
|
52
55
|
const res = await this.get(`/v1/modules/${sku}/${version}`, key);
|
|
53
56
|
if (!res.ok)
|
|
54
57
|
throw this.errorFor(res.status, await safeText(res), this.retryAfterMsOf(res));
|
|
55
|
-
|
|
58
|
+
const declared = Number(res.headers?.get('content-length'));
|
|
59
|
+
if (Number.isFinite(declared) && declared > this.maxArtifactBytes) {
|
|
60
|
+
throw this.oversizeError(res.status, declared);
|
|
61
|
+
}
|
|
62
|
+
const body = res.body;
|
|
63
|
+
if (!body) {
|
|
64
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
65
|
+
if (buf.length > this.maxArtifactBytes)
|
|
66
|
+
throw this.oversizeError(res.status, buf.length);
|
|
67
|
+
return buf;
|
|
68
|
+
}
|
|
69
|
+
return await this.readCapped(body, res.status);
|
|
56
70
|
});
|
|
57
71
|
}
|
|
72
|
+
async readCapped(body, status) {
|
|
73
|
+
const reader = body.getReader();
|
|
74
|
+
const chunks = [];
|
|
75
|
+
let total = 0;
|
|
76
|
+
try {
|
|
77
|
+
for (;;) {
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
if (done)
|
|
80
|
+
break;
|
|
81
|
+
total += value.byteLength;
|
|
82
|
+
if (total > this.maxArtifactBytes) {
|
|
83
|
+
try {
|
|
84
|
+
await reader.cancel('artifact exceeds byte cap');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
}
|
|
88
|
+
throw this.oversizeError(status, total);
|
|
89
|
+
}
|
|
90
|
+
chunks.push(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
reader.releaseLock();
|
|
95
|
+
}
|
|
96
|
+
const out = new Uint8Array(total);
|
|
97
|
+
let offset = 0;
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
out.set(chunk, offset);
|
|
100
|
+
offset += chunk.byteLength;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
oversizeError(status, sizeBytes) {
|
|
105
|
+
return new DeliveryError(`Artifact exceeds the ${this.maxArtifactBytes}-byte cap (${sizeBytes} bytes) — refusing to buffer further.`, status, 'oversize');
|
|
106
|
+
}
|
|
58
107
|
async withRetry(fn) {
|
|
59
108
|
let lastErr;
|
|
60
109
|
for (let attempt = 1; attempt <= this.retry.attempts; attempt++) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { DeliveryClient, DeliveryError } from './client.js';
|
|
2
2
|
import { sha256Hex } from './crypto.js';
|
|
3
|
-
import { verifyModuleSignature } from './signing.js';
|
|
3
|
+
import { verifyModuleSignature, verifyModuleSignatureV2, digestsRootSha256Hex, isModuleFileDigestArray, } from './signing.js';
|
|
4
4
|
import { installModule, readInstalledModule } from './store.js';
|
|
5
|
+
import { Logger } from '../utils/logger.js';
|
|
6
|
+
const logger = new Logger('Modules');
|
|
5
7
|
const SKU_RE = /^[a-z0-9-]{2,32}$/;
|
|
6
8
|
const VERSION_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
7
9
|
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
@@ -99,6 +101,20 @@ export async function provisionModules(key, opts = {}) {
|
|
|
99
101
|
});
|
|
100
102
|
continue;
|
|
101
103
|
}
|
|
104
|
+
let filesForRec;
|
|
105
|
+
let sigV2ForRec;
|
|
106
|
+
if (vEntry.files !== undefined || vEntry.sig_v2 !== undefined) {
|
|
107
|
+
if (isModuleFileDigestArray(vEntry.files) &&
|
|
108
|
+
typeof vEntry.sig_v2 === 'string' &&
|
|
109
|
+
verifyModuleSignatureV2(sku, latest, digest, digestsRootSha256Hex(vEntry.files), vEntry.sig_v2, opts.signingKeys)) {
|
|
110
|
+
filesForRec = vEntry.files;
|
|
111
|
+
sigV2ForRec = vEntry.sig_v2;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
logger.warn(`${sku} v${latest} manifest carried v2 fast-boot fields that failed verification — ` +
|
|
115
|
+
`installing via v1 trust only (boot will keep doing the full regen for this module)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
102
118
|
const contentKey = await client.fetchKey(key, sku);
|
|
103
119
|
installModule({
|
|
104
120
|
sku,
|
|
@@ -108,6 +124,7 @@ export async function provisionModules(key, opts = {}) {
|
|
|
108
124
|
alg: contentKey.alg,
|
|
109
125
|
content_key: contentKey.key,
|
|
110
126
|
sig: sig,
|
|
127
|
+
...(filesForRec && sigV2ForRec ? { files: filesForRec, sig_v2: sigV2ForRec } : {}),
|
|
111
128
|
}, blob, opts);
|
|
112
129
|
result.installed.push({ sku, version: latest });
|
|
113
130
|
}
|
package/dist/delivery/signing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPublicKey, verify } from 'node:crypto';
|
|
1
|
+
import { createHash, createPublicKey, verify } from 'node:crypto';
|
|
2
2
|
export const DEV_MODULE_SIGNING_PUBLIC_KEY = 'MCowBQYDK2VwAyEA0DfcGMets2SlBofK4iFzLddKZB6mJF/E3beKFpLRN2o=';
|
|
3
3
|
const PROD_MODULE_SIGNING_PUBLIC_KEY = 'MCowBQYDK2VwAyEAweoGOWvtf+aukiAD0FpuECSbCdZMeFzF2XYzbFWwfwo=';
|
|
4
4
|
export const MODULE_SIGNING_PUBLIC_KEYS = [PROD_MODULE_SIGNING_PUBLIC_KEY];
|
|
@@ -33,3 +33,40 @@ export function verifyModuleSignature(sku, version, sha256Hex, sigB64, pubKeys =
|
|
|
33
33
|
}
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
+
export function isModuleFileDigestArray(v) {
|
|
37
|
+
return (Array.isArray(v) &&
|
|
38
|
+
v.every((e) => typeof e === 'object' &&
|
|
39
|
+
e !== null &&
|
|
40
|
+
typeof e.path === 'string' &&
|
|
41
|
+
typeof e.sha256 === 'string'));
|
|
42
|
+
}
|
|
43
|
+
export function canonicalDigestListString(files) {
|
|
44
|
+
const sorted = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
45
|
+
return JSON.stringify(sorted.map((f) => ({ path: f.path, sha256: f.sha256 })));
|
|
46
|
+
}
|
|
47
|
+
export function digestsRootSha256Hex(files) {
|
|
48
|
+
return createHash('sha256').update(canonicalDigestListString(files), 'utf8').digest('hex');
|
|
49
|
+
}
|
|
50
|
+
export function moduleSigV2Message(sku, version, artifactSha256Hex, digestsRootHex) {
|
|
51
|
+
return Buffer.from(`editmamei-module-sig-v2\n${sku}\n${version}\n${artifactSha256Hex}\n${digestsRootHex}`, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
export function verifyModuleSignatureV2(sku, version, artifactSha256Hex, digestsRootHex, sigB64, pubKeys = MODULE_SIGNING_PUBLIC_KEYS) {
|
|
54
|
+
if (!sigB64)
|
|
55
|
+
return false;
|
|
56
|
+
const sig = Buffer.from(sigB64, 'base64');
|
|
57
|
+
if (sig.length === 0)
|
|
58
|
+
return false;
|
|
59
|
+
const msg = moduleSigV2Message(sku, version, artifactSha256Hex, digestsRootHex);
|
|
60
|
+
for (const b64 of pubKeys) {
|
|
61
|
+
const key = importSpki(b64);
|
|
62
|
+
if (!key)
|
|
63
|
+
continue;
|
|
64
|
+
try {
|
|
65
|
+
if (verify(null, msg, key, sig))
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
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,16 +50,27 @@ 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)}`);
|
|
@@ -73,6 +84,9 @@ function writeFileAtomic(path, data) {
|
|
|
73
84
|
writeFileSync(tmp, data, { mode: 0o600 });
|
|
74
85
|
renameSync(tmp, path);
|
|
75
86
|
}
|
|
87
|
+
function writeInstalledModule(rec, opts = {}) {
|
|
88
|
+
writeFileAtomic(installedPath(rec.sku, opts), JSON.stringify(rec, null, 2) + '\n');
|
|
89
|
+
}
|
|
76
90
|
export function installModule(rec, blob, opts = {}) {
|
|
77
91
|
const finalDir = installedModuleDir(rec.sku, rec.version, opts);
|
|
78
92
|
const skuDir = dirname(finalDir);
|
|
@@ -87,19 +101,37 @@ export function installModule(rec, blob, opts = {}) {
|
|
|
87
101
|
throw err;
|
|
88
102
|
}
|
|
89
103
|
if (opts.force && existsSync(finalDir)) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
const oldDir = join(skuDir, `${TMP_PREFIX}old-${rec.version}-${process.pid}`);
|
|
105
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
106
|
+
renameSync(finalDir, oldDir);
|
|
107
|
+
try {
|
|
108
|
+
renameSync(tmpDir, finalDir);
|
|
109
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
113
|
+
if (existsSync(finalDir)) {
|
|
114
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
renameSync(oldDir, finalDir);
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
94
121
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
else {
|
|
123
|
+
try {
|
|
124
|
+
renameSync(tmpDir, finalDir);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
128
|
+
if (!existsSync(finalDir))
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
99
131
|
}
|
|
100
132
|
const now = opts.now ?? Date.now;
|
|
101
133
|
const installed = { ...rec, installed_at: new Date(now()).toISOString() };
|
|
102
|
-
|
|
134
|
+
writeInstalledModule(installed, opts);
|
|
103
135
|
pruneOldModuleVersions(rec.sku, rec.version, opts);
|
|
104
136
|
return installed;
|
|
105
137
|
}
|
|
@@ -139,12 +171,72 @@ export function pruneOldModuleVersions(sku, keepVersion, opts = {}) {
|
|
|
139
171
|
}
|
|
140
172
|
}
|
|
141
173
|
}
|
|
174
|
+
function verifyFastPath(sku, version, dir, artifactSha256Hex, files, sigV2, pubKeys) {
|
|
175
|
+
const digestsRoot = digestsRootSha256Hex(files);
|
|
176
|
+
if (!verifyModuleSignatureV2(sku, version, artifactSha256Hex, digestsRoot, sigV2, pubKeys)) {
|
|
177
|
+
return { ok: false, reason: 'sig_v2 did not verify against the pinned key' };
|
|
178
|
+
}
|
|
179
|
+
const expected = new Set(files.map((f) => f.path));
|
|
180
|
+
expected.add(MODULE_ARTIFACT);
|
|
181
|
+
let onDisk;
|
|
182
|
+
try {
|
|
183
|
+
onDisk = listFilesRecursive(dir);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
reason: `tree enumeration failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const onDiskSet = new Set(onDisk);
|
|
192
|
+
for (const p of expected) {
|
|
193
|
+
if (!onDiskSet.has(p))
|
|
194
|
+
return { ok: false, reason: `missing file '${p}'` };
|
|
195
|
+
}
|
|
196
|
+
for (const p of onDisk) {
|
|
197
|
+
if (!expected.has(p))
|
|
198
|
+
return { ok: false, reason: `unexpected file '${p}'` };
|
|
199
|
+
}
|
|
200
|
+
for (const f of files) {
|
|
201
|
+
let bytes;
|
|
202
|
+
try {
|
|
203
|
+
bytes = readFileSync(join(dir, f.path));
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
reason: `could not read '${f.path}': ${err instanceof Error ? err.message : String(err)}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (sha256Hex(bytes) !== f.sha256) {
|
|
212
|
+
return { ok: false, reason: `content mismatch on '${f.path}'` };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { ok: true };
|
|
216
|
+
}
|
|
217
|
+
function listFilesRecursive(dir) {
|
|
218
|
+
const out = [];
|
|
219
|
+
const walk = (sub) => {
|
|
220
|
+
for (const entry of readdirSync(join(dir, sub), { withFileTypes: true })) {
|
|
221
|
+
const relPath = sub ? `${sub}/${entry.name}` : entry.name;
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
walk(relPath);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
out.push(relPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
walk('');
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
142
233
|
export function loadVerifiedModule(sku, opts = {}, pubKeys = MODULE_SIGNING_PUBLIC_KEYS) {
|
|
143
234
|
const rec = readInstalledModule(sku, opts);
|
|
144
235
|
if (!rec)
|
|
145
236
|
return null;
|
|
146
237
|
const version = rec.version;
|
|
147
238
|
const dir = installedModuleDir(sku, version, opts);
|
|
239
|
+
const skuDir = dirname(dir);
|
|
148
240
|
try {
|
|
149
241
|
const blob = readFileSync(moduleArtifactPath(sku, version, opts));
|
|
150
242
|
const digest = sha256Hex(blob);
|
|
@@ -156,12 +248,53 @@ export function loadVerifiedModule(sku, opts = {}, pubKeys = MODULE_SIGNING_PUBL
|
|
|
156
248
|
logger.warn(`module '${sku}' v${version} signature did not verify at boot — refusing to load`);
|
|
157
249
|
return null;
|
|
158
250
|
}
|
|
159
|
-
|
|
251
|
+
if (rec.files && rec.sig_v2) {
|
|
252
|
+
const fast = verifyFastPath(sku, version, dir, digest, rec.files, rec.sig_v2, pubKeys);
|
|
253
|
+
if (fast.ok) {
|
|
254
|
+
return {
|
|
255
|
+
version,
|
|
256
|
+
dir,
|
|
257
|
+
handlersPath: moduleHandlersPath(sku, version, opts),
|
|
258
|
+
binDir: moduleBinDir(sku, version, opts),
|
|
259
|
+
regenerated: false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
logger.warn(`module '${sku}' v${version} fast-path check failed (${fast.reason}) — ` +
|
|
263
|
+
`regenerating from the verified artifact`);
|
|
264
|
+
}
|
|
265
|
+
const regenDir = join(skuDir, `${TMP_PREFIX}regen-${version}-${process.pid}`);
|
|
266
|
+
rmSync(regenDir, { recursive: true, force: true });
|
|
267
|
+
try {
|
|
268
|
+
installBundle(blob, rec.content_key, regenDir);
|
|
269
|
+
writeFileSync(join(regenDir, MODULE_ARTIFACT), Buffer.from(blob), { mode: 0o600 });
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
rmSync(regenDir, { recursive: true, force: true });
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
const oldDir = join(skuDir, `${TMP_PREFIX}regen-old-${version}-${process.pid}`);
|
|
276
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
277
|
+
renameSync(dir, oldDir);
|
|
278
|
+
try {
|
|
279
|
+
renameSync(regenDir, dir);
|
|
280
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
rmSync(regenDir, { recursive: true, force: true });
|
|
284
|
+
if (existsSync(dir)) {
|
|
285
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
renameSync(oldDir, dir);
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
160
292
|
return {
|
|
161
293
|
version,
|
|
162
294
|
dir,
|
|
163
295
|
handlersPath: moduleHandlersPath(sku, version, opts),
|
|
164
296
|
binDir: moduleBinDir(sku, version, opts),
|
|
297
|
+
regenerated: true,
|
|
165
298
|
};
|
|
166
299
|
}
|
|
167
300
|
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-')
|
|
@@ -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
|
}
|