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
|
@@ -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) {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { EDITION } from '../edition.js';
|
|
5
|
+
import { resolveProBinaryPath } from '../api/snippet-client.js';
|
|
6
|
+
import { isProEntitled } from '../license/entitlement.js';
|
|
7
|
+
import { loadVerifiedModule, readInstalledModule, installedPath, PRO_SKU, } from '../delivery/store.js';
|
|
8
|
+
import { provisionModules } from '../delivery/provision.js';
|
|
9
|
+
import { readLicense } from '../license/store.js';
|
|
10
|
+
import { HOST_MIN_ABI } from './host-api.js';
|
|
11
|
+
export function classifyModuleOutcome(inputs) {
|
|
12
|
+
if (inputs.proModuleLoaded && inputs.skipReason === null)
|
|
13
|
+
return 'loaded';
|
|
14
|
+
if (inputs.skipReason === 'corrupt')
|
|
15
|
+
return 'skipped_corrupt';
|
|
16
|
+
if (inputs.skipReason === 'incompatible')
|
|
17
|
+
return 'skipped_incompatible';
|
|
18
|
+
return inputs.entitled ? 'absent' : 'lapsed';
|
|
19
|
+
}
|
|
20
|
+
export class ModuleLifecycle {
|
|
21
|
+
deps;
|
|
22
|
+
kernel;
|
|
23
|
+
_proModule = null;
|
|
24
|
+
_moduleSkipReason = null;
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
}
|
|
28
|
+
get proModule() {
|
|
29
|
+
return this._proModule;
|
|
30
|
+
}
|
|
31
|
+
get moduleSkipReason() {
|
|
32
|
+
return this._moduleSkipReason;
|
|
33
|
+
}
|
|
34
|
+
setKernel(kernel) {
|
|
35
|
+
this.kernel = kernel;
|
|
36
|
+
}
|
|
37
|
+
resolveProModule() {
|
|
38
|
+
if (isProEntitled()) {
|
|
39
|
+
const verified = loadVerifiedModule(PRO_SKU);
|
|
40
|
+
if (verified) {
|
|
41
|
+
const abi = readInstalledModule(PRO_SKU)?.abi ?? null;
|
|
42
|
+
this._proModule = {
|
|
43
|
+
importer: () => import(pathToFileURL(verified.handlersPath).href),
|
|
44
|
+
binDir: verified.binDir,
|
|
45
|
+
abi,
|
|
46
|
+
};
|
|
47
|
+
return this._proModule;
|
|
48
|
+
}
|
|
49
|
+
if (EDITION !== 'dev' && existsSync(installedPath(PRO_SKU))) {
|
|
50
|
+
this._moduleSkipReason = 'corrupt';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (EDITION === 'dev') {
|
|
54
|
+
this._proModule = {
|
|
55
|
+
importer: () => import('../modules/pro/index.js'),
|
|
56
|
+
binDir: dirname(resolveProBinaryPath()),
|
|
57
|
+
abi: null,
|
|
58
|
+
};
|
|
59
|
+
return this._proModule;
|
|
60
|
+
}
|
|
61
|
+
this._proModule = null;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
async loadModules() {
|
|
65
|
+
if (!this._proModule)
|
|
66
|
+
return;
|
|
67
|
+
if (this._proModule.abi !== null && this._proModule.abi < HOST_MIN_ABI) {
|
|
68
|
+
this.deps.logger.warn(`Pro module (abi ${this._proModule.abi}) is older than this host requires ` +
|
|
69
|
+
`(min abi ${HOST_MIN_ABI}) — booting Community; will re-provision in the background.`);
|
|
70
|
+
this._moduleSkipReason = 'incompatible';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const snapshot = this.deps.toolRegistry.snapshot();
|
|
74
|
+
try {
|
|
75
|
+
await this.kernel.loadDownloaded(this._proModule.importer);
|
|
76
|
+
this.deps.assertToolsClassified();
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const changed = this.deps.toolRegistry.count() - snapshot.size;
|
|
80
|
+
this.deps.toolRegistry.restore(snapshot);
|
|
81
|
+
this.deps.logger.warn(`Pro module could not be loaded on this host — booting Community and rolling back ` +
|
|
82
|
+
`${changed} module tool change(s); will re-provision in the background: ` +
|
|
83
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
84
|
+
this._moduleSkipReason = 'incompatible';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
computeModuleStatus() {
|
|
88
|
+
if (!readLicense())
|
|
89
|
+
return null;
|
|
90
|
+
const installed = readInstalledModule(PRO_SKU);
|
|
91
|
+
return {
|
|
92
|
+
module: PRO_SKU,
|
|
93
|
+
outcome: classifyModuleOutcome({
|
|
94
|
+
proModuleLoaded: this._proModule !== null,
|
|
95
|
+
skipReason: this._moduleSkipReason,
|
|
96
|
+
entitled: isProEntitled(),
|
|
97
|
+
}),
|
|
98
|
+
module_version: installed?.version ?? null,
|
|
99
|
+
abi: installed?.abi ?? null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async reprovisionIfModuleSkipped(delivery = {}) {
|
|
103
|
+
const reason = this._moduleSkipReason;
|
|
104
|
+
if (reason === null)
|
|
105
|
+
return;
|
|
106
|
+
const license = readLicense();
|
|
107
|
+
if (!license) {
|
|
108
|
+
this.deps.logger.warn('A Pro module was skipped but no cached license was found — staying Community. ' +
|
|
109
|
+
'Run `editmamei activate <key>` to restore Pro.');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const prov = await provisionModules(license.key, {
|
|
114
|
+
force: reason === 'corrupt',
|
|
115
|
+
config: delivery.config,
|
|
116
|
+
fetchImpl: delivery.fetchImpl,
|
|
117
|
+
signingKeys: delivery.signingKeys,
|
|
118
|
+
sleep: delivery.sleep,
|
|
119
|
+
});
|
|
120
|
+
if (prov.installed.length > 0) {
|
|
121
|
+
for (const m of prov.installed) {
|
|
122
|
+
this.deps.logger.info(`Pro module updated to v${m.version}, restart to load.`);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (prov.notConfigured) {
|
|
127
|
+
this.deps.logger.warn('Module delivery is not configured — staying Community.');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
for (const e of prov.errors) {
|
|
131
|
+
this.deps.logger.warn(`Could not re-provision the ${e.sku} module (staying Community): ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
if (prov.errors.length === 0) {
|
|
134
|
+
if (reason === 'incompatible') {
|
|
135
|
+
this.deps.logger.warn('The published Pro module does not yet support this host version — staying ' +
|
|
136
|
+
'Community. Update Editmamei when a compatible release ships; ' +
|
|
137
|
+
'`editmamei report` files a diagnostic.');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this.deps.logger.warn('Re-provision installed nothing for the corrupt Pro module — staying ' +
|
|
141
|
+
'Community. Try `editmamei repair`; `editmamei report` if it persists.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
this.deps.logger.warn(`Background Pro-module re-provision failed (staying Community): ` +
|
|
147
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async ensureEntitledModuleFresh(delivery = {}) {
|
|
151
|
+
if (this._moduleSkipReason !== null)
|
|
152
|
+
return;
|
|
153
|
+
if (!isProEntitled())
|
|
154
|
+
return;
|
|
155
|
+
const underTest = process.env.VITEST !== undefined || process.env.NODE_ENV === 'test';
|
|
156
|
+
if (underTest && !delivery.fetchImpl)
|
|
157
|
+
return;
|
|
158
|
+
const license = readLicense();
|
|
159
|
+
if (!license)
|
|
160
|
+
return;
|
|
161
|
+
try {
|
|
162
|
+
const prov = await provisionModules(license.key, {
|
|
163
|
+
config: delivery.config,
|
|
164
|
+
fetchImpl: delivery.fetchImpl,
|
|
165
|
+
signingKeys: delivery.signingKeys,
|
|
166
|
+
sleep: delivery.sleep,
|
|
167
|
+
});
|
|
168
|
+
for (const m of prov.installed) {
|
|
169
|
+
this.deps.logger.info(`Pro module updated to v${m.version} — restart to load.`);
|
|
170
|
+
}
|
|
171
|
+
for (const e of prov.errors) {
|
|
172
|
+
this.deps.logger.warn(`Pro-module freshness check could not provision the ${e.sku} module ` +
|
|
173
|
+
`(staying on the installed version): ${e.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
this.deps.logger.warn(`Background Pro-module freshness check failed (staying on the installed version): ` +
|
|
178
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolvePolarConfig } from './config.js';
|
|
2
2
|
import { computeDeviceHash } from './device-hash.js';
|
|
3
|
-
import { readLicense, writeLicense, clearLicense, } from './store.js';
|
|
3
|
+
import { readLicense, writeLicense, clearLicense, nextHighWaterMark, } from './store.js';
|
|
4
4
|
import { PolarLicenseClient, PolarLicenseError } from './polar-client.js';
|
|
5
5
|
import { Logger } from '../utils/logger.js';
|
|
6
6
|
const logger = new Logger('License');
|
|
@@ -22,6 +22,12 @@ export function evaluateEntitlement(rec, now) {
|
|
|
22
22
|
if (!Number.isFinite(last) || now - last > GRACE_MS) {
|
|
23
23
|
return { entitled: false, reason: 'grace-expired' };
|
|
24
24
|
}
|
|
25
|
+
if (rec.high_water_mark !== undefined) {
|
|
26
|
+
const hwm = Date.parse(rec.high_water_mark);
|
|
27
|
+
if (Number.isFinite(hwm) && now < hwm - CLOCK_SKEW_TOLERANCE_MS) {
|
|
28
|
+
return { entitled: false, reason: 'grace-expired' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
25
31
|
return { entitled: true, reason: 'granted' };
|
|
26
32
|
}
|
|
27
33
|
const defaultFetch = (url, init) => fetch(url, init);
|
|
@@ -51,6 +57,7 @@ export async function activate(key, ops = {}) {
|
|
|
51
57
|
const deviceHash = computeDeviceHash();
|
|
52
58
|
const activation = await polar.activate(trimmed, deviceHash);
|
|
53
59
|
const v = await polar.validate(trimmed);
|
|
60
|
+
const nowMs = now();
|
|
54
61
|
const rec = {
|
|
55
62
|
key: trimmed,
|
|
56
63
|
organization_id: cfg.organizationId,
|
|
@@ -59,7 +66,8 @@ export async function activate(key, ops = {}) {
|
|
|
59
66
|
activation_id: activation.id,
|
|
60
67
|
device_hash: deviceHash,
|
|
61
68
|
display_key: v.display_key,
|
|
62
|
-
last_validated_at: new Date(
|
|
69
|
+
last_validated_at: new Date(nowMs).toISOString(),
|
|
70
|
+
high_water_mark: nextHighWaterMark(existing, nowMs),
|
|
63
71
|
};
|
|
64
72
|
writeLicense(rec, ops);
|
|
65
73
|
return rec;
|
|
@@ -120,12 +128,14 @@ export async function refresh(ops = {}) {
|
|
|
120
128
|
const { polar } = makeClient(ops);
|
|
121
129
|
const now = ops.now ?? Date.now;
|
|
122
130
|
const v = await polar.validate(rec.key);
|
|
131
|
+
const nowMs = now();
|
|
123
132
|
const updated = {
|
|
124
133
|
...rec,
|
|
125
134
|
status: v.status,
|
|
126
135
|
expires_at: v.expires_at,
|
|
127
136
|
display_key: v.display_key,
|
|
128
|
-
last_validated_at: new Date(
|
|
137
|
+
last_validated_at: new Date(nowMs).toISOString(),
|
|
138
|
+
high_water_mark: nextHighWaterMark(rec, nowMs),
|
|
129
139
|
};
|
|
130
140
|
writeLicense(updated, ops);
|
|
131
141
|
return updated;
|
package/dist/license/store.js
CHANGED
|
@@ -18,7 +18,8 @@ function isLicenseRecord(v) {
|
|
|
18
18
|
typeof r.activation_id === 'string' &&
|
|
19
19
|
typeof r.device_hash === 'string' &&
|
|
20
20
|
typeof r.display_key === 'string' &&
|
|
21
|
-
typeof r.last_validated_at === 'string'
|
|
21
|
+
typeof r.last_validated_at === 'string' &&
|
|
22
|
+
(r.high_water_mark === undefined || typeof r.high_water_mark === 'string'));
|
|
22
23
|
}
|
|
23
24
|
export function readLicense(opts = {}) {
|
|
24
25
|
const path = licensePath(opts);
|
|
@@ -45,6 +46,12 @@ export function writeLicense(rec, opts = {}) {
|
|
|
45
46
|
writeFileSync(tmp, JSON.stringify(rec, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
46
47
|
renameSync(tmp, path);
|
|
47
48
|
}
|
|
49
|
+
export function nextHighWaterMark(existing, ...candidatesMs) {
|
|
50
|
+
const priorMs = existing?.high_water_mark ? Date.parse(existing.high_water_mark) : NaN;
|
|
51
|
+
const finite = [priorMs, ...candidatesMs].filter((ms) => Number.isFinite(ms));
|
|
52
|
+
const hwm = finite.length ? Math.max(...finite) : Date.now();
|
|
53
|
+
return new Date(hwm).toISOString();
|
|
54
|
+
}
|
|
48
55
|
export function clearLicense(opts = {}) {
|
|
49
56
|
const path = licensePath(opts);
|
|
50
57
|
if (existsSync(path))
|
package/dist/modules/ce/index.js
CHANGED
|
@@ -64,7 +64,7 @@ export const ceModule = {
|
|
|
64
64
|
const { connection, snippet } = host;
|
|
65
65
|
const defs = [
|
|
66
66
|
...ceFactories.flatMap((f) => f(connection, snippet)),
|
|
67
|
-
...createSceneTools(connection, snippet,
|
|
67
|
+
...createSceneTools(connection, snippet, { invokeTool: host.invokeTool }),
|
|
68
68
|
].filter((def) => isToolAllowedInEdition(def.tool.name, EDITION));
|
|
69
69
|
host.registerTools(defs);
|
|
70
70
|
},
|
|
@@ -37,7 +37,7 @@ export function pickThresholdLevel(hist) {
|
|
|
37
37
|
bright += bins[i];
|
|
38
38
|
return { level, bright_fraction: bright / total };
|
|
39
39
|
}
|
|
40
|
-
export function estimateHorizon(rowMeans, docHeight, pick) {
|
|
40
|
+
export function estimateHorizon(rowMeans, docHeight, pick, mainSubjectBox, primaryFaceBox) {
|
|
41
41
|
const split = pick.bright_fraction;
|
|
42
42
|
const splitConfidence = Math.max(0, Math.min(1, 1 - Math.abs(0.5 - split) * 2)) * 0.6 + 0.2;
|
|
43
43
|
if (!rowMeans || rowMeans.length === 0) {
|
|
@@ -55,12 +55,30 @@ export function estimateHorizon(rowMeans, docHeight, pick) {
|
|
|
55
55
|
return { y: Math.round(docHeight / 3), placement: 1 / 3, confidence: 0.2 };
|
|
56
56
|
}
|
|
57
57
|
const y = Math.round((crossRow / R) * docHeight);
|
|
58
|
+
if ((mainSubjectBox && isDominantSubjectCrossing(y, docHeight, mainSubjectBox)) ||
|
|
59
|
+
(primaryFaceBox && isPortraitFaceCrossing(y, docHeight, primaryFaceBox))) {
|
|
60
|
+
return { y: Math.round(docHeight / 3), placement: 1 / 3, confidence: 0.2 };
|
|
61
|
+
}
|
|
58
62
|
return {
|
|
59
63
|
y,
|
|
60
64
|
placement: y / docHeight,
|
|
61
65
|
confidence: Math.max(0, Math.min(1, splitConfidence)),
|
|
62
66
|
};
|
|
63
67
|
}
|
|
68
|
+
function isDominantSubjectCrossing(y, docHeight, box) {
|
|
69
|
+
if (docHeight <= 0)
|
|
70
|
+
return false;
|
|
71
|
+
const [, top, , bottom] = box;
|
|
72
|
+
const spans = (bottom - top) / docHeight;
|
|
73
|
+
return spans >= 0.6 && y >= top && y <= bottom;
|
|
74
|
+
}
|
|
75
|
+
function isPortraitFaceCrossing(y, docHeight, face) {
|
|
76
|
+
if (docHeight <= 0)
|
|
77
|
+
return false;
|
|
78
|
+
const [, top, , bottom] = face;
|
|
79
|
+
const spans = (bottom - top) / docHeight;
|
|
80
|
+
return spans >= 0.25 && y >= top;
|
|
81
|
+
}
|
|
64
82
|
export function computeTonalZones(hist) {
|
|
65
83
|
const { bins } = hist;
|
|
66
84
|
const total = hist.total || bins.reduce((a, b) => a + b, 0);
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
+
import { orderBySpatialPick } from '../detection/geometry.js';
|
|
1
2
|
export function bboxToBox(b) {
|
|
2
3
|
return { left: b[0], top: b[1], right: b[2], bottom: b[3] };
|
|
3
4
|
}
|
|
4
|
-
const cx = (b) => (b[0] + b[2]) / 2;
|
|
5
|
-
const cy = (b) => (b[1] + b[3]) / 2;
|
|
6
|
-
const area = (b) => (b[2] - b[0]) * (b[3] - b[1]);
|
|
7
5
|
function orderBy(items, pick) {
|
|
8
|
-
|
|
9
|
-
leftmost: (a, b) => cx(a.bbox) - cx(b.bbox),
|
|
10
|
-
rightmost: (a, b) => cx(b.bbox) - cx(a.bbox),
|
|
11
|
-
topmost: (a, b) => cy(a.bbox) - cy(b.bbox),
|
|
12
|
-
bottommost: (a, b) => cy(b.bbox) - cy(a.bbox),
|
|
13
|
-
largest: (a, b) => area(b.bbox) - area(a.bbox),
|
|
14
|
-
smallest: (a, b) => area(a.bbox) - area(b.bbox),
|
|
15
|
-
};
|
|
16
|
-
return pick === 'confidence' ? items : [...items].sort(cmp[pick]);
|
|
6
|
+
return pick === 'confidence' ? items : orderBySpatialPick(items, pick);
|
|
17
7
|
}
|
|
18
8
|
export function localizeAnchor(det, sel) {
|
|
19
9
|
const candidates = sel.kind === 'face'
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { decode } from 'jpeg-js';
|
|
2
1
|
import { ValidationError } from '../utils/validate.js';
|
|
3
2
|
import { detectActiveDoc } from '../detection/detect-active-doc.js';
|
|
4
3
|
import { localizeAnchor } from './grounding-anchors.js';
|
|
@@ -214,14 +213,7 @@ export async function resolveToGeometry(connection, client, input) {
|
|
|
214
213
|
});
|
|
215
214
|
const docW = det.docWidth;
|
|
216
215
|
const docH = det.docHeight;
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
if (det.exportBytes.length > 0)
|
|
220
|
-
exportImg = decode(det.exportBytes, { useTArray: true, formatAsRGBA: true });
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
exportImg = null;
|
|
224
|
-
}
|
|
216
|
+
const exportImg = det.decoded ?? null;
|
|
225
217
|
const s = exportImg ? exportImg.width / docW : 1;
|
|
226
218
|
const anchors = {};
|
|
227
219
|
const anchorMeta = {};
|
|
@@ -1,31 +1,5 @@
|
|
|
1
1
|
import { boundsOf } from './grounding-geometry.js';
|
|
2
|
-
|
|
3
|
-
if (x < 0 || y < 0 || x >= img.width || y >= img.height)
|
|
4
|
-
return;
|
|
5
|
-
const i = (y * img.width + x) * 4;
|
|
6
|
-
img.data[i] = r;
|
|
7
|
-
img.data[i + 1] = g;
|
|
8
|
-
img.data[i + 2] = b;
|
|
9
|
-
img.data[i + 3] = 255;
|
|
10
|
-
}
|
|
11
|
-
function drawRing(img, c, radius, thick, color) {
|
|
12
|
-
const cx = Math.round(c.x), cy = Math.round(c.y);
|
|
13
|
-
const outer = radius + thick;
|
|
14
|
-
for (let dy = -outer; dy <= outer; dy++) {
|
|
15
|
-
for (let dx = -outer; dx <= outer; dx++) {
|
|
16
|
-
const d = Math.hypot(dx, dy);
|
|
17
|
-
if (d >= radius - thick && d <= radius + thick)
|
|
18
|
-
setPx(img, cx + dx, cy + dy, color);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function disc(img, c, radius, color) {
|
|
23
|
-
const cx = Math.round(c.x), cy = Math.round(c.y);
|
|
24
|
-
for (let dy = -radius; dy <= radius; dy++)
|
|
25
|
-
for (let dx = -radius; dx <= radius; dx++)
|
|
26
|
-
if (dx * dx + dy * dy <= radius * radius)
|
|
27
|
-
setPx(img, cx + dx, cy + dy, color);
|
|
28
|
-
}
|
|
2
|
+
import { drawRing, fillDisc } from './overlay.js';
|
|
29
3
|
function drawPolyline(img, pts, thick, color, closed = false) {
|
|
30
4
|
const r = Math.max(0, thick - 1);
|
|
31
5
|
const seq = closed && pts.length > 2 ? [...pts, pts[0]] : pts;
|
|
@@ -34,7 +8,7 @@ function drawPolyline(img, pts, thick, color, closed = false) {
|
|
|
34
8
|
const steps = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y)));
|
|
35
9
|
for (let s = 0; s <= steps; s++) {
|
|
36
10
|
const t = s / steps;
|
|
37
|
-
|
|
11
|
+
fillDisc(img, a.x + t * (b.x - a.x), a.y + t * (b.y - a.y), r, color);
|
|
38
12
|
}
|
|
39
13
|
}
|
|
40
14
|
}
|
|
@@ -73,7 +47,8 @@ export function renderReviewCrop(img, placement, opts = {}) {
|
|
|
73
47
|
}
|
|
74
48
|
const toCrop = (p) => ({ x: p.x - ox, y: p.y - oy });
|
|
75
49
|
if (placement.kind === 'point') {
|
|
76
|
-
|
|
50
|
+
const p = toCrop(placement.point);
|
|
51
|
+
drawRing(out, p.x, p.y, opts.ringRadius ?? 13, opts.ringThickness ?? 2, color);
|
|
77
52
|
}
|
|
78
53
|
else if (placement.kind === 'curve') {
|
|
79
54
|
drawPolyline(out, placement.curve.map(toCrop), opts.strokeThickness ?? 2, color);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const ANNOTATION_RGB = {
|
|
2
|
+
face: [0, 220, 255],
|
|
3
|
+
object: [255, 0, 220],
|
|
4
|
+
horizon: [255, 230, 0],
|
|
5
|
+
};
|
|
6
|
+
export function annotationThickness(img) {
|
|
7
|
+
return Math.max(2, Math.round(Math.max(img.width, img.height) / 400));
|
|
8
|
+
}
|
|
9
|
+
export function setPx(img, x, y, [r, g, b]) {
|
|
10
|
+
if (x < 0 || y < 0 || x >= img.width || y >= img.height)
|
|
11
|
+
return;
|
|
12
|
+
const i = (y * img.width + x) * 4;
|
|
13
|
+
img.data[i] = r;
|
|
14
|
+
img.data[i + 1] = g;
|
|
15
|
+
img.data[i + 2] = b;
|
|
16
|
+
img.data[i + 3] = 255;
|
|
17
|
+
}
|
|
18
|
+
export function drawBoxOutline(img, box, rgb, thickness) {
|
|
19
|
+
const [x1, y1, x2, y2] = box.map(Math.round);
|
|
20
|
+
for (let x = x1; x <= x2; x++)
|
|
21
|
+
for (let k = 0; k < thickness; k++) {
|
|
22
|
+
setPx(img, x, y1 + k, rgb);
|
|
23
|
+
setPx(img, x, y2 - k, rgb);
|
|
24
|
+
}
|
|
25
|
+
for (let y = y1; y <= y2; y++)
|
|
26
|
+
for (let k = 0; k < thickness; k++) {
|
|
27
|
+
setPx(img, x1 + k, y, rgb);
|
|
28
|
+
setPx(img, x2 - k, y, rgb);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function drawHLine(img, y, rgb, thickness) {
|
|
32
|
+
for (let x = 0; x < img.width; x++)
|
|
33
|
+
for (let k = 0; k < thickness; k++)
|
|
34
|
+
setPx(img, x, y + k, rgb);
|
|
35
|
+
}
|
|
36
|
+
export function fillSquare(img, cx, cy, radius, rgb) {
|
|
37
|
+
const x0 = Math.round(cx);
|
|
38
|
+
const y0 = Math.round(cy);
|
|
39
|
+
for (let dy = -radius; dy <= radius; dy++)
|
|
40
|
+
for (let dx = -radius; dx <= radius; dx++)
|
|
41
|
+
setPx(img, x0 + dx, y0 + dy, rgb);
|
|
42
|
+
}
|
|
43
|
+
export function drawRing(img, cx, cy, radius, thick, rgb) {
|
|
44
|
+
const x0 = Math.round(cx);
|
|
45
|
+
const y0 = Math.round(cy);
|
|
46
|
+
const outer = radius + thick;
|
|
47
|
+
for (let dy = -outer; dy <= outer; dy++) {
|
|
48
|
+
for (let dx = -outer; dx <= outer; dx++) {
|
|
49
|
+
const d = Math.hypot(dx, dy);
|
|
50
|
+
if (d >= radius - thick && d <= radius + thick)
|
|
51
|
+
setPx(img, x0 + dx, y0 + dy, rgb);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function fillDisc(img, cx, cy, radius, rgb) {
|
|
56
|
+
const x0 = Math.round(cx);
|
|
57
|
+
const y0 = Math.round(cy);
|
|
58
|
+
for (let dy = -radius; dy <= radius; dy++)
|
|
59
|
+
for (let dx = -radius; dx <= radius; dx++)
|
|
60
|
+
if (dx * dx + dy * dy <= radius * radius)
|
|
61
|
+
setPx(img, x0 + dx, y0 + dy, rgb);
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const HASH_SAMPLE_BYTES = 4096;
|
|
2
|
+
const HASH_STRIDE_SAMPLES = 1024;
|
|
3
|
+
function fnv1a(data, start, end, seed) {
|
|
4
|
+
let h = seed;
|
|
5
|
+
for (let i = start; i < end; i++) {
|
|
6
|
+
h = Math.imul(h ^ data[i], 0x01000193) >>> 0;
|
|
7
|
+
}
|
|
8
|
+
return h;
|
|
9
|
+
}
|
|
10
|
+
export function docKeyFrom(context, docWidth, docHeight) {
|
|
11
|
+
const doc = context
|
|
12
|
+
?.document;
|
|
13
|
+
const name = typeof doc?.name === 'string' ? doc.name : null;
|
|
14
|
+
if (name === null)
|
|
15
|
+
return null;
|
|
16
|
+
const sel = doc?.hasSelection === true ? 'S' : 'N';
|
|
17
|
+
return `${name}:${docWidth}x${docHeight}:${sel}`;
|
|
18
|
+
}
|
|
19
|
+
export function computePixelIdentity(decoded, docKey) {
|
|
20
|
+
const { width, height, data } = decoded;
|
|
21
|
+
const n = data.length;
|
|
22
|
+
let h = fnv1a(data, 0, Math.min(HASH_SAMPLE_BYTES, n), 0x811c9dc5);
|
|
23
|
+
h = fnv1a(data, Math.max(0, n - HASH_SAMPLE_BYTES), n, h);
|
|
24
|
+
const stride = Math.max(1, Math.floor(n / HASH_STRIDE_SAMPLES));
|
|
25
|
+
for (let i = 0; i < n; i += stride) {
|
|
26
|
+
h = Math.imul(h ^ data[i], 0x01000193) >>> 0;
|
|
27
|
+
}
|
|
28
|
+
return { docKey, width, height, byteLength: n, hash: h };
|
|
29
|
+
}
|
|
30
|
+
export function samePixelIdentity(a, b) {
|
|
31
|
+
if (a.docKey === null || b.docKey === null)
|
|
32
|
+
return false;
|
|
33
|
+
return (a.docKey === b.docKey &&
|
|
34
|
+
a.width === b.width &&
|
|
35
|
+
a.height === b.height &&
|
|
36
|
+
a.byteLength === b.byteLength &&
|
|
37
|
+
a.hash === b.hash);
|
|
38
|
+
}
|
|
39
|
+
export function identityKeyString(identity) {
|
|
40
|
+
return `${identity.docKey}:${identity.width}x${identity.height}:${identity.byteLength}:${identity.hash.toString(16)}`;
|
|
41
|
+
}
|