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.
Files changed (80) hide show
  1. package/README.md +8 -6
  2. package/dist/api/extendscript/_helpers.js +97 -13
  3. package/dist/api/photoshop-api.js +4 -3
  4. package/dist/api/snippet-client.js +38 -2
  5. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  6. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  7. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  8. package/dist/core/server.js +56 -180
  9. package/dist/delivery/client.js +50 -1
  10. package/dist/delivery/provision.js +18 -1
  11. package/dist/delivery/signing.js +38 -1
  12. package/dist/delivery/store.js +149 -16
  13. package/dist/detection/detect-active-doc.js +41 -10
  14. package/dist/detection/detection-client.js +3 -3
  15. package/dist/detection/face-detector.js +2 -2
  16. package/dist/detection/geometry.js +14 -0
  17. package/dist/detection/landmark-detection-client.js +3 -3
  18. package/dist/detection/mesh-face.js +2 -3
  19. package/dist/detection/object-detector.js +2 -2
  20. package/dist/detection/runtime.js +14 -3
  21. package/dist/detection/sam-segmenter.js +69 -26
  22. package/dist/kernel/kernel.js +5 -9
  23. package/dist/kernel/module-lifecycle.js +181 -0
  24. package/dist/license/entitlement.js +13 -3
  25. package/dist/license/store.js +8 -1
  26. package/dist/modules/ce/index.js +1 -1
  27. package/dist/perception/facets.js +19 -1
  28. package/dist/perception/grounding-anchors.js +2 -12
  29. package/dist/perception/grounding-locate.js +1 -9
  30. package/dist/perception/grounding-review-crop.js +4 -29
  31. package/dist/perception/overlay.js +62 -0
  32. package/dist/perception/pixel-identity.js +41 -0
  33. package/dist/perception/region-precompute.js +85 -17
  34. package/dist/perception/scene-model.js +93 -50
  35. package/dist/perception/select-recipes.js +107 -66
  36. package/dist/perception/sky-ground-flood.js +0 -5
  37. package/dist/perception/sky-mask-transfer.js +72 -13
  38. package/dist/platform/connection.js +42 -11
  39. package/dist/platform/executor-queue.js +68 -0
  40. package/dist/platform/macos-detector.js +2 -1
  41. package/dist/platform/macos-executor.js +18 -67
  42. package/dist/platform/run-child.js +6 -3
  43. package/dist/platform/windows-detector.js +14 -7
  44. package/dist/platform/windows-executor.js +9 -64
  45. package/dist/skills/editmamei-skill.zip +0 -0
  46. package/dist/tools/adjustment-tools.js +78 -130
  47. package/dist/tools/brush-tools.js +3 -14
  48. package/dist/tools/channel-compose-tools.js +4 -23
  49. package/dist/tools/detection-tools.js +59 -47
  50. package/dist/tools/document-tools.js +121 -131
  51. package/dist/tools/filter-tools.js +489 -730
  52. package/dist/tools/group-tools.js +90 -156
  53. package/dist/tools/guide-tools.js +52 -83
  54. package/dist/tools/history-tools.js +29 -63
  55. package/dist/tools/image-placement-tools.js +18 -37
  56. package/dist/tools/image-tools.js +23 -51
  57. package/dist/tools/layer-ordering-tools.js +2 -9
  58. package/dist/tools/layer-properties-tools.js +245 -384
  59. package/dist/tools/layer-tools.js +115 -181
  60. package/dist/tools/layer-transform-tools.js +41 -106
  61. package/dist/tools/metadata-tools.js +2 -9
  62. package/dist/tools/path-tools.js +3 -14
  63. package/dist/tools/portrait-tools.js +2 -5
  64. package/dist/tools/preview-tools.js +53 -65
  65. package/dist/tools/retouch-tools.js +32 -70
  66. package/dist/tools/scene-tools.js +94 -73
  67. package/dist/tools/selection-tools.js +306 -563
  68. package/dist/tools/shape-tools.js +18 -10
  69. package/dist/tools/text-tools.js +24 -57
  70. package/dist/tools/transform-canvas-tools.js +21 -54
  71. package/dist/tools/vector-mask-tools.js +2 -9
  72. package/dist/utils/jpeg-quality.js +1 -0
  73. package/dist/utils/logger.js +3 -0
  74. package/dist/utils/operation-timeouts.js +7 -0
  75. package/dist/utils/run-script.js +7 -2
  76. package/dist/utils/session-log.js +134 -12
  77. package/dist/utils/template-storage.js +5 -6
  78. package/dist/utils/tool-helpers.js +37 -0
  79. package/dist/version.js +1 -1
  80. package/package.json +1 -1
@@ -1,7 +1,10 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
+ import { SCENE_CHANNEL_TIMEOUT_MS } from '../utils/operation-timeouts.js';
2
3
  import { jsLit } from '../utils/jsx.js';
3
- import { restoreCompositeChannel } from '../api/extendscript/_helpers.js';
4
+ import { restoreCompositeChannel, getSelectionInfo } from '../api/extendscript/_helpers.js';
5
+ import { Logger } from '../utils/logger.js';
4
6
  import { resolveSelection, } from './select-recipes.js';
7
+ const logger = new Logger('region-precompute');
5
8
  let lastPrecomputedKey = null;
6
9
  let cachedMenu = [];
7
10
  export function __resetPrecompute() {
@@ -10,7 +13,7 @@ export function __resetPrecompute() {
10
13
  }
11
14
  export const CHANNEL_PREFIX = 'scene:';
12
15
  export async function saveSelectionAsSceneChannel(connection, target) {
13
- await runScript(connection, saveSelectionToNamedChannelScript(`${CHANNEL_PREFIX}${target}`));
16
+ await runScript(connection, saveSelectionToNamedChannelScript(`${CHANNEL_PREFIX}${target}`), SCENE_CHANNEL_TIMEOUT_MS);
14
17
  }
15
18
  const PRECOMPUTE_TARGETS = [
16
19
  'sky',
@@ -36,6 +39,12 @@ function deleteSceneChannelsScript() {
36
39
  return { removed: removed };
37
40
  `;
38
41
  }
42
+ export async function purgeSceneChannels(connection) {
43
+ const r = (await runScript(connection, deleteSceneChannelsScript(), SCENE_CHANNEL_TIMEOUT_MS));
44
+ lastPrecomputedKey = null;
45
+ cachedMenu = [];
46
+ return r.removed ?? 0;
47
+ }
39
48
  function saveSelectionToNamedChannelScript(name) {
40
49
  return `
41
50
  var doc = app.activeDocument;
@@ -50,6 +59,37 @@ function saveSelectionToNamedChannelScript(name) {
50
59
  return { saved: true, name: ${jsLit(name)} };
51
60
  `;
52
61
  }
62
+ function channelsExistScript(names) {
63
+ const namesLit = `[${names.map((n) => jsLit(n)).join(', ')}]`;
64
+ return `
65
+ // __mcp_scene_chk__ cheap existence check for previously-saved scene:* channels
66
+ if (app.documents.length === 0) { throw new Error('No active document'); }
67
+ var doc = app.activeDocument;
68
+ var have = {};
69
+ for (var i = 0; i < doc.channels.length; i++) {
70
+ have[String(doc.channels[i].name)] = true;
71
+ }
72
+ var names = ${namesLit};
73
+ var allPresent = true;
74
+ for (var j = 0; j < names.length; j++) {
75
+ if (!have[names[j]]) { allPresent = false; break; }
76
+ }
77
+ return { all_present: allPresent };
78
+ `;
79
+ }
80
+ async function channelsExist(connection, names) {
81
+ if (names.length === 0)
82
+ return true;
83
+ try {
84
+ const r = (await runScript(connection, channelsExistScript(names)));
85
+ return r.all_present === true;
86
+ }
87
+ catch (err) {
88
+ const msg = err instanceof Error ? err.message : String(err);
89
+ logger.debug(`region-precompute: channelsExist check failed (${msg}) — treating as stale, rebuilding`);
90
+ return false;
91
+ }
92
+ }
53
93
  function loadNamedChannelScript(name) {
54
94
  return `
55
95
  if (app.documents.length === 0) { throw new Error('No active document'); }
@@ -62,27 +102,52 @@ function loadNamedChannelScript(name) {
62
102
  doc.selection.load(ch, SelectionType.REPLACE);
63
103
  ${restoreCompositeChannel}
64
104
  restoreCompositeChannel(doc);
65
- return { loaded: true, width: doc.width.as('px'), height: doc.height.as('px') };
105
+ // Measure in the SAME round trip. The fast path used to return
106
+ // selection_info:null, which forced any caller that wanted to verify the mask
107
+ // into a second PS call — the one thing this path exists to avoid.
108
+ ${getSelectionInfo}
109
+ return {
110
+ loaded: true,
111
+ width: doc.width.as('px'),
112
+ height: doc.height.as('px'),
113
+ selection_info: getSelectionInfo()
114
+ };
66
115
  `;
67
116
  }
117
+ function countingConnection(connection, tally) {
118
+ return new Proxy(connection, {
119
+ get(target, prop, receiver) {
120
+ if (prop === 'executeScript') {
121
+ return (script, timeoutMs) => {
122
+ tally.scripts++;
123
+ return target.executeScript(script, timeoutMs);
124
+ };
125
+ }
126
+ return Reflect.get(target, prop, receiver);
127
+ },
128
+ });
129
+ }
68
130
  export async function precomputeRegions(connection, snippet, model, composition, opts = {}) {
69
131
  if (!opts.force &&
70
132
  lastPrecomputedKey !== null &&
71
- lastPrecomputedKey === model.provenance.cache_key) {
133
+ lastPrecomputedKey === model.provenance.cache_key &&
134
+ (await channelsExist(connection, cachedMenu.filter((m) => !m.on_demand).map((m) => m.key)))) {
72
135
  return cachedMenu;
73
136
  }
74
- await runScript(connection, deleteSceneChannelsScript());
137
+ const tally = { scripts: 0 };
138
+ const countedConnection = countingConnection(connection, tally);
139
+ await runScript(countedConnection, deleteSceneChannelsScript(), SCENE_CHANNEL_TIMEOUT_MS);
75
140
  const menu = [];
76
141
  for (const target of PRECOMPUTE_TARGETS) {
77
142
  try {
78
- const res = await resolveSelection(connection, snippet, model, target, {
143
+ const res = await resolveSelection(countedConnection, snippet, model, target, {
79
144
  composition,
80
145
  proRefine: opts.proRefine,
81
146
  skyCtx: opts.skyCtx,
82
147
  });
83
148
  if (res.passed) {
84
149
  const key = `${CHANNEL_PREFIX}${target}`;
85
- await runScript(connection, saveSelectionToNamedChannelScript(key));
150
+ await runScript(countedConnection, saveSelectionToNamedChannelScript(key), SCENE_CHANNEL_TIMEOUT_MS);
86
151
  menu.push({
87
152
  key,
88
153
  target,
@@ -97,26 +162,29 @@ export async function precomputeRegions(connection, snippet, model, composition,
97
162
  continue;
98
163
  }
99
164
  }
100
- if (opts.precomputeFace) {
101
- try {
102
- const faceItems = await opts.precomputeFace();
103
- menu.push(...faceItems);
104
- }
105
- catch {
106
- }
165
+ if (opts.faceMenu?.length) {
166
+ menu.push(...opts.faceMenu);
107
167
  }
108
168
  try {
109
- await runScript(connection, await snippet.build('deselect'));
169
+ await runScript(countedConnection, await snippet.build('deselect'));
110
170
  }
111
171
  catch {
112
172
  }
173
+ logger.debug(`precompute: ${PRECOMPUTE_TARGETS.length} targets, ${tally.scripts} scripts` +
174
+ ` (7 CE targets ≈ 21 scripts before the 2026-07-29 derive+measure merge → 15 after, ` +
175
+ ` then 14 once skin short-circuits with no person/face (2026-08-01), ` +
176
+ `on the all-fail path; production pass-paths differ)`);
113
177
  lastPrecomputedKey = model.provenance.cache_key;
114
178
  cachedMenu = menu;
115
179
  return menu;
116
180
  }
117
181
  export async function loadPrecomputedRegion(connection, target) {
118
- const r = (await runScript(connection, loadNamedChannelScript(`${CHANNEL_PREFIX}${target}`)));
182
+ const r = (await runScript(connection, loadNamedChannelScript(`${CHANNEL_PREFIX}${target}`), SCENE_CHANNEL_TIMEOUT_MS));
119
183
  if (r.loaded !== true)
120
184
  return null;
121
- return { width: r.width ?? 0, height: r.height ?? 0 };
185
+ return {
186
+ width: r.width ?? 0,
187
+ height: r.height ?? 0,
188
+ selection_info: r.selection_info ?? null,
189
+ };
122
190
  }
@@ -1,36 +1,24 @@
1
- import { decode } from 'jpeg-js';
2
1
  import { runScript } from '../utils/run-script.js';
3
2
  import { getContextInfo } from '../api/extendscript/_helpers.js';
4
3
  import { detectActiveDoc } from '../detection/detect-active-doc.js';
4
+ import { computePixelIdentity, docKeyFrom, identityKeyString, samePixelIdentity, } from './pixel-identity.js';
5
+ import { Logger } from '../utils/logger.js';
5
6
  import { computeComposition, computeTonalZones, estimateHorizon, pickThresholdLevel, boxArea, toLumaHistogram, } from './facets.js';
7
+ const logger = new Logger('scene-model');
6
8
  let cache = null;
9
+ const NO_PIXEL_IDENTITY_COUNTER_WRAP = 1_000_000_000;
10
+ let noPixelIdentityCounter = 0;
11
+ function nextNoPixelIdentityId() {
12
+ noPixelIdentityCounter = (noPixelIdentityCounter + 1) % NO_PIXEL_IDENTITY_COUNTER_WRAP;
13
+ return noPixelIdentityCounter;
14
+ }
7
15
  export function __clearSceneCache() {
8
16
  cache = null;
9
17
  }
10
- async function readCacheKey(connection) {
11
- const script = `
12
- if (app.documents.length === 0) { throw new Error('No active document'); }
13
- var doc = app.activeDocument;
14
- var id = -1;
15
- try { id = doc.id; } catch (eId) {}
16
- var hs = -1;
17
- try { hs = doc.historyStates.length; } catch (eHs) {}
18
- return { doc_id: id, history_states: hs };
19
- `;
20
- const r = (await runScript(connection, script));
21
- return `${r.doc_id ?? -1}:${r.history_states ?? -1}`;
22
- }
23
- export function rowBrightnessProfile(exportBytes, strips = 64) {
24
- if (!exportBytes || exportBytes.length === 0)
25
- return undefined;
26
- let img;
27
- try {
28
- img = decode(exportBytes, { useTArray: true, formatAsRGBA: true });
29
- }
30
- catch {
18
+ export function rowBrightnessProfile(decoded, strips = 64) {
19
+ if (!decoded)
31
20
  return undefined;
32
- }
33
- const { width: w, height: h, data } = img;
21
+ const { width: w, height: h, data } = decoded;
34
22
  if (w <= 0 || h <= 0)
35
23
  return undefined;
36
24
  const R = Math.max(1, Math.min(strips, h));
@@ -50,24 +38,55 @@ export function rowBrightnessProfile(exportBytes, strips = 64) {
50
38
  }
51
39
  export async function buildSceneModel(connection, snippet, client, opts = {}) {
52
40
  const useCache = opts.useCache ?? true;
53
- const key = await readCacheKey(connection);
54
- if (useCache && cache && cache.key === key) {
55
- const cached = cache.result;
56
- return {
57
- ...cached,
58
- model: {
59
- ...cached.model,
60
- provenance: { ...cached.model.provenance, cached: true },
61
- },
62
- };
63
- }
41
+ let hit = null;
42
+ let freshIdentity = null;
64
43
  const det = await detectActiveDoc(connection, client, {
65
44
  faces: true,
66
45
  objects: true,
67
46
  maxDimension: opts.maxDimension,
68
47
  objectThreshold: opts.objectThreshold,
69
48
  faceThreshold: opts.faceThreshold,
70
- });
49
+ shouldDetect: ({ decoded, context, docWidth, docHeight }) => {
50
+ if (!decoded)
51
+ return true;
52
+ const identity = computePixelIdentity(decoded, docKeyFrom(context, docWidth, docHeight));
53
+ freshIdentity = identity;
54
+ if (useCache && cache && samePixelIdentity(cache.identity, identity)) {
55
+ hit = cache;
56
+ return false;
57
+ }
58
+ return true;
59
+ },
60
+ }, opts.detectDeps);
61
+ if (hit) {
62
+ const cached = hit;
63
+ logger.debug(`scene-model: pixel-identity cache HIT (${cached.cacheKey}) — reusing detection + ` +
64
+ `facets, skipping ONNX + histogram + facet rebuild`);
65
+ const model = {
66
+ doc: { width: det.docWidth, height: det.docHeight },
67
+ subjects: cached.subjects,
68
+ faces: cached.faces,
69
+ regions: cached.regions,
70
+ horizon: cached.horizon,
71
+ tonal_zones: cached.tonalZones,
72
+ composition: cached.composition,
73
+ provenance: {
74
+ backends: cached.backends,
75
+ edition: 'ce',
76
+ cache_key: cached.cacheKey,
77
+ cached: true,
78
+ },
79
+ };
80
+ return {
81
+ model,
82
+ exportBytes: det.exportBytes,
83
+ decoded: det.decoded,
84
+ context: det.context,
85
+ rawFaces: cached.rawFaces,
86
+ rawObjects: cached.rawObjects,
87
+ exportImage: cached.exportImage,
88
+ };
89
+ }
71
90
  const docW = det.docWidth;
72
91
  const docH = det.docHeight;
73
92
  const objects = det.result.objects ?? [];
@@ -105,8 +124,11 @@ export async function buildSceneModel(connection, snippet, client, opts = {}) {
105
124
  const hist = toLumaHistogram(histRaw);
106
125
  const pick = pickThresholdLevel(hist);
107
126
  const tonalZones = computeTonalZones(hist);
108
- const rowMeans = rowBrightnessProfile(det.exportBytes);
109
- const horizon = estimateHorizon(rowMeans, docH, pick);
127
+ const allBoxes = objects.map((o) => o.bbox);
128
+ const mainBox = mainIdx >= 0 ? objects[mainIdx].bbox : null;
129
+ const primaryFaceBox = faces.find((f) => f.is_primary)?.bbox ?? faces[0]?.bbox ?? null;
130
+ const rowMeans = rowBrightnessProfile(det.decoded);
131
+ const horizon = estimateHorizon(rowMeans, docH, pick, mainBox, primaryFaceBox);
110
132
  const skyCoverage = pick.bright_fraction;
111
133
  const regions = [
112
134
  {
@@ -120,9 +142,18 @@ export async function buildSceneModel(connection, snippet, client, opts = {}) {
120
142
  recipe: { kind: 'posterize_region', levels: 3, sample: 'below_horizon' },
121
143
  },
122
144
  ];
123
- const allBoxes = objects.map((o) => o.bbox);
124
- const mainBox = mainIdx >= 0 ? objects[mainIdx].bbox : null;
125
145
  const composition = computeComposition(mainBox, allBoxes, docW, docH, horizon.placement);
146
+ const backends = {
147
+ faces: det.result.backends.faces,
148
+ objects: det.result.backends.objects,
149
+ regions: 'heuristic',
150
+ };
151
+ const rawFaces = (det.raw.faces ?? []).map((f) => f.bbox);
152
+ const rawObjects = (det.raw.objects ?? []).map((o) => ({ label: o.label, bbox: o.bbox }));
153
+ const exportImage = det.raw.image;
154
+ const cacheKey = freshIdentity
155
+ ? identityKeyString(freshIdentity)
156
+ : `nopixel-${nextNoPixelIdentityId()}`;
126
157
  const model = {
127
158
  doc: { width: docW, height: docH },
128
159
  subjects,
@@ -132,25 +163,37 @@ export async function buildSceneModel(connection, snippet, client, opts = {}) {
132
163
  tonal_zones: tonalZones,
133
164
  composition,
134
165
  provenance: {
135
- backends: {
136
- faces: det.result.backends.faces,
137
- objects: det.result.backends.objects,
138
- regions: 'heuristic',
139
- },
166
+ backends,
140
167
  edition: 'ce',
141
- cache_key: key,
168
+ cache_key: cacheKey,
142
169
  cached: false,
143
170
  },
144
171
  };
145
172
  const result = {
146
173
  model,
147
174
  exportBytes: det.exportBytes,
148
- rawFaces: (det.raw.faces ?? []).map((f) => f.bbox),
149
- rawObjects: (det.raw.objects ?? []).map((o) => ({ label: o.label, bbox: o.bbox })),
150
- exportImage: det.raw.image,
175
+ decoded: det.decoded,
176
+ rawFaces,
177
+ rawObjects,
178
+ exportImage,
151
179
  context: det.context,
152
180
  };
153
- cache = { key, result };
181
+ if (freshIdentity) {
182
+ cache = {
183
+ identity: freshIdentity,
184
+ cacheKey,
185
+ subjects,
186
+ faces,
187
+ regions,
188
+ horizon,
189
+ tonalZones,
190
+ composition,
191
+ backends,
192
+ rawFaces,
193
+ rawObjects,
194
+ exportImage,
195
+ };
196
+ }
154
197
  return result;
155
198
  }
156
199
  export { getContextInfo };