editmamei 0.23.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/NOTICES.md +6 -30
  2. package/README.md +47 -13
  3. package/dist/api/extendscript/_helpers.js +1 -101
  4. package/dist/api/photoshop-api.js +23 -16
  5. package/dist/api/snippet-client.js +38 -2
  6. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  7. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  8. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  9. package/dist/core/server.js +60 -184
  10. package/dist/core/session.js +9 -10
  11. package/dist/core/tool-registry.js +4 -4
  12. package/dist/delivery/client.js +50 -1
  13. package/dist/delivery/provision.js +18 -1
  14. package/dist/delivery/signing.js +38 -1
  15. package/dist/delivery/store.js +157 -16
  16. package/dist/detection/detect-active-doc.js +42 -11
  17. package/dist/detection/detection-client.js +3 -3
  18. package/dist/detection/face-detector.js +2 -2
  19. package/dist/detection/geometry.js +14 -0
  20. package/dist/detection/landmark-detection-client.js +3 -3
  21. package/dist/detection/mesh-face.js +2 -3
  22. package/dist/detection/object-detector.js +2 -2
  23. package/dist/detection/runtime.js +14 -3
  24. package/dist/detection/sam-segmenter.js +69 -26
  25. package/dist/kernel/kernel.js +5 -9
  26. package/dist/kernel/module-lifecycle.js +187 -0
  27. package/dist/license/entitlement.js +13 -3
  28. package/dist/license/store.js +8 -1
  29. package/dist/modules/ce/index.js +1 -1
  30. package/dist/perception/facets.js +19 -1
  31. package/dist/perception/grounding-anchors.js +2 -12
  32. package/dist/perception/grounding-locate.js +1 -9
  33. package/dist/perception/grounding-review-crop.js +4 -29
  34. package/dist/perception/overlay.js +62 -0
  35. package/dist/perception/pixel-identity.js +41 -0
  36. package/dist/perception/region-precompute.js +96 -20
  37. package/dist/perception/scene-model.js +93 -50
  38. package/dist/perception/select-recipes.js +109 -68
  39. package/dist/perception/sky-ground-flood.js +0 -5
  40. package/dist/perception/sky-mask-transfer.js +73 -14
  41. package/dist/platform/connection.js +60 -43
  42. package/dist/platform/host-platform.js +16 -0
  43. package/dist/platform/macos-detector.js +41 -70
  44. package/dist/platform/macos-runner.js +94 -0
  45. package/dist/platform/script-queue.js +68 -0
  46. package/dist/platform/script-result.js +39 -0
  47. package/dist/platform/windows-detector.js +49 -66
  48. package/dist/platform/windows-runner.js +84 -0
  49. package/dist/skills/editmamei-skill.zip +0 -0
  50. package/dist/tools/adjustment-tools.js +80 -132
  51. package/dist/tools/brush-tools.js +4 -15
  52. package/dist/tools/channel-compose-tools.js +4 -23
  53. package/dist/tools/detection-tools.js +59 -47
  54. package/dist/tools/document-tools.js +92 -127
  55. package/dist/tools/filter-tools.js +490 -731
  56. package/dist/tools/group-tools.js +75 -156
  57. package/dist/tools/guide-tools.js +52 -83
  58. package/dist/tools/history-tools.js +29 -63
  59. package/dist/tools/image-placement-tools.js +18 -37
  60. package/dist/tools/image-tools.js +23 -51
  61. package/dist/tools/layer-ordering-tools.js +2 -9
  62. package/dist/tools/layer-properties-tools.js +207 -395
  63. package/dist/tools/layer-tools.js +224 -190
  64. package/dist/tools/layer-transform-tools.js +41 -106
  65. package/dist/tools/metadata-tools.js +2 -9
  66. package/dist/tools/path-tools.js +4 -15
  67. package/dist/tools/portrait-tools.js +2 -5
  68. package/dist/tools/preview-tools.js +59 -72
  69. package/dist/tools/retouch-tools.js +33 -71
  70. package/dist/tools/scene-tools.js +123 -75
  71. package/dist/tools/selection-tools.js +386 -566
  72. package/dist/tools/shape-tools.js +2 -9
  73. package/dist/tools/text-tools.js +24 -57
  74. package/dist/tools/transform-canvas-tools.js +21 -54
  75. package/dist/tools/vector-mask-tools.js +2 -9
  76. package/dist/update/check.js +1 -1
  77. package/dist/utils/jpeg-quality.js +1 -0
  78. package/dist/utils/logger.js +55 -45
  79. package/dist/utils/operation-timeouts.js +1 -0
  80. package/dist/utils/run-script.js +7 -2
  81. package/dist/utils/session-log.js +134 -12
  82. package/dist/utils/template-storage.js +5 -6
  83. package/dist/utils/tool-helpers.js +43 -0
  84. package/dist/version.js +1 -1
  85. package/package.json +10 -3
  86. package/LICENSE +0 -81
  87. package/dist/platform/detector.js +0 -32
  88. package/dist/platform/macos-executor.js +0 -151
  89. package/dist/platform/windows-executor.js +0 -160
  90. /package/dist/platform/{script-executor.js → ports.js} +0 -0
@@ -1,9 +1,17 @@
1
- import { decode, encode } from 'jpeg-js';
1
+ import { encode } from 'jpeg-js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
3
  import { OnnxDetectionClient } from '../detection/detection-client.js';
4
+ import { ANNOTATED_PREVIEW_JPEG_QUALITY } from '../utils/jpeg-quality.js';
4
5
  import { buildSceneModel } from '../perception/scene-model.js';
5
6
  import { resolveSelection, SELECT_REFERENCE_TARGETS, } from '../perception/select-recipes.js';
6
- import { precomputeRegions, loadPrecomputedRegion, } from '../perception/region-precompute.js';
7
+ import { precomputeRegions, loadPrecomputedRegion, saveSelectionAsSceneChannel, CHANNEL_PREFIX, } from '../perception/region-precompute.js';
8
+ import { toolErrorResult } from '../utils/tool-helpers.js';
9
+ import { Logger } from '../utils/logger.js';
10
+ import { isProEntitled } from '../license/entitlement.js';
11
+ import { EDITION } from '../edition.js';
12
+ import { isToolAllowedInEdition } from '../core/tool-tiers.js';
13
+ import { ANNOTATION_RGB, annotationThickness, drawBoxOutline, drawHLine, } from '../perception/overlay.js';
14
+ const logger = new Logger('scene-tools');
7
15
  function makeProRefine(invokeTool) {
8
16
  return async (req) => {
9
17
  try {
@@ -29,29 +37,31 @@ function makeProRefine(invokeTool) {
29
37
  }
30
38
  };
31
39
  }
32
- function makePrecomputeFace(invokeTool) {
33
- return async () => {
34
- try {
35
- const res = await invokeTool('ps_select_face_feature', { precompute_all: true });
36
- if (res.isError)
37
- return [];
38
- const saved = res.structuredContent?.saved ?? [];
39
- return saved.map((s) => ({
40
- key: s.key,
41
- target: `face_${s.feature}`,
42
- method: 'face_mesh',
43
- confidence: 1,
44
- bounds: null,
45
- }));
46
- }
47
- catch {
48
- return [];
49
- }
50
- };
40
+ export const FACE_MENU_TARGETS = [
41
+ 'face_skin',
42
+ 'face_eyes',
43
+ 'face_brows',
44
+ 'face_lips',
45
+ 'face_teeth',
46
+ 'face_nose',
47
+ 'face_under_eye',
48
+ 'face_cheeks',
49
+ ];
50
+ function faceMenuFor(model, hasPro) {
51
+ if (!hasPro || model.faces.length === 0)
52
+ return [];
53
+ return FACE_MENU_TARGETS.map((target) => ({
54
+ key: `${CHANNEL_PREFIX}${target}`,
55
+ target,
56
+ method: 'face_mesh',
57
+ confidence: 1,
58
+ bounds: null,
59
+ on_demand: true,
60
+ }));
51
61
  }
52
62
  function skyCtxFrom(built) {
53
63
  return {
54
- exportBytes: built.exportBytes,
64
+ decoded: built.decoded,
55
65
  boxes: built.rawObjects.map((o) => o.bbox),
56
66
  docW: built.model.doc.width,
57
67
  docH: built.model.doc.height,
@@ -73,7 +83,7 @@ const sceneSchema = {
73
83
  save_regions: {
74
84
  type: 'boolean',
75
85
  default: true,
76
- description: 'Precompute every confident region (sky/ground/shadows/highlights/skin/subject/face — and, on a Pro host with a face, the face-feature set scene:face_skin/_eyes/_brows/_lips/_teeth/_nose/_under_eye/_cheeks) and SAVE each as a managed `scene:*` alpha channel, returning the `regions` MENU of what is confidently selectable (each with its method + confidence). ps_select_by_reference then loads the saved channel instantly. Set false for a light read with no channels.',
86
+ description: 'Precompute every confident region (sky/ground/shadows/highlights/skin/subject/face — and, on a Pro host with a face, the face-feature set scene:face_skin/_eyes/_brows/_lips/_teeth/_nose/_under_eye/_cheeks) and SAVE each as a managed `scene:*` alpha channel, returning the `regions` MENU of what is confidently selectable (each with its method + confidence). ps_select_by_reference then loads the saved channel instantly. Set false for a light read with no channels. The `scene:` channel-name prefix is RESERVED: channels matching it are treated as derived and are deleted on the next scene read and on ps_save_psd, so do not give a channel you want to keep a `scene:`-prefixed name.',
77
87
  },
78
88
  composition_context: {
79
89
  type: 'object',
@@ -97,42 +107,19 @@ const sceneSchema = {
97
107
  },
98
108
  },
99
109
  };
100
- function annotateScene(bytes, faces, objects, horizonY) {
101
- const img = decode(bytes, { useTArray: true, formatAsRGBA: true });
102
- const { width: w, height: h, data } = img;
103
- const thickness = Math.max(2, Math.round(Math.max(w, h) / 400));
104
- const setPx = (x, y, r, g, b) => {
105
- if (x < 0 || y < 0 || x >= w || y >= h)
106
- return;
107
- const i = (y * w + x) * 4;
108
- data[i] = r;
109
- data[i + 1] = g;
110
- data[i + 2] = b;
111
- data[i + 3] = 255;
112
- };
113
- const drawBox = (box, rgb) => {
114
- const [x1, y1, x2, y2] = box.map(Math.round);
115
- for (let x = x1; x <= x2; x++)
116
- for (let k = 0; k < thickness; k++) {
117
- setPx(x, y1 + k, rgb[0], rgb[1], rgb[2]);
118
- setPx(x, y2 - k, rgb[0], rgb[1], rgb[2]);
119
- }
120
- for (let y = y1; y <= y2; y++)
121
- for (let k = 0; k < thickness; k++) {
122
- setPx(x1 + k, y, rgb[0], rgb[1], rgb[2]);
123
- setPx(x2 - k, y, rgb[0], rgb[1], rgb[2]);
124
- }
125
- };
110
+ export function annotateScene(img, faces, objects, horizonY) {
111
+ const { width: w, height: h } = img;
112
+ const data = Uint8Array.from(img.data);
113
+ const out = { data, width: w, height: h };
114
+ const thickness = annotationThickness(out);
126
115
  for (const o of objects)
127
- drawBox(o, [255, 0, 220]);
116
+ drawBoxOutline(out, o, ANNOTATION_RGB.object, thickness);
128
117
  for (const f of faces)
129
- drawBox(f, [0, 220, 255]);
118
+ drawBoxOutline(out, f, ANNOTATION_RGB.face, thickness);
130
119
  if (horizonY !== null && horizonY >= 0 && horizonY < h) {
131
- for (let x = 0; x < w; x++)
132
- for (let k = 0; k < thickness; k++)
133
- setPx(x, horizonY + k, 255, 230, 0);
120
+ drawHLine(out, horizonY, ANNOTATION_RGB.horizon, thickness);
134
121
  }
135
- return encode({ data, width: w, height: h }, 88).data;
122
+ return encode({ data, width: w, height: h }, ANNOTATED_PREVIEW_JPEG_QUALITY).data;
136
123
  }
137
124
  function summarizeScene(model) {
138
125
  const subjLabels = new Map();
@@ -151,7 +138,7 @@ function summarizeScene(model) {
151
138
  parts.push(`main subject in the ${cell.row}-${cell.col} third`);
152
139
  return `Scene (${model.doc.width}×${model.doc.height}): ${parts.join('; ')}. Select named regions with ps_select_by_reference.`;
153
140
  }
154
- async function scene(connection, snippet, client, rawArgs, proRefine, precomputeFace) {
141
+ async function scene(connection, snippet, client, rawArgs, proRefine, hasPro = false, detectDeps) {
155
142
  try {
156
143
  const args = validateArgs(sceneSchema, rawArgs);
157
144
  const annotate = args.annotate ?? true;
@@ -160,23 +147,31 @@ async function scene(connection, snippet, client, rawArgs, proRefine, precompute
160
147
  const built = await buildSceneModel(connection, snippet, client, {
161
148
  useCache: !refresh,
162
149
  maxDimension: args.max_dimension,
150
+ detectDeps,
163
151
  });
164
152
  const model = built.model;
165
153
  let regions = [];
154
+ let precomputeOk = true;
166
155
  if (saveRegions) {
167
156
  try {
168
- regions = await precomputeRegions(connection, snippet, model, toCompositionContext(args.composition_context), { force: refresh, proRefine, skyCtx: skyCtxFrom(built), precomputeFace });
157
+ regions = await precomputeRegions(connection, snippet, model, toCompositionContext(args.composition_context), {
158
+ force: refresh,
159
+ proRefine,
160
+ skyCtx: skyCtxFrom(built),
161
+ faceMenu: faceMenuFor(model, hasPro),
162
+ });
169
163
  }
170
164
  catch {
165
+ precomputeOk = false;
171
166
  }
172
167
  }
173
168
  const content = [];
174
- if (annotate) {
169
+ if (annotate && built.decoded) {
175
170
  try {
176
171
  const exportH = built.exportImage.height || 0;
177
172
  const sy = model.doc.height > 0 ? exportH / model.doc.height : 1;
178
173
  const horizonExportY = Math.round(model.horizon.y * sy);
179
- const annotated = annotateScene(built.exportBytes, built.rawFaces, built.rawObjects.map((o) => o.bbox), horizonExportY);
174
+ const annotated = annotateScene(built.decoded, built.rawFaces, built.rawObjects.map((o) => o.bbox), horizonExportY);
180
175
  content.push({
181
176
  type: 'image',
182
177
  data: annotated.toString('base64'),
@@ -198,17 +193,35 @@ async function scene(connection, snippet, client, rawArgs, proRefine, precompute
198
193
  content,
199
194
  structuredContent: {
200
195
  ...model,
196
+ regions: reconcileRegions(model, regions, saveRegions && precomputeOk),
201
197
  region_menu: regions,
202
198
  },
203
199
  };
204
200
  }
205
201
  catch (error) {
206
- const msg = error instanceof Error ? error.message : String(error);
202
+ return toolErrorResult('Error reading scene', error);
203
+ }
204
+ }
205
+ function reconcileRegions(model, menu, resolved) {
206
+ return model.regions.map((r) => {
207
+ const base = r;
208
+ if (!resolved) {
209
+ return {
210
+ ...base,
211
+ coverage_is_estimate: true,
212
+ selectable: null,
213
+ selectable_state: 'not_resolved',
214
+ };
215
+ }
216
+ const hit = menu.find((m) => m.target === r.kind);
207
217
  return {
208
- content: [{ type: 'text', text: `Error reading scene: ${msg}` }],
209
- isError: true,
218
+ ...base,
219
+ coverage_is_estimate: true,
220
+ selectable: hit !== undefined,
221
+ selectable_state: hit !== undefined ? 'selectable' : 'not_selectable',
222
+ ...(hit ? { selectable_via: hit.method, selectable_confidence: hit.confidence } : {}),
210
223
  };
211
- }
224
+ });
212
225
  }
213
226
  const selectByReferenceSchema = {
214
227
  type: 'object',
@@ -272,7 +285,7 @@ function toCompositionContext(raw) {
272
285
  ctx.passThreshold = r.pass_threshold;
273
286
  return Object.keys(ctx).length ? ctx : undefined;
274
287
  }
275
- async function selectByReference(connection, snippet, client, rawArgs, proRefine) {
288
+ async function selectByReference(connection, snippet, client, rawArgs, proRefine, detectDeps) {
276
289
  try {
277
290
  const args = validateArgs(selectByReferenceSchema, rawArgs);
278
291
  const target = args.target;
@@ -294,7 +307,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
294
307
  passed: true,
295
308
  confidence: 1,
296
309
  reasons: ['loaded the saved scene:* channel by name'],
297
- selection_info: null,
310
+ selection_info: loaded.selection_info,
298
311
  detail: { from: 'precompute' },
299
312
  doc: { width: loaded.width, height: loaded.height },
300
313
  },
@@ -304,6 +317,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
304
317
  const built = await buildSceneModel(connection, snippet, client, {
305
318
  useCache: !refresh,
306
319
  maxDimension: args.max_dimension,
320
+ detectDeps,
307
321
  });
308
322
  const res = await resolveSelection(connection, snippet, built.model, target, {
309
323
  label: args.label,
@@ -312,6 +326,15 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
312
326
  proRefine,
313
327
  skyCtx: skyCtxFrom(built),
314
328
  });
329
+ if (res.passed && target.startsWith('face_')) {
330
+ try {
331
+ await saveSelectionAsSceneChannel(connection, target);
332
+ }
333
+ catch (err) {
334
+ logger.debug(`select_by_reference: could not persist ${CHANNEL_PREFIX}${target} — ` +
335
+ `${err instanceof Error ? err.message : String(err)}`);
336
+ }
337
+ }
315
338
  const conf = res.confidence.toFixed(2);
316
339
  const summary = res.passed
317
340
  ? `Selected "${target}" via ${res.method} (confidence ${conf}). Verify with ps_get_selection_preview.`
@@ -332,16 +355,14 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
332
355
  };
333
356
  }
334
357
  catch (error) {
335
- const msg = error instanceof Error ? error.message : String(error);
336
- return {
337
- content: [{ type: 'text', text: `Error in ps_select_by_reference: ${msg}` }],
338
- isError: true,
339
- };
358
+ return toolErrorResult('Error in ps_select_by_reference', error);
340
359
  }
341
360
  }
342
- export function createSceneTools(connection, snippetClient, client = new OnnxDetectionClient(), invokeTool) {
361
+ export function createSceneTools(connection, snippetClient, opts = {}) {
362
+ const { client = new OnnxDetectionClient(), invokeTool, detectDeps } = opts;
343
363
  const proRefine = invokeTool ? makeProRefine(invokeTool) : undefined;
344
- const precomputeFace = invokeTool ? makePrecomputeFace(invokeTool) : undefined;
364
+ const proMeshReachable = isToolAllowedInEdition('ps_select_face_feature', EDITION) || isProEntitled();
365
+ const hasPro = invokeTool !== undefined && proMeshReachable;
345
366
  return [
346
367
  {
347
368
  tool: {
@@ -354,7 +375,34 @@ export function createSceneTools(connection, snippetClient, client = new OnnxDet
354
375
  doc: { type: 'object' },
355
376
  subjects: { type: 'array', items: { type: 'object' } },
356
377
  faces: { type: 'array', items: { type: 'object' } },
357
- regions: { type: 'array', items: { type: 'object' } },
378
+ regions: {
379
+ type: 'array',
380
+ items: {
381
+ type: 'object',
382
+ properties: {
383
+ kind: { type: 'string' },
384
+ coverage: { type: 'number' },
385
+ coverage_is_estimate: {
386
+ type: 'boolean',
387
+ description: 'Always true today: `coverage` is the coarse histogram split, never a measured mask. It and the real selection routinely disagree (a night shot read sky at 0.08 while the gated selection found a real sky at 0.83) — trust `selectable_confidence` and the precomputed channel over this number. Carried as a field rather than left implicit so a future measured-coverage path can report false.',
388
+ },
389
+ selectable: {
390
+ type: ['boolean', 'null'],
391
+ description: 'Tristate as null when the region was never resolved — never read this as a plain boolean; branch on `selectable_state`.',
392
+ },
393
+ selectable_state: {
394
+ type: 'string',
395
+ enum: ['selectable', 'not_selectable', 'not_resolved'],
396
+ description: '`selectable`: a precomputed channel is ready to load. `not_selectable`: resolution ran and this region did not pass the confidence gate. `not_resolved`: precompute did not run or failed, so absence here is NOT evidence the region is unavailable.',
397
+ },
398
+ selectable_via: {
399
+ type: 'string',
400
+ description: 'The method that resolved it (only when selectable).',
401
+ },
402
+ selectable_confidence: { type: 'number' },
403
+ },
404
+ },
405
+ },
358
406
  region_menu: { type: 'array', items: { type: 'object' } },
359
407
  horizon: { type: 'object' },
360
408
  tonal_zones: { type: 'object' },
@@ -368,7 +416,7 @@ export function createSceneTools(connection, snippetClient, client = new OnnxDet
368
416
  idempotentHint: true,
369
417
  },
370
418
  },
371
- handler: async (args) => scene(connection, snippetClient, client, args, proRefine, precomputeFace),
419
+ handler: async (args) => scene(connection, snippetClient, client, args, proRefine, hasPro, detectDeps),
372
420
  },
373
421
  {
374
422
  tool: {
@@ -395,7 +443,7 @@ export function createSceneTools(connection, snippetClient, client = new OnnxDet
395
443
  openWorldHint: false,
396
444
  },
397
445
  },
398
- handler: async (args) => selectByReference(connection, snippetClient, client, args, proRefine),
446
+ handler: async (args) => selectByReference(connection, snippetClient, client, args, proRefine, detectDeps),
399
447
  },
400
448
  ];
401
449
  }