editmamei 0.19.0 → 0.22.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 +17 -1
- package/README.md +9 -8
- package/dist/api/extendscript/_helpers.js +1 -1
- 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/cli/config.js +3 -3
- package/dist/cli/help.js +4 -0
- package/dist/cli/install.js +2 -2
- package/dist/cli/report.js +11 -0
- package/dist/cli/router.js +20 -0
- package/dist/core/server.js +8 -8
- package/dist/core/settings.js +2 -2
- package/dist/core/tool-groups.js +92 -82
- package/dist/core/tool-tiers.js +91 -81
- package/dist/detection/landmark-detection-client.js +8 -0
- package/dist/detection/mesh-eye-correction.js +126 -0
- package/dist/detection/sam-segmenter.js +83 -0
- package/dist/diagnostics/collect.js +169 -0
- package/dist/index.js +2 -0
- package/dist/license/entitlement.js +52 -0
- package/dist/modules/ce/index.js +6 -4
- package/dist/perception/grounding-anchors.js +37 -0
- package/dist/perception/grounding-corners.js +93 -0
- package/dist/perception/grounding-edge-trace.js +90 -0
- package/dist/perception/grounding-extrema.js +41 -0
- package/dist/perception/grounding-gate.js +154 -0
- package/dist/perception/grounding-geometry.js +132 -0
- package/dist/perception/grounding-grid.js +34 -0
- package/dist/perception/grounding-landmarks.js +40 -0
- package/dist/perception/grounding-locate.js +341 -0
- package/dist/perception/grounding-resolver.js +202 -0
- package/dist/perception/grounding-review-crop.js +85 -0
- package/dist/perception/grounding-warp.js +182 -0
- package/dist/perception/sky-mask-transfer.js +50 -18
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +3 -3
- package/dist/tools/brush-tools.js +111 -23
- package/dist/tools/channel-compose-tools.js +241 -0
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/diagnostics-tools.js +66 -0
- package/dist/tools/document-tools.js +6 -6
- package/dist/tools/filter-tools.js +476 -19
- package/dist/tools/group-tools.js +10 -10
- package/dist/tools/guide-tools.js +4 -4
- package/dist/tools/history-tools.js +4 -4
- package/dist/tools/image-placement-tools.js +1 -1
- package/dist/tools/image-tools.js +62 -19
- package/dist/tools/inspect-tools.js +2 -2
- package/dist/tools/layer-ordering-tools.js +1 -1
- package/dist/tools/layer-properties-tools.js +42 -23
- package/dist/tools/layer-tools.js +10 -10
- package/dist/tools/layer-transform-tools.js +80 -41
- package/dist/tools/overview-tools.js +42 -42
- package/dist/tools/path-tools.js +53 -10
- package/dist/tools/portrait-tools.js +3 -3
- package/dist/tools/preview-tools.js +7 -82
- package/dist/tools/retouch-tools.js +93 -25
- package/dist/tools/scene-tools.js +13 -13
- package/dist/tools/selection-tools.js +319 -40
- package/dist/tools/shape-tools.js +216 -0
- package/dist/tools/text-tools.js +1 -1
- package/dist/tools/transform-canvas-tools.js +2 -2
- package/dist/tools/vector-mask-tools.js +23 -8
- package/dist/utils/log-buffer.js +41 -0
- package/dist/utils/logger.js +12 -1
- package/dist/utils/session-log-reader.js +27 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/preview/coordinate-grid.js +0 -250
- package/dist/tools/object-tools.js +0 -193
- package/dist/tools/text-on-object-tools.js +0 -225
package/dist/core/tool-tiers.js
CHANGED
|
@@ -1,85 +1,95 @@
|
|
|
1
1
|
export const TOOL_TIERS = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
2
|
+
ps_ping: 'community',
|
|
3
|
+
ps_list_capabilities: 'community',
|
|
4
|
+
ps_report_problem: 'community',
|
|
5
|
+
ps_list_actions: 'pro',
|
|
6
|
+
ps_play_action: 'pro',
|
|
7
|
+
ps_execute_script: 'pro',
|
|
8
|
+
ps_add_adjustment_layer: 'community',
|
|
9
|
+
ps_apply_adjustment: 'community',
|
|
10
|
+
ps_create_document: 'community',
|
|
11
|
+
ps_close_document: 'community',
|
|
12
|
+
ps_open_document: 'community',
|
|
13
|
+
ps_save_psd: 'community',
|
|
14
|
+
ps_export: 'community',
|
|
15
|
+
ps_apply_filter: 'community',
|
|
16
|
+
ps_create_group: 'community',
|
|
17
|
+
ps_move_layer_to_group: 'community',
|
|
18
|
+
ps_set_group_blend_mode: 'community',
|
|
19
|
+
ps_ungroup: 'community',
|
|
20
|
+
ps_delete_group: 'community',
|
|
21
|
+
ps_create_clipping_mask: 'community',
|
|
22
|
+
ps_release_clipping_mask: 'dev',
|
|
23
|
+
ps_undo: 'community',
|
|
24
|
+
ps_redo: 'community',
|
|
25
|
+
ps_place_image: 'community',
|
|
26
|
+
ps_resize_image: 'community',
|
|
27
|
+
ps_crop_document: 'community',
|
|
28
|
+
ps_convert_image_mode: 'community',
|
|
29
|
+
ps_move_layer_to_position: 'community',
|
|
30
|
+
ps_convert_to_smart_object: 'community',
|
|
31
|
+
ps_rasterize_layer: 'community',
|
|
32
|
+
ps_set_layer: 'community',
|
|
33
|
+
ps_duplicate_layer: 'community',
|
|
34
|
+
ps_copy_to_new_layer: 'community',
|
|
35
|
+
ps_merge: 'community',
|
|
36
|
+
ps_bake_layer: 'community',
|
|
37
|
+
ps_add_layer_style: 'community',
|
|
38
|
+
ps_create_layer: 'community',
|
|
39
|
+
ps_delete_layer: 'community',
|
|
40
|
+
ps_create_text_layer: 'community',
|
|
41
|
+
ps_fill_layer: 'community',
|
|
42
|
+
ps_add_fill_layer: 'community',
|
|
43
|
+
ps_select_layer: 'community',
|
|
44
|
+
ps_transform_layer: 'community',
|
|
45
|
+
ps_warp_layer: 'pro',
|
|
46
|
+
ps_warp_layer_mesh: 'pro',
|
|
47
|
+
ps_warp_layer_along: 'pro',
|
|
48
|
+
ps_warp_layer_region: 'pro',
|
|
49
|
+
ps_warp_layer_to: 'pro',
|
|
50
|
+
ps_apply_camera_raw: 'pro',
|
|
51
|
+
ps_transform_canvas: 'community',
|
|
52
|
+
ps_guides: 'community',
|
|
53
|
+
ps_inspect: 'community',
|
|
54
|
+
ps_overview: 'community',
|
|
55
|
+
ps_get_preview: 'community',
|
|
56
|
+
ps_get_layer_bounds_diff: 'community',
|
|
57
|
+
ps_compare_regions: 'community',
|
|
58
|
+
ps_get_histogram: 'community',
|
|
59
|
+
ps_retouch: 'community',
|
|
60
|
+
ps_apply_brush_stroke: 'dev',
|
|
61
|
+
ps_detect: 'community',
|
|
62
|
+
ps_edit_object: 'pro',
|
|
63
|
+
ps_portrait_touchup: 'dev',
|
|
64
|
+
ps_add_text_to_object: 'pro',
|
|
65
|
+
ps_detect_landmarks: 'pro',
|
|
66
|
+
ps_select_face_feature: 'pro',
|
|
67
|
+
ps_stroke_face_contour: 'dev',
|
|
68
|
+
ps_read_scene: 'community',
|
|
69
|
+
ps_select_by_reference: 'community',
|
|
70
|
+
ps_select: 'community',
|
|
71
|
+
ps_select_subject: 'community',
|
|
72
|
+
ps_select_sky: 'community',
|
|
73
|
+
ps_select_subject_instance: 'pro',
|
|
74
|
+
ps_select_object: 'pro',
|
|
75
|
+
ps_modify_selection: 'community',
|
|
76
|
+
ps_get_selection_preview: 'community',
|
|
77
|
+
ps_selection_channel: 'community',
|
|
78
|
+
ps_layer_mask: 'community',
|
|
79
|
+
ps_path: 'community',
|
|
80
|
+
ps_vector_mask: 'community',
|
|
81
|
+
ps_apply_image: 'community',
|
|
82
|
+
ps_calculations: 'community',
|
|
83
|
+
ps_shape: 'community',
|
|
84
|
+
ps_resolve_placement: 'pro',
|
|
85
|
+
ps_template_create_evidence: 'pro',
|
|
86
|
+
ps_template_save: 'pro',
|
|
87
|
+
ps_template_list: 'pro',
|
|
88
|
+
ps_template_apply: 'pro',
|
|
89
|
+
ps_template_verify: 'pro',
|
|
90
|
+
ps_template_recall: 'pro',
|
|
91
|
+
ps_template_delete: 'pro',
|
|
92
|
+
ps_set_text: 'community',
|
|
83
93
|
};
|
|
84
94
|
export function tierOf(toolName) {
|
|
85
95
|
const tier = TOOL_TIERS[toolName];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { decodeJpeg as realDecodeJpeg } from './runtime.js';
|
|
2
2
|
import { detectLandmarks as realDetectLandmarks, } from './landmark-detector.js';
|
|
3
3
|
import { OnnxDetectionClient, } from './detection-client.js';
|
|
4
|
+
import { correctMeshEyes } from './mesh-eye-correction.js';
|
|
4
5
|
const LANDMARK_BACKEND = 'facemesh-468';
|
|
5
6
|
export class OnnxLandmarkDetectionClient {
|
|
6
7
|
base;
|
|
@@ -20,11 +21,18 @@ export class OnnxLandmarkDetectionClient {
|
|
|
20
21
|
const img = this.decodeJpeg(imagePath);
|
|
21
22
|
const minScore = this.opts.minScore ?? 0.5;
|
|
22
23
|
const lmOpts = { margin: this.opts.margin };
|
|
24
|
+
const correctEyes = this.opts.correctEyes ?? true;
|
|
23
25
|
for (const face of result.faces) {
|
|
24
26
|
try {
|
|
25
27
|
const mesh = await this.detectLandmarks(img, face.bbox, lmOpts);
|
|
26
28
|
if (mesh.score >= minScore) {
|
|
27
29
|
face.features = { points: mesh.points, score: mesh.score, backend: LANDMARK_BACKEND };
|
|
30
|
+
if (correctEyes) {
|
|
31
|
+
const c = correctMeshEyes(img, face.features.points);
|
|
32
|
+
face.features.points = c.points;
|
|
33
|
+
if (c.correction)
|
|
34
|
+
face.features.eyeCorrection = c.correction;
|
|
35
|
+
}
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
catch {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { LANDMARK_GROUPS, LANDMARK_COUNT } from './landmark-spec.js';
|
|
2
|
+
const EYE_RIGID_GROUPS = ['leftEye', 'rightEye', 'leftEyebrow', 'rightEyebrow', 'nose'];
|
|
3
|
+
const EYE_RIGID_INDICES = new Set();
|
|
4
|
+
for (const g of EYE_RIGID_GROUPS)
|
|
5
|
+
for (const i of LANDMARK_GROUPS[g])
|
|
6
|
+
EYE_RIGID_INDICES.add(i);
|
|
7
|
+
const centroid = (pts) => ({
|
|
8
|
+
x: pts.reduce((s, p) => s + p.x, 0) / pts.length,
|
|
9
|
+
y: pts.reduce((s, p) => s + p.y, 0) / pts.length,
|
|
10
|
+
});
|
|
11
|
+
const bbox = (pts) => ({
|
|
12
|
+
x0: Math.min(...pts.map((p) => p.x)),
|
|
13
|
+
y0: Math.min(...pts.map((p) => p.y)),
|
|
14
|
+
x1: Math.max(...pts.map((p) => p.x)),
|
|
15
|
+
y1: Math.max(...pts.map((p) => p.y)),
|
|
16
|
+
});
|
|
17
|
+
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
18
|
+
function makeCtx(img) {
|
|
19
|
+
const { width: W, height: H, data } = img;
|
|
20
|
+
const luma = (x, y) => {
|
|
21
|
+
const i = (y * W + x) * 4;
|
|
22
|
+
return 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
|
23
|
+
};
|
|
24
|
+
const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
25
|
+
const blur = (x, y, r) => {
|
|
26
|
+
let s = 0, n = 0;
|
|
27
|
+
for (let dy = -r; dy <= r; dy++)
|
|
28
|
+
for (let dx = -r; dx <= r; dx++) {
|
|
29
|
+
s += luma(clamp(x + dx, 0, W - 1), clamp(y + dy, 0, H - 1));
|
|
30
|
+
n++;
|
|
31
|
+
}
|
|
32
|
+
return s / n;
|
|
33
|
+
};
|
|
34
|
+
return { W, H, luma, clamp, blur };
|
|
35
|
+
}
|
|
36
|
+
function findEyeCenter(ctx, center, ring) {
|
|
37
|
+
const { W, H, luma, clamp, blur } = ctx;
|
|
38
|
+
const rw = ring.x1 - ring.x0, rh = ring.y1 - ring.y0;
|
|
39
|
+
const x0 = clamp(Math.round(center.x - 0.55 * rw), 0, W - 1);
|
|
40
|
+
const x1 = clamp(Math.round(center.x + 0.55 * rw), 0, W - 1);
|
|
41
|
+
const y0 = clamp(Math.round(center.y - 0.5 * rh), 0, H - 1);
|
|
42
|
+
const y1 = clamp(Math.round(center.y + 2.0 * rh), 0, H - 1);
|
|
43
|
+
const ww = x1 - x0 + 1, wh = y1 - y0 + 1;
|
|
44
|
+
if (ww < 5 || wh < 5)
|
|
45
|
+
return { x: center.x, y: center.y, atEdge: false };
|
|
46
|
+
const L = new Float32Array(ww * wh);
|
|
47
|
+
for (let y = 0; y < wh; y++)
|
|
48
|
+
for (let x = 0; x < ww; x++)
|
|
49
|
+
L[y * ww + x] = luma(x0 + x, y0 + y);
|
|
50
|
+
const edges = [];
|
|
51
|
+
const gg = [];
|
|
52
|
+
let magSum = 0;
|
|
53
|
+
for (let y = 1; y < wh - 1; y++)
|
|
54
|
+
for (let x = 1; x < ww - 1; x++) {
|
|
55
|
+
const gx = (L[y * ww + x + 1] - L[y * ww + x - 1]) / 2;
|
|
56
|
+
const gy = (L[(y + 1) * ww + x] - L[(y - 1) * ww + x]) / 2;
|
|
57
|
+
const m = Math.hypot(gx, gy);
|
|
58
|
+
gg.push({ m, x, y, gx, gy });
|
|
59
|
+
magSum += m;
|
|
60
|
+
}
|
|
61
|
+
const mean = magSum / (gg.length || 1);
|
|
62
|
+
let v = 0;
|
|
63
|
+
for (const g of gg)
|
|
64
|
+
v += (g.m - mean) ** 2;
|
|
65
|
+
const std = Math.sqrt(v / (gg.length || 1));
|
|
66
|
+
const thr = mean + 0.3 * std;
|
|
67
|
+
for (const g of gg)
|
|
68
|
+
if (g.m > thr)
|
|
69
|
+
edges.push({ x: g.x, y: g.y, gx: g.gx / g.m, gy: g.gy / g.m });
|
|
70
|
+
let best = -Infinity, bx = Math.round(center.x - x0), by = Math.round(center.y - y0);
|
|
71
|
+
for (let cy = 2; cy < wh - 2; cy += 2)
|
|
72
|
+
for (let cx = 2; cx < ww - 2; cx += 2) {
|
|
73
|
+
let s = 0;
|
|
74
|
+
for (const e of edges) {
|
|
75
|
+
const dx = e.x - cx, dy = e.y - cy, l = Math.hypot(dx, dy);
|
|
76
|
+
if (l < 3)
|
|
77
|
+
continue;
|
|
78
|
+
const dot = (dx / l) * e.gx + (dy / l) * e.gy;
|
|
79
|
+
if (dot > 0)
|
|
80
|
+
s += dot * dot;
|
|
81
|
+
}
|
|
82
|
+
s *= 255 - blur(x0 + cx, y0 + cy, 3);
|
|
83
|
+
if (s > best) {
|
|
84
|
+
best = s;
|
|
85
|
+
bx = cx;
|
|
86
|
+
by = cy;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const atEdge = bx <= 3 || bx >= ww - 4 || by >= wh - 4;
|
|
90
|
+
return { x: x0 + bx, y: y0 + by, atEdge };
|
|
91
|
+
}
|
|
92
|
+
function similarity(m1, m2, t1, t2) {
|
|
93
|
+
const dmx = m2.x - m1.x, dmy = m2.y - m1.y, dtx = t2.x - t1.x, dty = t2.y - t1.y;
|
|
94
|
+
const s = Math.hypot(dtx, dty) / (Math.hypot(dmx, dmy) || 1);
|
|
95
|
+
const th = Math.atan2(dty, dtx) - Math.atan2(dmy, dmx);
|
|
96
|
+
const c = Math.cos(th) * s, sn = Math.sin(th) * s;
|
|
97
|
+
return (p) => ({
|
|
98
|
+
x: t1.x + c * (p.x - m1.x) - sn * (p.y - m1.y),
|
|
99
|
+
y: t1.y + sn * (p.x - m1.x) + c * (p.y - m1.y),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function correctMeshEyes(img, points) {
|
|
103
|
+
if (!points || points.length < LANDMARK_COUNT)
|
|
104
|
+
return { points, correction: null };
|
|
105
|
+
if (!img || !img.data || img.data.length < img.width * img.height * 4) {
|
|
106
|
+
return { points, correction: null };
|
|
107
|
+
}
|
|
108
|
+
const ctx = makeCtx(img);
|
|
109
|
+
const leftEye = LANDMARK_GROUPS.leftEye.map((i) => points[i]);
|
|
110
|
+
const rightEye = LANDMARK_GROUPS.rightEye.map((i) => points[i]);
|
|
111
|
+
const eyeLm = centroid(leftEye), eyeRm = centroid(rightEye);
|
|
112
|
+
if (dist(eyeLm, eyeRm) < 4)
|
|
113
|
+
return { points, correction: null };
|
|
114
|
+
const eyeLc = findEyeCenter(ctx, eyeLm, bbox(leftEye));
|
|
115
|
+
const eyeRc = findEyeCenter(ctx, eyeRm, bbox(rightEye));
|
|
116
|
+
const T = similarity(eyeLm, eyeRm, eyeLc, eyeRc);
|
|
117
|
+
const corrected = points.map((p, i) => (EYE_RIGID_INDICES.has(i) ? { ...T(p), z: p.z } : p));
|
|
118
|
+
return {
|
|
119
|
+
points: corrected,
|
|
120
|
+
correction: {
|
|
121
|
+
drift_left: Math.round(dist(eyeLc, eyeLm)),
|
|
122
|
+
drift_right: Math.round(dist(eyeRc, eyeRm)),
|
|
123
|
+
low_confidence: eyeLc.atEdge || eyeRc.atEdge,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { loadModel, resolveModelPath, ort } from './runtime.js';
|
|
2
|
+
const MODEL_ENCODER = 'pro/mobile_sam_encoder.onnx';
|
|
3
|
+
const MODEL_DECODER = 'pro/mobile_sam_decoder.onnx';
|
|
4
|
+
const SAM_INPUT = 1024;
|
|
5
|
+
export function boxToPromptCoords(box, imgW, imgH) {
|
|
6
|
+
const s = SAM_INPUT / Math.max(imgW, imgH);
|
|
7
|
+
const [x1, y1, x2, y2] = box;
|
|
8
|
+
return {
|
|
9
|
+
coords: Float32Array.from([x1 * s, y1 * s, x2 * s, y2 * s]),
|
|
10
|
+
labels: Float32Array.from([2, 3]),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function pointsToPromptCoords(fg, bg, imgW, imgH) {
|
|
14
|
+
const s = SAM_INPUT / Math.max(imgW, imgH);
|
|
15
|
+
const pts = [...fg, ...bg, { x: 0, y: 0 }];
|
|
16
|
+
const labels = [...fg.map(() => 1), ...bg.map(() => 0), -1];
|
|
17
|
+
const coords = new Float32Array(pts.length * 2);
|
|
18
|
+
pts.forEach((p, i) => {
|
|
19
|
+
coords[i * 2] = p.x * s;
|
|
20
|
+
coords[i * 2 + 1] = p.y * s;
|
|
21
|
+
});
|
|
22
|
+
return { coords, labels: Float32Array.from(labels) };
|
|
23
|
+
}
|
|
24
|
+
export function toEncoderInput(img) {
|
|
25
|
+
const { width: w, height: h, data } = img;
|
|
26
|
+
const out = new Float32Array(h * w * 3);
|
|
27
|
+
for (let p = 0; p < h * w; p++) {
|
|
28
|
+
out[p * 3] = data[p * 4];
|
|
29
|
+
out[p * 3 + 1] = data[p * 4 + 1];
|
|
30
|
+
out[p * 3 + 2] = data[p * 4 + 2];
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
export function thresholdMask(maskData, n) {
|
|
35
|
+
const mask = new Uint8Array(n);
|
|
36
|
+
for (let i = 0; i < n; i++)
|
|
37
|
+
mask[i] = maskData[i] > 0 ? 1 : 0;
|
|
38
|
+
return mask;
|
|
39
|
+
}
|
|
40
|
+
export class OnnxSamSegmenter {
|
|
41
|
+
async encodeImage(img) {
|
|
42
|
+
const session = await loadModel(resolveModelPath(MODEL_ENCODER));
|
|
43
|
+
const input = new ort.Tensor('float32', toEncoderInput(img), [img.height, img.width, 3]);
|
|
44
|
+
const out = await session.run({ input_image: input });
|
|
45
|
+
return out.image_embeddings;
|
|
46
|
+
}
|
|
47
|
+
async runDecoder(embedding, coords, labels, imgW, imgH) {
|
|
48
|
+
const session = await loadModel(resolveModelPath(MODEL_DECODER));
|
|
49
|
+
const nPts = labels.length;
|
|
50
|
+
const out = await session.run({
|
|
51
|
+
image_embeddings: embedding,
|
|
52
|
+
point_coords: new ort.Tensor('float32', coords, [1, nPts, 2]),
|
|
53
|
+
point_labels: new ort.Tensor('float32', labels, [1, nPts]),
|
|
54
|
+
mask_input: new ort.Tensor('float32', new Float32Array(256 * 256), [1, 1, 256, 256]),
|
|
55
|
+
has_mask_input: new ort.Tensor('float32', Float32Array.from([0]), [1]),
|
|
56
|
+
orig_im_size: new ort.Tensor('float32', Float32Array.from([imgH, imgW]), [2]),
|
|
57
|
+
});
|
|
58
|
+
const masks = out.masks;
|
|
59
|
+
const [, , mh, mw] = masks.dims;
|
|
60
|
+
return {
|
|
61
|
+
mask: thresholdMask(masks.data, mh * mw),
|
|
62
|
+
width: mw,
|
|
63
|
+
height: mh,
|
|
64
|
+
iou: out.iou_predictions.data[0] ?? 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async segmentBox(embedding, box, imgW, imgH) {
|
|
68
|
+
const { coords, labels } = boxToPromptCoords(box, imgW, imgH);
|
|
69
|
+
return this.runDecoder(embedding, coords, labels, imgW, imgH);
|
|
70
|
+
}
|
|
71
|
+
async segmentPointsAt(embedding, fg, bg, imgW, imgH) {
|
|
72
|
+
const { coords, labels } = pointsToPromptCoords(fg, bg, imgW, imgH);
|
|
73
|
+
return this.runDecoder(embedding, coords, labels, imgW, imgH);
|
|
74
|
+
}
|
|
75
|
+
async segment(img, box) {
|
|
76
|
+
const embedding = await this.encodeImage(img);
|
|
77
|
+
return this.segmentBox(embedding, box, img.width, img.height);
|
|
78
|
+
}
|
|
79
|
+
async segmentPoints(img, fg, bg = []) {
|
|
80
|
+
const embedding = await this.encodeImage(img);
|
|
81
|
+
return this.segmentPointsAt(embedding, fg, bg, img.width, img.height);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { homedir, release as osRelease, arch as osArch, platform as osPlatform } from 'node:os';
|
|
2
|
+
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { VERSION } from '../version.js';
|
|
5
|
+
import { EDITION } from '../edition.js';
|
|
6
|
+
import { loadSettings, settingsDir } from '../core/settings.js';
|
|
7
|
+
import { sanitizeMessage } from '../telemetry/sanitize.js';
|
|
8
|
+
import { sharedLogBuffer } from '../utils/log-buffer.js';
|
|
9
|
+
import { readSessionLog, listRecentSessionIds } from '../utils/session-log-reader.js';
|
|
10
|
+
import { classifyError, generateSessionId, } from '../utils/session-log.js';
|
|
11
|
+
import { detectDownloadsDir } from '../cli/downloads-dir.js';
|
|
12
|
+
export const DIAGNOSTIC_BUNDLE_SCHEMA = 1;
|
|
13
|
+
export const ISSUES_URL = 'https://github.com/editmamei/editmamei-wiki/issues';
|
|
14
|
+
const MAX_LOG_LINES = 1000;
|
|
15
|
+
const MAX_DESKTOP_LOG_LINES = 400;
|
|
16
|
+
const MAX_RECENT_SESSIONS = 3;
|
|
17
|
+
const MAX_CALLS_PER_SESSION = 500;
|
|
18
|
+
const MAX_NOTE_LEN = 1000;
|
|
19
|
+
const MAX_LINE_LEN = 4000;
|
|
20
|
+
const MAX_DESKTOP_LINE_LEN = 1000;
|
|
21
|
+
function mcpClientLabel(meta) {
|
|
22
|
+
if (!meta?.mcp_client)
|
|
23
|
+
return null;
|
|
24
|
+
return `${meta.mcp_client.name} ${meta.mcp_client.version}`;
|
|
25
|
+
}
|
|
26
|
+
export async function collectDiagnostics(opts = {}) {
|
|
27
|
+
const logBuffer = opts.logBuffer ?? sharedLogBuffer;
|
|
28
|
+
const emHome = opts.homeDir ?? settingsDir();
|
|
29
|
+
const { settings } = loadSettings(opts.homeDir ? { dir: opts.homeDir } : {});
|
|
30
|
+
const server_log = logBuffer
|
|
31
|
+
.snapshot()
|
|
32
|
+
.slice(-MAX_LOG_LINES)
|
|
33
|
+
.map((line) => sanitizeMessage(line, MAX_LINE_LEN));
|
|
34
|
+
const sessionsDir = join(emHome, 'sessions');
|
|
35
|
+
const recentIds = await listRecentSessionIds(MAX_RECENT_SESSIONS, { dir: sessionsDir });
|
|
36
|
+
const recent_sessions = [];
|
|
37
|
+
let psVersion = opts.psVersion ?? null;
|
|
38
|
+
let mcpClient = null;
|
|
39
|
+
for (const id of recentIds) {
|
|
40
|
+
const entries = await readSessionLog(id, { dir: sessionsDir });
|
|
41
|
+
const meta = [...entries].reverse().find((e) => e.type === 'meta');
|
|
42
|
+
const calls = entries.filter((e) => e.type === 'call');
|
|
43
|
+
const reduced = calls.slice(-MAX_CALLS_PER_SESSION).map((c) => ({
|
|
44
|
+
seq: c.seq,
|
|
45
|
+
ts: c.ts,
|
|
46
|
+
tool: c.tool,
|
|
47
|
+
success: c.success,
|
|
48
|
+
duration_ms: c.duration_ms,
|
|
49
|
+
error_class: c.error_class ?? (c.success ? null : classifyError(c.error)),
|
|
50
|
+
}));
|
|
51
|
+
if (meta) {
|
|
52
|
+
if (!psVersion && meta.ps_version)
|
|
53
|
+
psVersion = meta.ps_version;
|
|
54
|
+
if (!mcpClient)
|
|
55
|
+
mcpClient = mcpClientLabel(meta);
|
|
56
|
+
}
|
|
57
|
+
recent_sessions.push({
|
|
58
|
+
session_id: id,
|
|
59
|
+
editmamei_version: meta?.editmamei_version ?? null,
|
|
60
|
+
ps_version: meta?.ps_version ?? null,
|
|
61
|
+
mcp_client: mcpClientLabel(meta),
|
|
62
|
+
call_count: calls.length,
|
|
63
|
+
calls: reduced,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const desktop = await readDesktopLogTail({
|
|
67
|
+
env: opts.env,
|
|
68
|
+
overrideDir: opts.desktopLogDir,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
schema: DIAGNOSTIC_BUNDLE_SCHEMA,
|
|
72
|
+
report_id: generateSessionId(opts.now),
|
|
73
|
+
editmamei_version: VERSION,
|
|
74
|
+
edition: EDITION,
|
|
75
|
+
platform: osPlatform(),
|
|
76
|
+
os_release: osRelease(),
|
|
77
|
+
arch: osArch(),
|
|
78
|
+
node_version: process.version,
|
|
79
|
+
install_id: settings.telemetry.install_id,
|
|
80
|
+
ps_version: psVersion,
|
|
81
|
+
mcp_client: mcpClient,
|
|
82
|
+
settings: {
|
|
83
|
+
telemetry_usage: settings.telemetry.usage,
|
|
84
|
+
telemetry_diagnostics: settings.telemetry.diagnostics,
|
|
85
|
+
update_check: settings.update_check,
|
|
86
|
+
send_previews_to_llm: settings.privacy.send_previews_to_llm,
|
|
87
|
+
},
|
|
88
|
+
note: opts.note ? sanitizeMessage(opts.note, MAX_NOTE_LEN) : null,
|
|
89
|
+
server_log,
|
|
90
|
+
desktop_log: desktop.lines,
|
|
91
|
+
desktop_log_source: desktop.source,
|
|
92
|
+
recent_sessions,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export async function writeDiagnosticBundle(bundle, opts = {}) {
|
|
96
|
+
const dest = opts.downloadsDir ?? detectDownloadsDir(opts.env ?? process.env).path;
|
|
97
|
+
await mkdir(dest, { recursive: true });
|
|
98
|
+
const path = join(dest, `editmamei-diagnostics-${bundle.report_id}.json`);
|
|
99
|
+
const json = JSON.stringify(bundle, null, 2);
|
|
100
|
+
await writeFile(path, json, { encoding: 'utf8', mode: 0o600 });
|
|
101
|
+
return { path, bytes: Buffer.byteLength(json, 'utf8') };
|
|
102
|
+
}
|
|
103
|
+
export function defaultDesktopLogDir(env = process.env) {
|
|
104
|
+
if (process.platform === 'darwin')
|
|
105
|
+
return join(homedir(), 'Library', 'Logs', 'Claude');
|
|
106
|
+
if (process.platform === 'win32') {
|
|
107
|
+
const appData = env.APPDATA;
|
|
108
|
+
return appData
|
|
109
|
+
? join(appData, 'Claude', 'logs')
|
|
110
|
+
: join(homedir(), 'AppData', 'Roaming', 'Claude', 'logs');
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const BASE64_RUN = /[A-Za-z0-9+/]{200,}={0,2}/g;
|
|
115
|
+
const JSONRPC_FRAME = /^(.*\bMessage (?:from|to) (?:client|server):\s*)(\{.*)$/;
|
|
116
|
+
const PAYLOAD_OBJECT = /("(?:arguments|params|result|input)"\s*:\s*)([{[][\s\S]*)$/;
|
|
117
|
+
export function sanitizeDesktopLogLine(line) {
|
|
118
|
+
const frame = JSONRPC_FRAME.exec(line);
|
|
119
|
+
if (frame) {
|
|
120
|
+
const prefix = sanitizeMessage(frame[1], MAX_LINE_LEN).replace(/\s+$/, '');
|
|
121
|
+
const body = frame[2];
|
|
122
|
+
const method = /"method"\s*:\s*"([^"]+)"/.exec(body)?.[1];
|
|
123
|
+
const id = /"id"\s*:\s*("?[\w.-]+"?)/.exec(body)?.[1];
|
|
124
|
+
const kind = method
|
|
125
|
+
? `method=${method}`
|
|
126
|
+
: /"result"/.test(body)
|
|
127
|
+
? 'result'
|
|
128
|
+
: /"error"/.test(body)
|
|
129
|
+
? 'error'
|
|
130
|
+
: 'frame';
|
|
131
|
+
return `${prefix} [${kind}${id ? ` id=${id}` : ''}, payload redacted, ${body.length} bytes]`;
|
|
132
|
+
}
|
|
133
|
+
let out = sanitizeMessage(line, MAX_LINE_LEN);
|
|
134
|
+
out = out.replace(PAYLOAD_OBJECT, '$1<redacted>');
|
|
135
|
+
out = out.replace(BASE64_RUN, '…[binary redacted]');
|
|
136
|
+
if (out.length > MAX_DESKTOP_LINE_LEN)
|
|
137
|
+
out = out.slice(0, MAX_DESKTOP_LINE_LEN) + '…[truncated]';
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
async function readDesktopLogTail(opts) {
|
|
141
|
+
const dir = opts.overrideDir ?? defaultDesktopLogDir(opts.env ?? process.env);
|
|
142
|
+
if (!dir)
|
|
143
|
+
return { lines: [], source: null };
|
|
144
|
+
let names;
|
|
145
|
+
try {
|
|
146
|
+
names = await readdir(dir);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return { lines: [], source: null };
|
|
150
|
+
}
|
|
151
|
+
const logs = names.filter((f) => f.toLowerCase().endsWith('.log'));
|
|
152
|
+
if (logs.length === 0)
|
|
153
|
+
return { lines: [], source: null };
|
|
154
|
+
const pick = logs.find((f) => /editmamei/i.test(f)) ??
|
|
155
|
+
logs.find((f) => f.toLowerCase() === 'mcp.log') ??
|
|
156
|
+
logs[0];
|
|
157
|
+
try {
|
|
158
|
+
const raw = await readFile(join(dir, pick), 'utf8');
|
|
159
|
+
const tail = raw
|
|
160
|
+
.split(/\r?\n/)
|
|
161
|
+
.filter((l) => l.length > 0)
|
|
162
|
+
.slice(-MAX_DESKTOP_LOG_LINES)
|
|
163
|
+
.map(sanitizeDesktopLogLine);
|
|
164
|
+
return { lines: tail, source: pick };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return { lines: [], source: null };
|
|
168
|
+
}
|
|
169
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
import { EditmameiServer } from './core/server.js';
|
|
5
5
|
import { routeCli } from './cli/router.js';
|
|
6
6
|
import { maybeActivateFromEnv } from './license/env-activation.js';
|
|
7
|
+
import { refreshIfStale } from './license/entitlement.js';
|
|
7
8
|
import { Logger } from './utils/logger.js';
|
|
8
9
|
const logger = new Logger('Main');
|
|
9
10
|
function installProcessHandlers() {
|
|
@@ -37,6 +38,7 @@ async function main() {
|
|
|
37
38
|
try {
|
|
38
39
|
logger.info('Starting Editmamei...');
|
|
39
40
|
await maybeActivateFromEnv();
|
|
41
|
+
await refreshIfStale();
|
|
40
42
|
const server = new EditmameiServer();
|
|
41
43
|
await server.start();
|
|
42
44
|
let shuttingDown = false;
|
|
@@ -2,7 +2,11 @@ import { resolvePolarConfig } from './config.js';
|
|
|
2
2
|
import { computeDeviceHash } from './device-hash.js';
|
|
3
3
|
import { readLicense, writeLicense, clearLicense, } from './store.js';
|
|
4
4
|
import { PolarLicenseClient, PolarLicenseError } from './polar-client.js';
|
|
5
|
+
import { Logger } from '../utils/logger.js';
|
|
6
|
+
const logger = new Logger('License');
|
|
5
7
|
export const GRACE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
8
|
+
export const REFRESH_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
export const EXPIRED_REFRESH_TIMEOUT_MS = 5_000;
|
|
6
10
|
export function evaluateEntitlement(rec, now) {
|
|
7
11
|
if (!rec)
|
|
8
12
|
return { entitled: false, reason: 'no-license' };
|
|
@@ -59,6 +63,54 @@ export async function activate(key, ops = {}) {
|
|
|
59
63
|
writeLicense(rec, ops);
|
|
60
64
|
return rec;
|
|
61
65
|
}
|
|
66
|
+
export async function refreshIfStale(ops = {}) {
|
|
67
|
+
const env = ops.env ?? process.env;
|
|
68
|
+
if (env.VITEST !== undefined || env.NODE_ENV === 'test')
|
|
69
|
+
return;
|
|
70
|
+
const rec = readLicense(ops);
|
|
71
|
+
if (!rec)
|
|
72
|
+
return;
|
|
73
|
+
if (rec.status === 'revoked' || rec.status === 'disabled')
|
|
74
|
+
return;
|
|
75
|
+
const now = (ops.now ?? Date.now)();
|
|
76
|
+
if (rec.expires_at !== null && Date.parse(rec.expires_at) <= now)
|
|
77
|
+
return;
|
|
78
|
+
const last = Date.parse(rec.last_validated_at);
|
|
79
|
+
const age = Number.isFinite(last) ? now - last : Infinity;
|
|
80
|
+
if (age <= REFRESH_AFTER_MS)
|
|
81
|
+
return;
|
|
82
|
+
if (age <= GRACE_MS) {
|
|
83
|
+
refresh(ops).catch((err) => {
|
|
84
|
+
logger.warn(`Background license refresh failed (grace covers offline use): ` +
|
|
85
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const timeoutMs = ops.expiredRefreshTimeoutMs ?? EXPIRED_REFRESH_TIMEOUT_MS;
|
|
90
|
+
const attempt = refresh(ops);
|
|
91
|
+
attempt.catch(() => { });
|
|
92
|
+
let timer;
|
|
93
|
+
try {
|
|
94
|
+
const winner = await Promise.race([
|
|
95
|
+
attempt,
|
|
96
|
+
new Promise((resolve) => {
|
|
97
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
if (winner === 'timeout') {
|
|
102
|
+
logger.warn('License re-validation timed out past the grace window — continuing as Community; the next boot retries.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
logger.warn(`License re-validation failed past the grace window (continuing as Community): ` +
|
|
107
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (timer)
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
62
114
|
export async function refresh(ops = {}) {
|
|
63
115
|
const rec = readLicense(ops);
|
|
64
116
|
if (!rec)
|