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,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',
@@ -23,7 +26,7 @@ const PRECOMPUTE_TARGETS = [
23
26
  ];
24
27
  function deleteSceneChannelsScript() {
25
28
  return `
26
- if (app.documents.length === 0) { throw new Error('No active document'); }
29
+ if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
27
30
  var doc = app.activeDocument;
28
31
  var removed = 0;
29
32
  for (var i = doc.channels.length - 1; i >= 0; i--) {
@@ -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,9 +59,50 @@ 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 document is open in Photoshop'); }
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
- if (app.documents.length === 0) { throw new Error('No active document'); }
95
+ // getSelectionInfo's own source begins with the restoreCompositeChannel
96
+ // definition (see _helpers.ts), so interpolating it here brings BOTH into
97
+ // scope. Interpolating restoreCompositeChannel separately as well emitted a
98
+ // second, byte-identical copy of that function body into every load script —
99
+ // legal (a later function declaration just wins) but pure waste in a string
100
+ // that crosses the COM/AppleScript boundary on every select_by_reference.
101
+ // Hoisted to the TOP so the restoreCompositeChannel(doc) call below sits
102
+ // after its definition textually, rather than relying on hoisting across an
103
+ // interpolation boundary.
104
+ ${getSelectionInfo}
105
+ if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
56
106
  var doc = app.activeDocument;
57
107
  var ch = null;
58
108
  for (var i = 0; i < doc.channels.length; i++) {
@@ -60,29 +110,52 @@ function loadNamedChannelScript(name) {
60
110
  }
61
111
  if (!ch) { return { loaded: false }; }
62
112
  doc.selection.load(ch, SelectionType.REPLACE);
63
- ${restoreCompositeChannel}
64
113
  restoreCompositeChannel(doc);
65
- return { loaded: true, width: doc.width.as('px'), height: doc.height.as('px') };
114
+ // Measure in the SAME round trip. The fast path used to return
115
+ // selection_info:null, which forced any caller that wanted to verify the mask
116
+ // into a second PS call — the one thing this path exists to avoid.
117
+ return {
118
+ loaded: true,
119
+ width: doc.width.as('px'),
120
+ height: doc.height.as('px'),
121
+ selection_info: getSelectionInfo()
122
+ };
66
123
  `;
67
124
  }
125
+ function countingConnection(connection, tally) {
126
+ return new Proxy(connection, {
127
+ get(target, prop, receiver) {
128
+ if (prop === 'executeScript') {
129
+ return (script, timeoutMs) => {
130
+ tally.scripts++;
131
+ return target.executeScript(script, timeoutMs);
132
+ };
133
+ }
134
+ return Reflect.get(target, prop, receiver);
135
+ },
136
+ });
137
+ }
68
138
  export async function precomputeRegions(connection, snippet, model, composition, opts = {}) {
69
139
  if (!opts.force &&
70
140
  lastPrecomputedKey !== null &&
71
- lastPrecomputedKey === model.provenance.cache_key) {
141
+ lastPrecomputedKey === model.provenance.cache_key &&
142
+ (await channelsExist(connection, cachedMenu.filter((m) => !m.on_demand).map((m) => m.key)))) {
72
143
  return cachedMenu;
73
144
  }
74
- await runScript(connection, deleteSceneChannelsScript());
145
+ const tally = { scripts: 0 };
146
+ const countedConnection = countingConnection(connection, tally);
147
+ await runScript(countedConnection, deleteSceneChannelsScript(), SCENE_CHANNEL_TIMEOUT_MS);
75
148
  const menu = [];
76
149
  for (const target of PRECOMPUTE_TARGETS) {
77
150
  try {
78
- const res = await resolveSelection(connection, snippet, model, target, {
151
+ const res = await resolveSelection(countedConnection, snippet, model, target, {
79
152
  composition,
80
153
  proRefine: opts.proRefine,
81
154
  skyCtx: opts.skyCtx,
82
155
  });
83
156
  if (res.passed) {
84
157
  const key = `${CHANNEL_PREFIX}${target}`;
85
- await runScript(connection, saveSelectionToNamedChannelScript(key));
158
+ await runScript(countedConnection, saveSelectionToNamedChannelScript(key), SCENE_CHANNEL_TIMEOUT_MS);
86
159
  menu.push({
87
160
  key,
88
161
  target,
@@ -97,26 +170,29 @@ export async function precomputeRegions(connection, snippet, model, composition,
97
170
  continue;
98
171
  }
99
172
  }
100
- if (opts.precomputeFace) {
101
- try {
102
- const faceItems = await opts.precomputeFace();
103
- menu.push(...faceItems);
104
- }
105
- catch {
106
- }
173
+ if (opts.faceMenu?.length) {
174
+ menu.push(...opts.faceMenu);
107
175
  }
108
176
  try {
109
- await runScript(connection, await snippet.build('deselect'));
177
+ await runScript(countedConnection, await snippet.build('deselect'));
110
178
  }
111
179
  catch {
112
180
  }
181
+ logger.debug(`precompute: ${PRECOMPUTE_TARGETS.length} targets, ${tally.scripts} scripts` +
182
+ ` (7 CE targets ≈ 21 scripts before the 2026-07-29 derive+measure merge → 15 after, ` +
183
+ ` then 14 once skin short-circuits with no person/face (2026-08-01), ` +
184
+ `on the all-fail path; production pass-paths differ)`);
113
185
  lastPrecomputedKey = model.provenance.cache_key;
114
186
  cachedMenu = menu;
115
187
  return menu;
116
188
  }
117
189
  export async function loadPrecomputedRegion(connection, target) {
118
- const r = (await runScript(connection, loadNamedChannelScript(`${CHANNEL_PREFIX}${target}`)));
190
+ const r = (await runScript(connection, loadNamedChannelScript(`${CHANNEL_PREFIX}${target}`), SCENE_CHANNEL_TIMEOUT_MS));
119
191
  if (r.loaded !== true)
120
192
  return null;
121
- return { width: r.width ?? 0, height: r.height ?? 0 };
193
+ return {
194
+ width: r.width ?? 0,
195
+ height: r.height ?? 0,
196
+ selection_info: r.selection_info ?? null,
197
+ };
122
198
  }
@@ -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 };