editmamei 0.17.4 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/NOTICES.md +43 -0
  2. package/README.md +91 -8
  3. package/dist/api/extendscript/_helpers.js +8 -0
  4. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  5. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  6. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  7. package/dist/cli/clients/json-config.js +1 -1
  8. package/dist/core/server.js +92 -24
  9. package/dist/core/tool-groups.js +178 -0
  10. package/dist/core/tool-tiers.js +34 -48
  11. package/dist/delivery/bundle.js +8 -0
  12. package/dist/delivery/config.js +23 -0
  13. package/dist/delivery/provision.js +30 -2
  14. package/dist/delivery/store.js +40 -1
  15. package/dist/detection/detect-active-doc.js +84 -0
  16. package/dist/detection/detection-client.js +62 -0
  17. package/dist/detection/face-contour-geometry.js +137 -0
  18. package/dist/detection/face-detector.js +34 -0
  19. package/dist/detection/face-feature-geometry.js +180 -0
  20. package/dist/detection/geometry.js +35 -0
  21. package/dist/detection/landmark-detection-client.js +38 -0
  22. package/dist/detection/landmark-detector.js +63 -0
  23. package/dist/detection/landmark-spec.js +29 -0
  24. package/dist/detection/mesh-face.js +36 -0
  25. package/dist/detection/object-detector.js +135 -0
  26. package/dist/detection/runtime.js +46 -0
  27. package/dist/license/env-activation.js +46 -7
  28. package/dist/models/dfine-s-coco-quant.onnx +0 -0
  29. package/dist/models/ultraface-rfb-320.onnx +0 -0
  30. package/dist/modules/ce/index.js +23 -5
  31. package/dist/perception/facets.js +148 -0
  32. package/dist/perception/region-precompute.js +122 -0
  33. package/dist/perception/region-scorer.js +151 -0
  34. package/dist/perception/scene-model.js +156 -0
  35. package/dist/perception/select-recipes.js +493 -0
  36. package/dist/perception/sky-ground-flood.js +234 -0
  37. package/dist/perception/sky-mask-transfer.js +77 -0
  38. package/dist/platform/macos-executor.js +0 -1
  39. package/dist/platform/windows-executor.js +6 -1
  40. package/dist/preview/coordinate-grid.js +250 -0
  41. package/dist/skills/editmamei-skill.zip +0 -0
  42. package/dist/telemetry/events.js +2 -1
  43. package/dist/telemetry/sanitize.js +15 -1
  44. package/dist/tools/adjustment-tools.js +46 -51
  45. package/dist/tools/detection-tools.js +209 -0
  46. package/dist/tools/document-tools.js +41 -30
  47. package/dist/tools/filter-tools.js +616 -101
  48. package/dist/tools/guide-tools.js +186 -0
  49. package/dist/tools/history-tools.js +1 -39
  50. package/dist/tools/image-tools.js +88 -0
  51. package/dist/tools/inspect-tools.js +86 -0
  52. package/dist/tools/layer-properties-tools.js +195 -106
  53. package/dist/tools/layer-tools.js +55 -14
  54. package/dist/tools/layer-transform-tools.js +236 -73
  55. package/dist/tools/metadata-tools.js +2 -32
  56. package/dist/tools/object-tools.js +193 -0
  57. package/dist/tools/overview-tools.js +69 -43
  58. package/dist/tools/path-tools.js +274 -0
  59. package/dist/tools/portrait-tools.js +146 -0
  60. package/dist/tools/preview-tools.js +78 -3
  61. package/dist/tools/retouch-tools.js +45 -33
  62. package/dist/tools/scene-tools.js +401 -0
  63. package/dist/tools/selection-tools.js +766 -228
  64. package/dist/tools/text-on-object-tools.js +225 -0
  65. package/dist/tools/text-tools.js +47 -56
  66. package/dist/tools/transform-canvas-tools.js +138 -0
  67. package/dist/tools/vector-mask-tools.js +116 -0
  68. package/dist/version.js +1 -1
  69. package/package.json +4 -2
@@ -0,0 +1,180 @@
1
+ import { LANDMARK_GROUPS } from './landmark-spec.js';
2
+ export const FACE_FEATURES = [
3
+ 'face',
4
+ 'skin',
5
+ 'eyes',
6
+ 'left_eye',
7
+ 'right_eye',
8
+ 'brows',
9
+ 'left_brow',
10
+ 'right_brow',
11
+ 'lips',
12
+ 'teeth',
13
+ 'nose',
14
+ 'under_eye',
15
+ 'cheeks',
16
+ ];
17
+ function ring(points, indices) {
18
+ const out = [];
19
+ for (const i of indices) {
20
+ const p = points[i];
21
+ if (p)
22
+ out.push([p.x, p.y]);
23
+ }
24
+ return out;
25
+ }
26
+ export function convexHull(pts) {
27
+ const uniq = Array.from(new Set(pts.map((p) => `${p[0]},${p[1]}`))).map((s) => s.split(',').map(Number));
28
+ if (uniq.length <= 2)
29
+ return uniq;
30
+ uniq.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
31
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
32
+ const lower = [];
33
+ for (const p of uniq) {
34
+ while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
35
+ lower.pop();
36
+ lower.push(p);
37
+ }
38
+ const upper = [];
39
+ for (let i = uniq.length - 1; i >= 0; i--) {
40
+ const p = uniq[i];
41
+ while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
42
+ upper.pop();
43
+ upper.push(p);
44
+ }
45
+ lower.pop();
46
+ upper.pop();
47
+ return lower.concat(upper);
48
+ }
49
+ function bbox(pts) {
50
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
51
+ for (const [x, y] of pts) {
52
+ if (x < minX)
53
+ minX = x;
54
+ if (y < minY)
55
+ minY = y;
56
+ if (x > maxX)
57
+ maxX = x;
58
+ if (y > maxY)
59
+ maxY = y;
60
+ }
61
+ return [minX, minY, maxX, maxY];
62
+ }
63
+ function underEyeBand(eyeRing, drop = 1.1) {
64
+ if (eyeRing.length < 3)
65
+ return null;
66
+ const [minX, minY, maxX, maxY] = bbox(eyeRing);
67
+ const h = maxY - minY;
68
+ if (h <= 0)
69
+ return null;
70
+ const top = maxY + h * 0.15;
71
+ const bottom = maxY + h * (0.15 + drop);
72
+ return [
73
+ [minX, top],
74
+ [maxX, top],
75
+ [maxX, bottom],
76
+ [minX, bottom],
77
+ ];
78
+ }
79
+ function cheekPatch(eyeRing, noseAla, mouthCorner) {
80
+ if (eyeRing.length < 3 || !noseAla || !mouthCorner)
81
+ return null;
82
+ const [minX, , maxX, maxY] = bbox(eyeRing);
83
+ const outerX = Math.abs(minX - noseAla[0]) > Math.abs(maxX - noseAla[0]) ? minX : maxX;
84
+ const eyeOuterLow = [outerX, maxY];
85
+ return convexHull([eyeOuterLow, noseAla, mouthCorner, [noseAla[0], mouthCorner[1]]]);
86
+ }
87
+ function rings(points) {
88
+ return {
89
+ faceOval: ring(points, LANDMARK_GROUPS.faceOval),
90
+ leftEye: ring(points, LANDMARK_GROUPS.leftEye),
91
+ rightEye: ring(points, LANDMARK_GROUPS.rightEye),
92
+ leftBrow: convexHull(ring(points, LANDMARK_GROUPS.leftEyebrow)),
93
+ rightBrow: convexHull(ring(points, LANDMARK_GROUPS.rightEyebrow)),
94
+ lipsOuter: ring(points, LANDMARK_GROUPS.lipsOuter),
95
+ lipsInner: ring(points, LANDMARK_GROUPS.lipsInner),
96
+ nose: convexHull(ring(points, LANDMARK_GROUPS.nose)),
97
+ };
98
+ }
99
+ function ok(p) {
100
+ return Array.isArray(p) && p.length >= 3;
101
+ }
102
+ export function buildFeaturePlan(points, feature) {
103
+ const r = rings(points);
104
+ const noseAla = points[129] ?? points[358] ?? null;
105
+ const leftMouthCorner = points[61] ? [points[61].x, points[61].y] : null;
106
+ const rightMouthCorner = points[291] ? [points[291].x, points[291].y] : null;
107
+ const ala = noseAla ? [noseAla.x, noseAla.y] : null;
108
+ switch (feature) {
109
+ case 'face':
110
+ return ok(r.faceOval) ? [{ points: r.faceOval, op: 'replace' }] : null;
111
+ case 'skin': {
112
+ if (!ok(r.faceOval))
113
+ return null;
114
+ const plan = [{ points: r.faceOval, op: 'replace' }];
115
+ for (const hole of [r.leftEye, r.rightEye, r.leftBrow, r.rightBrow, r.lipsOuter]) {
116
+ if (ok(hole))
117
+ plan.push({ points: hole, op: 'subtract' });
118
+ }
119
+ return plan;
120
+ }
121
+ case 'left_eye':
122
+ return ok(r.leftEye) ? [{ points: r.leftEye, op: 'replace' }] : null;
123
+ case 'right_eye':
124
+ return ok(r.rightEye) ? [{ points: r.rightEye, op: 'replace' }] : null;
125
+ case 'eyes': {
126
+ const plan = [];
127
+ if (ok(r.leftEye))
128
+ plan.push({ points: r.leftEye, op: 'replace' });
129
+ if (ok(r.rightEye))
130
+ plan.push({ points: r.rightEye, op: plan.length ? 'add' : 'replace' });
131
+ return plan.length ? plan : null;
132
+ }
133
+ case 'left_brow':
134
+ return ok(r.leftBrow) ? [{ points: r.leftBrow, op: 'replace' }] : null;
135
+ case 'right_brow':
136
+ return ok(r.rightBrow) ? [{ points: r.rightBrow, op: 'replace' }] : null;
137
+ case 'brows': {
138
+ const plan = [];
139
+ if (ok(r.leftBrow))
140
+ plan.push({ points: r.leftBrow, op: 'replace' });
141
+ if (ok(r.rightBrow))
142
+ plan.push({ points: r.rightBrow, op: plan.length ? 'add' : 'replace' });
143
+ return plan.length ? plan : null;
144
+ }
145
+ case 'lips': {
146
+ if (!ok(r.lipsOuter))
147
+ return null;
148
+ const plan = [{ points: r.lipsOuter, op: 'replace' }];
149
+ if (ok(r.lipsInner))
150
+ plan.push({ points: r.lipsInner, op: 'subtract' });
151
+ return plan;
152
+ }
153
+ case 'teeth':
154
+ return ok(r.lipsInner) ? [{ points: r.lipsInner, op: 'replace' }] : null;
155
+ case 'nose':
156
+ return ok(r.nose) ? [{ points: r.nose, op: 'replace' }] : null;
157
+ case 'under_eye': {
158
+ const plan = [];
159
+ const lb = underEyeBand(r.leftEye);
160
+ const rb = underEyeBand(r.rightEye);
161
+ if (ok(lb))
162
+ plan.push({ points: lb, op: 'replace' });
163
+ if (ok(rb))
164
+ plan.push({ points: rb, op: plan.length ? 'add' : 'replace' });
165
+ return plan.length ? plan : null;
166
+ }
167
+ case 'cheeks': {
168
+ const plan = [];
169
+ const lc = cheekPatch(r.leftEye, ala, leftMouthCorner);
170
+ const rc = cheekPatch(r.rightEye, ala, rightMouthCorner);
171
+ if (ok(lc))
172
+ plan.push({ points: lc, op: 'replace' });
173
+ if (ok(rc))
174
+ plan.push({ points: rc, op: plan.length ? 'add' : 'replace' });
175
+ return plan.length ? plan : null;
176
+ }
177
+ default:
178
+ return null;
179
+ }
180
+ }
@@ -0,0 +1,35 @@
1
+ export function iou(a, b) {
2
+ const x1 = Math.max(a[0], b[0]);
3
+ const y1 = Math.max(a[1], b[1]);
4
+ const x2 = Math.min(a[2], b[2]);
5
+ const y2 = Math.min(a[3], b[3]);
6
+ const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
7
+ const ua = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter;
8
+ return inter / (ua + 1e-9);
9
+ }
10
+ export function greedyNms(items, iouThreshold, sameGroup) {
11
+ const keep = [];
12
+ for (const item of items) {
13
+ const overlaps = keep.some((k) => (!sameGroup || sameGroup(k, item)) && iou(k.bbox, item.bbox) >= iouThreshold);
14
+ if (!overlaps)
15
+ keep.push(item);
16
+ }
17
+ return keep;
18
+ }
19
+ export function resizeToCHW(img, outW, outH, normalize) {
20
+ const { width: w, height: h, data } = img;
21
+ const out = new Float32Array(3 * outH * outW);
22
+ const plane = outH * outW;
23
+ for (let y = 0; y < outH; y++) {
24
+ const sy = Math.min(h - 1, Math.floor((y * h) / outH));
25
+ for (let x = 0; x < outW; x++) {
26
+ const sx = Math.min(w - 1, Math.floor((x * w) / outW));
27
+ const si = (sy * w + sx) * 4;
28
+ const di = y * outW + x;
29
+ out[di] = normalize(data[si]);
30
+ out[plane + di] = normalize(data[si + 1]);
31
+ out[2 * plane + di] = normalize(data[si + 2]);
32
+ }
33
+ }
34
+ return out;
35
+ }
@@ -0,0 +1,38 @@
1
+ import { decodeJpeg as realDecodeJpeg } from './runtime.js';
2
+ import { detectLandmarks as realDetectLandmarks, } from './landmark-detector.js';
3
+ import { OnnxDetectionClient, } from './detection-client.js';
4
+ const LANDMARK_BACKEND = 'facemesh-468';
5
+ export class OnnxLandmarkDetectionClient {
6
+ base;
7
+ opts;
8
+ detectLandmarks;
9
+ decodeJpeg;
10
+ constructor(base = new OnnxDetectionClient(), opts = {}, deps = {}) {
11
+ this.base = base;
12
+ this.opts = opts;
13
+ this.detectLandmarks = deps.detectLandmarks ?? realDetectLandmarks;
14
+ this.decodeJpeg = deps.decodeJpeg ?? realDecodeJpeg;
15
+ }
16
+ async detect(imagePath, opts) {
17
+ const result = await this.base.detect(imagePath, opts);
18
+ if (!opts.faces || !result.faces || result.faces.length === 0)
19
+ return result;
20
+ const img = this.decodeJpeg(imagePath);
21
+ const minScore = this.opts.minScore ?? 0.5;
22
+ const lmOpts = { margin: this.opts.margin };
23
+ for (const face of result.faces) {
24
+ try {
25
+ const mesh = await this.detectLandmarks(img, face.bbox, lmOpts);
26
+ if (mesh.score >= minScore) {
27
+ face.features = { points: mesh.points, score: mesh.score, backend: LANDMARK_BACKEND };
28
+ }
29
+ }
30
+ catch {
31
+ }
32
+ }
33
+ result.backends.faces = result.backends.faces
34
+ ? `${result.backends.faces}+${LANDMARK_BACKEND}`
35
+ : LANDMARK_BACKEND;
36
+ return result;
37
+ }
38
+ }
@@ -0,0 +1,63 @@
1
+ import { loadModel, resolveModelPath, ort } from './runtime.js';
2
+ import { LANDMARK_COUNT, MODEL_INPUT_SIZE } from './landmark-spec.js';
3
+ const MODEL_FILE = 'pro/face_mesh_468.onnx';
4
+ const DEFAULT_MARGIN = 0.25;
5
+ export function squareCrop(box, imgW, imgH, margin) {
6
+ const [x1, y1, x2, y2] = box;
7
+ const bw = Math.max(0, x2 - x1);
8
+ const bh = Math.max(0, y2 - y1);
9
+ const cx = x1 + bw / 2;
10
+ const cy = y1 + bh / 2;
11
+ let side = Math.max(bw, bh) * (1 + 2 * margin);
12
+ side = Math.min(side, imgW, imgH);
13
+ let x0 = cx - side / 2;
14
+ let y0 = cy - side / 2;
15
+ x0 = Math.max(0, Math.min(x0, imgW - side));
16
+ y0 = Math.max(0, Math.min(y0, imgH - side));
17
+ return { x0, y0, side };
18
+ }
19
+ export function cropResizeToCHW(img, crop, out) {
20
+ const { width: w, height: h, data } = img;
21
+ const tensor = new Float32Array(3 * out * out);
22
+ const plane = out * out;
23
+ for (let oy = 0; oy < out; oy++) {
24
+ const syf = crop.y0 + ((oy + 0.5) / out) * crop.side;
25
+ const sy = Math.min(h - 1, Math.max(0, Math.floor(syf)));
26
+ for (let ox = 0; ox < out; ox++) {
27
+ const sxf = crop.x0 + ((ox + 0.5) / out) * crop.side;
28
+ const sx = Math.min(w - 1, Math.max(0, Math.floor(sxf)));
29
+ const si = (sy * w + sx) * 4;
30
+ const di = oy * out + ox;
31
+ tensor[di] = data[si] / 255;
32
+ tensor[plane + di] = data[si + 1] / 255;
33
+ tensor[2 * plane + di] = data[si + 2] / 255;
34
+ }
35
+ }
36
+ return tensor;
37
+ }
38
+ export function mapLandmarks(rawFlat, crop) {
39
+ const points = new Array(LANDMARK_COUNT);
40
+ for (let i = 0; i < LANDMARK_COUNT; i++) {
41
+ points[i] = {
42
+ x: crop.x0 + rawFlat[i * 3] * crop.side,
43
+ y: crop.y0 + rawFlat[i * 3 + 1] * crop.side,
44
+ z: rawFlat[i * 3 + 2] * crop.side,
45
+ };
46
+ }
47
+ return points;
48
+ }
49
+ export async function detectLandmarks(img, box, opts = {}) {
50
+ const margin = opts.margin ?? DEFAULT_MARGIN;
51
+ const crop = squareCrop(box, img.width, img.height, margin);
52
+ if (crop.side <= 0)
53
+ throw new Error('degenerate face box for landmark crop');
54
+ const session = await loadModel(resolveModelPath(MODEL_FILE));
55
+ const input = cropResizeToCHW(img, crop, MODEL_INPUT_SIZE);
56
+ const feeds = {
57
+ image: new ort.Tensor('float32', input, [1, 3, MODEL_INPUT_SIZE, MODEL_INPUT_SIZE]),
58
+ };
59
+ const out = await session.run(feeds);
60
+ const score = out.scores.data[0] ?? 0;
61
+ const landmarks = out.landmarks.data;
62
+ return { score, points: mapLandmarks(landmarks, crop) };
63
+ }
@@ -0,0 +1,29 @@
1
+ export const LANDMARK_COUNT = 468;
2
+ export const MODEL_INPUT_SIZE = 192;
3
+ export const LANDMARK_GROUPS = {
4
+ faceOval: [
5
+ 10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288, 397, 365, 379, 378, 400, 377, 152,
6
+ 148, 176, 149, 150, 136, 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109,
7
+ ],
8
+ leftEye: [263, 249, 390, 373, 374, 380, 381, 382, 362, 398, 384, 385, 386, 387, 388, 466],
9
+ rightEye: [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246],
10
+ leftEyebrow: [276, 283, 282, 295, 285, 300, 293, 334, 296, 336],
11
+ rightEyebrow: [46, 53, 52, 65, 55, 70, 63, 105, 66, 107],
12
+ lipsOuter: [
13
+ 61, 146, 91, 181, 84, 17, 314, 405, 321, 375, 291, 409, 270, 269, 267, 0, 37, 39, 40, 185,
14
+ ],
15
+ lipsInner: [
16
+ 78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308, 415, 310, 311, 312, 13, 82, 81, 80, 191,
17
+ ],
18
+ nose: [168, 6, 197, 195, 5, 4, 1, 19, 94, 2, 98, 97, 326, 327, 129, 358],
19
+ };
20
+ export const LANDMARK_GROUP_KEYS = Object.keys(LANDMARK_GROUPS);
21
+ export const LANDMARK_ANCHORS = {
22
+ noseTip: 1,
23
+ chin: 152,
24
+ foreheadTop: 10,
25
+ betweenEyes: 168,
26
+ upperLipTop: 0,
27
+ lowerLipBottom: 17,
28
+ };
29
+ export const LANDMARK_ANCHOR_KEYS = Object.keys(LANDMARK_ANCHORS);
@@ -0,0 +1,36 @@
1
+ export function meshFaces(faces) {
2
+ const out = [];
3
+ for (const f of faces) {
4
+ if (f.features) {
5
+ out.push({
6
+ bbox: f.bbox,
7
+ confidence: f.confidence,
8
+ score: f.features.score,
9
+ points: f.features.points,
10
+ });
11
+ }
12
+ }
13
+ return out;
14
+ }
15
+ const area = (f) => (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]);
16
+ export function pickFace(faces, which) {
17
+ if (faces.length === 0)
18
+ return undefined;
19
+ const byLeft = [...faces].sort((a, b) => a.bbox[0] - b.bbox[0]);
20
+ switch (which) {
21
+ case 'best':
22
+ return [...faces].sort((a, b) => b.score - a.score || area(b) - area(a))[0];
23
+ case 'leftmost':
24
+ return byLeft[0];
25
+ case 'rightmost':
26
+ return byLeft[byLeft.length - 1];
27
+ case 'largest':
28
+ return [...faces].sort((a, b) => area(b) - area(a))[0];
29
+ default: {
30
+ const idx = Number.parseInt(which, 10);
31
+ if (String(idx) === which.trim() && idx >= 0 && idx < byLeft.length)
32
+ return byLeft[idx];
33
+ return undefined;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,135 @@
1
+ import { loadModel, resolveModelPath, decodeJpeg, ort } from './runtime.js';
2
+ import { greedyNms, resizeToCHW } from './geometry.js';
3
+ const MODEL_FILE = 'dfine-s-coco-quant.onnx';
4
+ const IN_SIZE = 640;
5
+ const NUM_QUERIES = 300;
6
+ const NUM_CLASSES = 80;
7
+ const DEFAULT_THRESHOLD = 0.4;
8
+ const IOU_THRESHOLD = 0.5;
9
+ const DEFAULT_MAX = 50;
10
+ export const COCO_LABELS = [
11
+ 'person',
12
+ 'bicycle',
13
+ 'car',
14
+ 'motorbike',
15
+ 'aeroplane',
16
+ 'bus',
17
+ 'train',
18
+ 'truck',
19
+ 'boat',
20
+ 'traffic light',
21
+ 'fire hydrant',
22
+ 'stop sign',
23
+ 'parking meter',
24
+ 'bench',
25
+ 'bird',
26
+ 'cat',
27
+ 'dog',
28
+ 'horse',
29
+ 'sheep',
30
+ 'cow',
31
+ 'elephant',
32
+ 'bear',
33
+ 'zebra',
34
+ 'giraffe',
35
+ 'backpack',
36
+ 'umbrella',
37
+ 'handbag',
38
+ 'tie',
39
+ 'suitcase',
40
+ 'frisbee',
41
+ 'skis',
42
+ 'snowboard',
43
+ 'sports ball',
44
+ 'kite',
45
+ 'baseball bat',
46
+ 'baseball glove',
47
+ 'skateboard',
48
+ 'surfboard',
49
+ 'tennis racket',
50
+ 'bottle',
51
+ 'wine glass',
52
+ 'cup',
53
+ 'fork',
54
+ 'knife',
55
+ 'spoon',
56
+ 'bowl',
57
+ 'banana',
58
+ 'apple',
59
+ 'sandwich',
60
+ 'orange',
61
+ 'broccoli',
62
+ 'carrot',
63
+ 'hot dog',
64
+ 'pizza',
65
+ 'donut',
66
+ 'cake',
67
+ 'chair',
68
+ 'sofa',
69
+ 'pottedplant',
70
+ 'bed',
71
+ 'diningtable',
72
+ 'toilet',
73
+ 'tvmonitor',
74
+ 'laptop',
75
+ 'mouse',
76
+ 'remote',
77
+ 'keyboard',
78
+ 'cell phone',
79
+ 'microwave',
80
+ 'oven',
81
+ 'toaster',
82
+ 'sink',
83
+ 'refrigerator',
84
+ 'book',
85
+ 'clock',
86
+ 'vase',
87
+ 'scissors',
88
+ 'teddy bear',
89
+ 'hair drier',
90
+ 'toothbrush',
91
+ ];
92
+ const sigmoid = (x) => 1 / (1 + Math.exp(-x));
93
+ export async function detectObjects(imagePath, opts = {}) {
94
+ const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
95
+ const maxObjects = opts.maxObjects ?? DEFAULT_MAX;
96
+ const img = decodeJpeg(imagePath);
97
+ const session = await loadModel(resolveModelPath(MODEL_FILE));
98
+ const input = resizeToCHW(img, IN_SIZE, IN_SIZE, (v) => v / 255);
99
+ const feeds = { pixel_values: new ort.Tensor('float32', input, [1, 3, IN_SIZE, IN_SIZE]) };
100
+ const out = await session.run(feeds);
101
+ const logits = out.logits.data;
102
+ const boxes = out.pred_boxes.data;
103
+ const cand = [];
104
+ for (let q = 0; q < NUM_QUERIES; q++) {
105
+ let bestC = -1;
106
+ let bestS = 0;
107
+ for (let c = 0; c < NUM_CLASSES; c++) {
108
+ const s = sigmoid(logits[q * NUM_CLASSES + c]);
109
+ if (s > bestS) {
110
+ bestS = s;
111
+ bestC = c;
112
+ }
113
+ }
114
+ if (bestS >= threshold) {
115
+ const cx = boxes[q * 4];
116
+ const cy = boxes[q * 4 + 1];
117
+ const bw = boxes[q * 4 + 2];
118
+ const bh = boxes[q * 4 + 3];
119
+ cand.push({
120
+ label: COCO_LABELS[bestC],
121
+ class_id: bestC,
122
+ confidence: bestS,
123
+ bbox: [
124
+ (cx - bw / 2) * img.width,
125
+ (cy - bh / 2) * img.height,
126
+ (cx + bw / 2) * img.width,
127
+ (cy + bh / 2) * img.height,
128
+ ],
129
+ });
130
+ }
131
+ }
132
+ cand.sort((a, b) => b.confidence - a.confidence);
133
+ const kept = greedyNms(cand, IOU_THRESHOLD, (a, b) => a.label === b.label);
134
+ return { width: img.width, height: img.height, objects: kept.slice(0, maxObjects) };
135
+ }
@@ -0,0 +1,46 @@
1
+ import { createRequire } from 'node:module';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { decode } from 'jpeg-js';
5
+ import { readFileSync } from 'node:fs';
6
+ import * as ort from 'onnxruntime-web';
7
+ let configured = false;
8
+ function configureOrt() {
9
+ if (configured)
10
+ return;
11
+ const require = createRequire(import.meta.url);
12
+ const ortDist = dirname(require.resolve('onnxruntime-web'));
13
+ ort.env.wasm.wasmPaths = pathToFileURL(join(ortDist, '/')).href;
14
+ ort.env.wasm.numThreads = 1;
15
+ ort.env.logLevel = 'error';
16
+ configured = true;
17
+ }
18
+ const sessions = new Map();
19
+ export function loadModel(absPath) {
20
+ configureOrt();
21
+ let p = sessions.get(absPath);
22
+ if (!p) {
23
+ p = ort.InferenceSession.create(absPath, { logSeverityLevel: 3 });
24
+ sessions.set(absPath, p);
25
+ }
26
+ return p;
27
+ }
28
+ export function resolveModelPath(filename) {
29
+ const override = process.env.EDITMAMEI_MODELS_DIR;
30
+ if (override)
31
+ return join(override, filename);
32
+ const here = dirname(fileURLToPath(import.meta.url));
33
+ return join(here, '..', 'models', filename);
34
+ }
35
+ export function decodeJpeg(path) {
36
+ let img;
37
+ try {
38
+ img = decode(readFileSync(path), { useTArray: true, formatAsRGBA: true });
39
+ }
40
+ catch (err) {
41
+ const msg = err instanceof Error ? err.message : String(err);
42
+ throw new Error(`failed to decode JPEG at ${path}: ${msg}`);
43
+ }
44
+ return { width: img.width, height: img.height, data: img.data };
45
+ }
46
+ export { ort };
@@ -1,20 +1,59 @@
1
1
  import { Logger } from '../utils/logger.js';
2
2
  import { readLicense } from './store.js';
3
- import { activate } from './entitlement.js';
3
+ import { activate, isProEntitled } from './entitlement.js';
4
+ import { provisionModules } from '../delivery/provision.js';
5
+ import { readInstalledModule, PRO_SKU } from '../delivery/store.js';
4
6
  const logger = new Logger('License');
5
- export async function maybeActivateFromEnv(env = process.env, ops = {}) {
7
+ const PROVISION_BOOT_TIMEOUT_MS = 20_000;
8
+ export async function maybeActivateFromEnv(env = process.env, ops = {}, delivery = {}) {
6
9
  const key = (env.EDITMAMEI_LICENSE_KEY ?? '').trim();
7
10
  if (!key)
8
11
  return;
9
12
  const cached = readLicense(ops);
10
- if (cached && cached.key === key)
11
- return;
13
+ if (!cached || cached.key !== key) {
14
+ try {
15
+ await activate(key, ops);
16
+ logger.info('Activated Pro from EDITMAMEI_LICENSE_KEY.');
17
+ }
18
+ catch (err) {
19
+ logger.warn(`Could not activate from EDITMAMEI_LICENSE_KEY (continuing as Community): ` +
20
+ `${err instanceof Error ? err.message : String(err)}`);
21
+ return;
22
+ }
23
+ }
12
24
  try {
13
- await activate(key, ops);
14
- logger.info('Activated Pro from EDITMAMEI_LICENSE_KEY.');
25
+ if (isProEntitled(ops) && !readInstalledModule(PRO_SKU, ops)) {
26
+ const provisioning = provisionModules(key, {
27
+ dir: ops.dir,
28
+ now: ops.now,
29
+ config: delivery.config,
30
+ fetchImpl: delivery.fetchImpl,
31
+ sleep: delivery.sleep,
32
+ signingKeys: delivery.signingKeys,
33
+ });
34
+ provisioning.catch(() => { });
35
+ let timer;
36
+ const deadline = new Promise((resolve) => {
37
+ timer = setTimeout(() => resolve(null), delivery.bootTimeoutMs ?? PROVISION_BOOT_TIMEOUT_MS);
38
+ timer.unref?.();
39
+ });
40
+ const prov = await Promise.race([provisioning, deadline]);
41
+ if (timer)
42
+ clearTimeout(timer);
43
+ if (!prov) {
44
+ logger.warn('Pro module provisioning did not finish in time — continuing as Community; the next start retries.');
45
+ }
46
+ else {
47
+ for (const m of prov.installed)
48
+ logger.info(`Installed ${m.sku} module v${m.version}.`);
49
+ for (const e of prov.errors) {
50
+ logger.warn(`Could not provision the ${e.sku} module (Pro stays dark): ${e.message}`);
51
+ }
52
+ }
53
+ }
15
54
  }
16
55
  catch (err) {
17
- logger.warn(`Could not activate from EDITMAMEI_LICENSE_KEY (continuing as Community): ` +
56
+ logger.warn(`Pro module provisioning failed (continuing without Pro): ` +
18
57
  `${err instanceof Error ? err.message : String(err)}`);
19
58
  }
20
59
  }