editmamei 1.2.0 → 1.3.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/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/help.js +1 -0
- package/dist/cli/repair.js +11 -3
- package/dist/core/server.js +9 -3
- package/dist/core/tool-groups.js +1 -0
- package/dist/core/tool-registry.js +3 -0
- package/dist/core/tool-tiers.js +2 -1
- package/dist/delivery/provision.js +21 -3
- package/dist/kernel/kernel.js +2 -1
- package/dist/kernel/module-lifecycle.js +54 -8
- package/dist/modules/ce/index.js +1 -1
- package/dist/perception/grounding-locate.js +7 -5
- package/dist/perception/region-precompute.js +39 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/document-tools.js +142 -2
- package/dist/tools/image-tools.js +2 -1
- package/dist/tools/layer-transform-tools.js +2 -1
- package/dist/tools/path-tools.js +1 -1
- package/dist/tools/scene-tools.js +45 -21
- package/dist/tools/selection-tools.js +1 -1
- package/dist/tools/shape-tools.js +1 -1
- package/dist/utils/session-log.js +13 -0
- package/dist/version.js +1 -1
- package/package.json +4 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/help.js
CHANGED
package/dist/cli/repair.js
CHANGED
|
@@ -26,11 +26,19 @@ export async function runRepair(opts = {}) {
|
|
|
26
26
|
out(` Installed ${m.sku} module v${m.version}.\n`);
|
|
27
27
|
for (const s of prov.skipped)
|
|
28
28
|
out(` Skipped ${s.sku} v${s.version}: ${s.reason}.\n`);
|
|
29
|
-
|
|
29
|
+
const tooNew = prov.errors.filter((e) => e.code === 'abi_too_new');
|
|
30
|
+
const failures = prov.errors.filter((e) => e.code !== 'abi_too_new');
|
|
31
|
+
for (const e of tooNew) {
|
|
32
|
+
out(` The published ${e.sku} module needs a newer Editmamei than this one — ` +
|
|
33
|
+
`update Editmamei to load it. Your installed module is unchanged.\n`);
|
|
34
|
+
}
|
|
35
|
+
for (const e of failures) {
|
|
30
36
|
err(` Error: could not provision the ${e.sku} module: ${e.message}\n`);
|
|
31
37
|
}
|
|
32
|
-
if (
|
|
38
|
+
if (failures.length > 0) {
|
|
33
39
|
throw new Error('module re-provisioning failed');
|
|
34
40
|
}
|
|
35
|
-
|
|
41
|
+
if (prov.installed.length > 0 || tooNew.length === 0) {
|
|
42
|
+
out('\nRestart your MCP client (Claude Desktop / Claude Code) to load Pro tools.\n');
|
|
43
|
+
}
|
|
36
44
|
}
|
package/dist/core/server.js
CHANGED
|
@@ -222,6 +222,7 @@ export class EditmameiServer {
|
|
|
222
222
|
toolRegistry: this.toolRegistry,
|
|
223
223
|
logger: this.logger,
|
|
224
224
|
assertToolsClassified: () => this.assertToolsClassified(),
|
|
225
|
+
classifyTool: (name) => this.classifyTool(name),
|
|
225
226
|
});
|
|
226
227
|
const proModule = this.moduleLifecycle.resolveProModule();
|
|
227
228
|
this.kernel = new Kernel({
|
|
@@ -252,10 +253,13 @@ export class EditmameiServer {
|
|
|
252
253
|
async ensureEntitledModuleFresh(delivery = {}) {
|
|
253
254
|
return this.moduleLifecycle.ensureEntitledModuleFresh(delivery);
|
|
254
255
|
}
|
|
256
|
+
classifyTool(name) {
|
|
257
|
+
tierOf(name);
|
|
258
|
+
groupOf(name);
|
|
259
|
+
}
|
|
255
260
|
assertToolsClassified() {
|
|
256
261
|
for (const tool of this.toolRegistry.list()) {
|
|
257
|
-
|
|
258
|
-
groupOf(tool.name);
|
|
262
|
+
this.classifyTool(tool.name);
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
265
|
listCapabilities() {
|
|
@@ -515,7 +519,9 @@ export class EditmameiServer {
|
|
|
515
519
|
}
|
|
516
520
|
return {
|
|
517
521
|
note: ` IMPORTANT — tell the user before continuing: Editmamei v${u.latest} is available` +
|
|
518
|
-
` (this install runs v${u.current}).${fixNote} To update: ${u.how_to_update}
|
|
522
|
+
` (this install runs v${u.current}).${fixNote} To update: ${u.how_to_update}` +
|
|
523
|
+
` Not urgent: they can also read and subscribe to release notes at` +
|
|
524
|
+
` https://editmamei.com/blog?src=update_notice`,
|
|
519
525
|
notify: true,
|
|
520
526
|
};
|
|
521
527
|
}
|
package/dist/core/tool-groups.js
CHANGED
|
@@ -93,6 +93,7 @@ export const TOOL_GROUPS = {
|
|
|
93
93
|
ps_compare_regions: 'verify',
|
|
94
94
|
ps_get_layer_bounds_diff: 'verify',
|
|
95
95
|
ps_get_selection_preview: 'verify',
|
|
96
|
+
ps_document: 'document',
|
|
96
97
|
ps_create_document: 'document',
|
|
97
98
|
ps_open_document: 'document',
|
|
98
99
|
ps_close_document: 'document',
|
package/dist/core/tool-tiers.js
CHANGED
|
@@ -8,6 +8,7 @@ export const TOOL_TIERS = {
|
|
|
8
8
|
ps_batch: 'pro',
|
|
9
9
|
ps_add_adjustment_layer: 'community',
|
|
10
10
|
ps_apply_adjustment: 'community',
|
|
11
|
+
ps_document: 'community',
|
|
11
12
|
ps_create_document: 'community',
|
|
12
13
|
ps_close_document: 'community',
|
|
13
14
|
ps_open_document: 'community',
|
|
@@ -68,7 +69,7 @@ export const TOOL_TIERS = {
|
|
|
68
69
|
ps_select_subject_instance: 'pro',
|
|
69
70
|
ps_select_object: 'pro',
|
|
70
71
|
ps_select_focus_area: 'dev',
|
|
71
|
-
ps_replace_sky: '
|
|
72
|
+
ps_replace_sky: 'community',
|
|
72
73
|
ps_modify_selection: 'community',
|
|
73
74
|
ps_get_selection_preview: 'community',
|
|
74
75
|
ps_selection_channel: 'community',
|
|
@@ -2,10 +2,11 @@ import { DeliveryClient, DeliveryError } from './client.js';
|
|
|
2
2
|
import { sha256Hex } from './crypto.js';
|
|
3
3
|
import { verifyModuleSignature, verifyModuleSignatureV2, digestsRootSha256Hex, isModuleFileDigestArray, } from './signing.js';
|
|
4
4
|
import { installModule, readInstalledModule } from './store.js';
|
|
5
|
+
import { KERNEL_ABI } from '../kernel/host-api.js';
|
|
5
6
|
import { Logger } from '../utils/logger.js';
|
|
6
7
|
const logger = new Logger('Modules');
|
|
7
8
|
const SKU_RE = /^[a-z0-9-]{2,32}$/;
|
|
8
|
-
const VERSION_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
9
|
+
export const VERSION_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
9
10
|
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
10
11
|
export async function provisionModules(key, opts = {}) {
|
|
11
12
|
const result = {
|
|
@@ -52,6 +53,23 @@ export async function provisionModules(key, opts = {}) {
|
|
|
52
53
|
});
|
|
53
54
|
continue;
|
|
54
55
|
}
|
|
56
|
+
const abi = entry.abi;
|
|
57
|
+
if (!Number.isInteger(abi) || abi < 1) {
|
|
58
|
+
result.errors.push({
|
|
59
|
+
sku,
|
|
60
|
+
code: 'abi_invalid',
|
|
61
|
+
message: `the manifest entry for ${sku} declares an invalid abi '${String(abi)}' (expected a whole number of at least 1) — refusing to install it`,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (abi > KERNEL_ABI) {
|
|
66
|
+
result.errors.push({
|
|
67
|
+
sku,
|
|
68
|
+
code: 'abi_too_new',
|
|
69
|
+
message: `the published ${sku} module needs a newer Editmamei than this one (module abi ${abi}, this host runs up to abi ${KERNEL_ABI}) — update Editmamei; the installed module is left as it is`,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
55
73
|
const installed = readInstalledModule(sku, opts);
|
|
56
74
|
if (installed && installed.version === latest && !opts.force) {
|
|
57
75
|
result.skipped.push({ sku, version: latest, reason: 'up-to-date' });
|
|
@@ -119,7 +137,7 @@ export async function provisionModules(key, opts = {}) {
|
|
|
119
137
|
installModule({
|
|
120
138
|
sku,
|
|
121
139
|
version: latest,
|
|
122
|
-
abi
|
|
140
|
+
abi,
|
|
123
141
|
sha256: digest,
|
|
124
142
|
alg: contentKey.alg,
|
|
125
143
|
content_key: contentKey.key,
|
|
@@ -137,7 +155,7 @@ export async function provisionModules(key, opts = {}) {
|
|
|
137
155
|
function errMsg(e) {
|
|
138
156
|
return e instanceof Error ? e.message : String(e);
|
|
139
157
|
}
|
|
140
|
-
function compareVersions(a, b) {
|
|
158
|
+
export function compareVersions(a, b) {
|
|
141
159
|
const [aCore, aPre] = a.split('-', 2);
|
|
142
160
|
const [bCore, bPre] = b.split('-', 2);
|
|
143
161
|
const ap = aCore.split('.').map(Number);
|
package/dist/kernel/kernel.js
CHANGED
|
@@ -73,9 +73,10 @@ export class Kernel {
|
|
|
73
73
|
throw new Error('downloaded module export is not an EditmameiModule (expected a default export with a manifest + register())');
|
|
74
74
|
}
|
|
75
75
|
if (this.abiTooNew(mod.manifest))
|
|
76
|
-
return;
|
|
76
|
+
return 'abi-too-new';
|
|
77
77
|
const before = this.registry.count();
|
|
78
78
|
await mod.register(this.hostApiFor(mod.manifest));
|
|
79
79
|
this.logger.info(`Loaded downloaded module '${mod.manifest.id}' (${mod.manifest.name}) — ${this.registry.count() - before} tools.`);
|
|
80
|
+
return 'loaded';
|
|
80
81
|
}
|
|
81
82
|
}
|
|
@@ -5,9 +5,10 @@ import { EDITION } from '../edition.js';
|
|
|
5
5
|
import { resolveProBinaryPath } from '../api/snippet-client.js';
|
|
6
6
|
import { isProEntitled } from '../license/entitlement.js';
|
|
7
7
|
import { loadVerifiedModule, readInstalledModule, installedPath, PRO_SKU, } from '../delivery/store.js';
|
|
8
|
-
import { provisionModules } from '../delivery/provision.js';
|
|
8
|
+
import { provisionModules, compareVersions, VERSION_RE, } from '../delivery/provision.js';
|
|
9
9
|
import { readLicense } from '../license/store.js';
|
|
10
|
-
import { HOST_MIN_ABI } from './host-api.js';
|
|
10
|
+
import { HOST_MIN_ABI, KERNEL_ABI } from './host-api.js';
|
|
11
|
+
import { VERSION } from '../version.js';
|
|
11
12
|
export function classifyModuleOutcome(inputs) {
|
|
12
13
|
if (inputs.proModuleLoaded && inputs.skipReason === null)
|
|
13
14
|
return 'loaded';
|
|
@@ -38,11 +39,12 @@ export class ModuleLifecycle {
|
|
|
38
39
|
if (isProEntitled()) {
|
|
39
40
|
const verified = loadVerifiedModule(PRO_SKU);
|
|
40
41
|
if (verified) {
|
|
41
|
-
const
|
|
42
|
+
const installed = readInstalledModule(PRO_SKU);
|
|
42
43
|
this._proModule = {
|
|
43
44
|
importer: () => import(pathToFileURL(verified.handlersPath).href),
|
|
44
45
|
binDir: verified.binDir,
|
|
45
|
-
abi,
|
|
46
|
+
abi: installed?.abi ?? null,
|
|
47
|
+
version: installed?.version ?? null,
|
|
46
48
|
};
|
|
47
49
|
return this._proModule;
|
|
48
50
|
}
|
|
@@ -61,6 +63,7 @@ export class ModuleLifecycle {
|
|
|
61
63
|
importer: () => import(inTreeProSpecifier),
|
|
62
64
|
binDir: dirname(resolveProBinaryPath()),
|
|
63
65
|
abi: null,
|
|
66
|
+
version: null,
|
|
64
67
|
};
|
|
65
68
|
return this._proModule;
|
|
66
69
|
}
|
|
@@ -76,13 +79,48 @@ export class ModuleLifecycle {
|
|
|
76
79
|
this._moduleSkipReason = 'incompatible';
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
82
|
+
if (this._proModule.abi !== null && this._proModule.abi > KERNEL_ABI) {
|
|
83
|
+
this.deps.logger.warn(`Pro module (abi ${this._proModule.abi}) needs a newer host than this build ` +
|
|
84
|
+
`(kernel abi ${KERNEL_ABI}) — booting Community; update Editmamei to load it.`);
|
|
85
|
+
this._moduleSkipReason = 'incompatible';
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
79
88
|
const snapshot = this.deps.toolRegistry.snapshot();
|
|
80
89
|
try {
|
|
81
|
-
await this.kernel.loadDownloaded(this._proModule.importer);
|
|
90
|
+
const outcome = await this.kernel.loadDownloaded(this._proModule.importer);
|
|
91
|
+
if (outcome === 'abi-too-new') {
|
|
92
|
+
this.deps.logger.warn(`The installed Pro module needs a newer host than this build (kernel abi ` +
|
|
93
|
+
`${KERNEL_ABI}) — booting Community; update Editmamei to load it.`);
|
|
94
|
+
this._moduleSkipReason = 'incompatible';
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this._proModule.version !== null &&
|
|
98
|
+
VERSION_RE.test(this._proModule.version) &&
|
|
99
|
+
compareVersions(this._proModule.version, VERSION) > 0) {
|
|
100
|
+
const added = this.deps.toolRegistry
|
|
101
|
+
.list()
|
|
102
|
+
.map((tool) => tool.name)
|
|
103
|
+
.filter((name) => !snapshot.has(name));
|
|
104
|
+
for (const name of added) {
|
|
105
|
+
try {
|
|
106
|
+
this.deps.classifyTool(name);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
this.deps.toolRegistry.unregister(name);
|
|
110
|
+
this.deps.logger.warn(`Module tool '${name}' is not recognized by this host — skipping it; the ` +
|
|
111
|
+
`rest of the module is loaded. Update Editmamei to use it.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
82
115
|
this.deps.assertToolsClassified();
|
|
83
116
|
}
|
|
84
117
|
catch (err) {
|
|
85
|
-
|
|
118
|
+
let changed = 0;
|
|
119
|
+
for (const tool of this.deps.toolRegistry.list()) {
|
|
120
|
+
const prior = snapshot.get(tool.name);
|
|
121
|
+
if (!prior || this.deps.toolRegistry.get(tool.name) !== prior)
|
|
122
|
+
changed++;
|
|
123
|
+
}
|
|
86
124
|
this.deps.toolRegistry.restore(snapshot);
|
|
87
125
|
this.deps.logger.warn(`Pro module could not be loaded on this host — booting Community and rolling back ` +
|
|
88
126
|
`${changed} module tool change(s); will re-provision in the background: ` +
|
|
@@ -133,11 +171,14 @@ export class ModuleLifecycle {
|
|
|
133
171
|
this.deps.logger.warn('Module delivery is not configured — staying Community.');
|
|
134
172
|
return;
|
|
135
173
|
}
|
|
174
|
+
const abiTooNew = prov.errors.some((e) => e.code === 'abi_too_new');
|
|
136
175
|
for (const e of prov.errors) {
|
|
176
|
+
if (e.code === 'abi_too_new')
|
|
177
|
+
continue;
|
|
137
178
|
this.deps.logger.warn(`Could not re-provision the ${e.sku} module (staying Community): ${e.message}`);
|
|
138
179
|
}
|
|
139
|
-
if (prov.errors.length === 0) {
|
|
140
|
-
if (reason === 'incompatible') {
|
|
180
|
+
if (prov.errors.length === 0 || abiTooNew) {
|
|
181
|
+
if (reason === 'incompatible' || abiTooNew) {
|
|
141
182
|
this.deps.logger.warn('The published Pro module does not yet support this host version — staying ' +
|
|
142
183
|
'Community. Update Editmamei when a compatible release ships; ' +
|
|
143
184
|
'`editmamei report` files a diagnostic.');
|
|
@@ -175,6 +216,11 @@ export class ModuleLifecycle {
|
|
|
175
216
|
this.deps.logger.info(`Pro module updated to v${m.version} — restart to load.`);
|
|
176
217
|
}
|
|
177
218
|
for (const e of prov.errors) {
|
|
219
|
+
if (e.code === 'abi_too_new') {
|
|
220
|
+
this.deps.logger.info(`A newer Pro module is published but needs a newer Editmamei than this ` +
|
|
221
|
+
`one — staying on the installed version, which works. Update Editmamei to get it.`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
178
224
|
this.deps.logger.warn(`Pro-module freshness check could not provision the ${e.sku} module ` +
|
|
179
225
|
`(staying on the installed version): ${e.message}`);
|
|
180
226
|
}
|
package/dist/modules/ce/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import { createVectorMaskTools } from '../../tools/vector-mask-tools.js';
|
|
|
30
30
|
import { createChannelComposeTools } from '../../tools/channel-compose-tools.js';
|
|
31
31
|
import { createShapeTools } from '../../tools/shape-tools.js';
|
|
32
32
|
import { createSkyTools } from '../../tools/sky-tools.js';
|
|
33
|
-
const ceFactories = [
|
|
33
|
+
export const ceFactories = [
|
|
34
34
|
createDocumentTools,
|
|
35
35
|
createLayerTools,
|
|
36
36
|
createGroupTools,
|
|
@@ -254,17 +254,19 @@ export const PLACEMENT_SCHEMA = {
|
|
|
254
254
|
type: 'object',
|
|
255
255
|
description: 'ANCHOR-RELATIONAL placement (preferred over guessing pixels): name anchors + a relation and the ' +
|
|
256
256
|
'spatial-grounding resolver computes the geometry, verified by an objective gate; the action runs ONLY if the ' +
|
|
257
|
-
'gate PASSES. For a HARD or ambiguous placement, first concur
|
|
258
|
-
'
|
|
259
|
-
'
|
|
257
|
+
'gate PASSES. For a HARD or ambiguous placement, first concur with the placement-resolver tool — when this ' +
|
|
258
|
+
'build has one, it appears in tools/list — by calling it with review_crop: true for a read-only zoomed crop ' +
|
|
259
|
+
'with a marker at the resolved spot, then pass the SAME anchors + relation here. The full anchors + relation ' +
|
|
260
|
+
'vocabulary is documented on that tool.',
|
|
260
261
|
properties: {
|
|
261
262
|
anchors: {
|
|
262
263
|
type: 'array',
|
|
263
|
-
description: 'Named anchors, same vocabulary as
|
|
264
|
+
description: 'Named anchors, same vocabulary as the placement-resolver tool, when this build has one ' +
|
|
265
|
+
'(face/object/grid/extremum/corner/edge/landmark).',
|
|
264
266
|
},
|
|
265
267
|
relation: {
|
|
266
268
|
type: 'object',
|
|
267
|
-
description: 'The relation, same vocabulary as
|
|
269
|
+
description: 'The relation, same vocabulary as the placement-resolver tool, when this build has one.',
|
|
268
270
|
},
|
|
269
271
|
max_dimension: {
|
|
270
272
|
type: 'number',
|
|
@@ -10,11 +10,24 @@ let cachedMenu = [];
|
|
|
10
10
|
export function __resetPrecompute() {
|
|
11
11
|
lastPrecomputedKey = null;
|
|
12
12
|
cachedMenu = [];
|
|
13
|
+
channelsDocState = null;
|
|
13
14
|
}
|
|
14
15
|
export const CHANNEL_PREFIX = 'scene:';
|
|
15
16
|
export async function saveSelectionAsSceneChannel(connection, target) {
|
|
16
17
|
await runScript(connection, saveSelectionToNamedChannelScript(`${CHANNEL_PREFIX}${target}`), SCENE_CHANNEL_TIMEOUT_MS);
|
|
17
18
|
}
|
|
19
|
+
let channelsDocState = null;
|
|
20
|
+
export async function invalidateSceneChannelsIfStale(connection, cacheKey) {
|
|
21
|
+
if (channelsDocState === cacheKey)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
await runScript(connection, deleteSceneChannelsScript(), SCENE_CHANNEL_TIMEOUT_MS);
|
|
25
|
+
channelsDocState = cacheKey;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
channelsDocState = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
18
31
|
const PRECOMPUTE_TARGETS = [
|
|
19
32
|
'sky',
|
|
20
33
|
'ground',
|
|
@@ -24,6 +37,31 @@ const PRECOMPUTE_TARGETS = [
|
|
|
24
37
|
'subject',
|
|
25
38
|
'face',
|
|
26
39
|
];
|
|
40
|
+
export function candidateMenu(model) {
|
|
41
|
+
const advertise = (target) => ({
|
|
42
|
+
key: `${CHANNEL_PREFIX}${target}`,
|
|
43
|
+
target,
|
|
44
|
+
method: 'on_demand',
|
|
45
|
+
bounds: null,
|
|
46
|
+
on_demand: true,
|
|
47
|
+
});
|
|
48
|
+
const hasFace = model.faces.length > 0;
|
|
49
|
+
const hasSubject = model.subjects.length > 0;
|
|
50
|
+
const hasPerson = model.subjects.some((s) => s.label === 'person');
|
|
51
|
+
const menu = [
|
|
52
|
+
advertise('sky'),
|
|
53
|
+
advertise('ground'),
|
|
54
|
+
advertise('shadows'),
|
|
55
|
+
advertise('highlights'),
|
|
56
|
+
];
|
|
57
|
+
if (hasPerson || hasFace)
|
|
58
|
+
menu.push(advertise('skin'));
|
|
59
|
+
if (hasSubject)
|
|
60
|
+
menu.push(advertise('subject'));
|
|
61
|
+
if (hasFace)
|
|
62
|
+
menu.push(advertise('face'));
|
|
63
|
+
return menu;
|
|
64
|
+
}
|
|
27
65
|
function deleteSceneChannelsScript() {
|
|
28
66
|
return `
|
|
29
67
|
if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
|
|
@@ -145,6 +183,7 @@ export async function precomputeRegions(connection, snippet, model, composition,
|
|
|
145
183
|
const tally = { scripts: 0 };
|
|
146
184
|
const countedConnection = countingConnection(connection, tally);
|
|
147
185
|
await runScript(countedConnection, deleteSceneChannelsScript(), SCENE_CHANNEL_TIMEOUT_MS);
|
|
186
|
+
channelsDocState = model.provenance.cache_key;
|
|
148
187
|
const menu = [];
|
|
149
188
|
for (const target of PRECOMPUTE_TARGETS) {
|
|
150
189
|
try {
|
|
Binary file
|
|
@@ -36,6 +36,17 @@ const createDocumentSchema = {
|
|
|
36
36
|
},
|
|
37
37
|
required: ['width', 'height'],
|
|
38
38
|
};
|
|
39
|
+
const documentTargetProps = {
|
|
40
|
+
name: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: "Target an open document by its exact Photoshop name, INCLUDING the extension as shown in the tab (e.g. 'portrait.jpg', not 'portrait'). If two open documents share a name the call fails rather than guessing — target by id instead.",
|
|
43
|
+
},
|
|
44
|
+
id: {
|
|
45
|
+
type: 'integer',
|
|
46
|
+
minimum: 1,
|
|
47
|
+
description: 'Target an open document by its Photoshop document id. Unambiguous — prefer this when names collide.',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
39
50
|
const closeDocumentSchema = {
|
|
40
51
|
type: 'object',
|
|
41
52
|
properties: {
|
|
@@ -44,8 +55,22 @@ const closeDocumentSchema = {
|
|
|
44
55
|
description: 'Whether to save changes before closing',
|
|
45
56
|
default: false,
|
|
46
57
|
},
|
|
58
|
+
...documentTargetProps,
|
|
47
59
|
},
|
|
48
60
|
};
|
|
61
|
+
const DOCUMENT_OPS = ['list', 'activate'];
|
|
62
|
+
const documentSchema = {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
op: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
enum: [...DOCUMENT_OPS],
|
|
68
|
+
description: 'list: every open document (index, id, name, path, saved, active, dimensions) — safe to call when NOTHING is open, which is the point. activate: make one of them the active document, by name or id.',
|
|
69
|
+
},
|
|
70
|
+
...documentTargetProps,
|
|
71
|
+
},
|
|
72
|
+
required: ['op'],
|
|
73
|
+
};
|
|
49
74
|
const openDocumentSchema = {
|
|
50
75
|
type: 'object',
|
|
51
76
|
properties: {
|
|
@@ -180,10 +205,56 @@ export function createDocumentTools(connection, snippetClient) {
|
|
|
180
205
|
},
|
|
181
206
|
handler: async (args) => createDocument(connection, snippetClient, args),
|
|
182
207
|
},
|
|
208
|
+
{
|
|
209
|
+
tool: {
|
|
210
|
+
name: 'ps_document',
|
|
211
|
+
description: "See and steer WHICH documents are open, without touching their content. op=list answers 'what is open, which one is active, and does it have unsaved changes' — and it is the one document tool that works when nothing is open at all, so it is the recovery read after a 'No document is open' failure. op=activate switches the active document by name or id, which is how you fix having edited the wrong one. Read-only with respect to pixels; use ps_open_document to load a file and ps_close_document to close one.",
|
|
212
|
+
inputSchema: documentSchema,
|
|
213
|
+
outputSchema: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
op: { type: 'string' },
|
|
217
|
+
count: { type: 'number' },
|
|
218
|
+
documents: {
|
|
219
|
+
type: 'array',
|
|
220
|
+
items: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
index: { type: 'number' },
|
|
224
|
+
id: { type: 'number' },
|
|
225
|
+
name: { type: 'string' },
|
|
226
|
+
path: {
|
|
227
|
+
type: ['string', 'null'],
|
|
228
|
+
description: 'Absolute path, or null for a document never saved to disk.',
|
|
229
|
+
},
|
|
230
|
+
saved: {
|
|
231
|
+
type: ['boolean', 'null'],
|
|
232
|
+
description: 'False when the document has unsaved changes. Null when Photoshop would not report it.',
|
|
233
|
+
},
|
|
234
|
+
active: { type: 'boolean' },
|
|
235
|
+
width_px: { type: ['number', 'null'] },
|
|
236
|
+
height_px: { type: ['number', 'null'] },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
activated: { type: 'boolean' },
|
|
241
|
+
id: { type: 'number' },
|
|
242
|
+
name: { type: 'string' },
|
|
243
|
+
context: { type: 'object' },
|
|
244
|
+
},
|
|
245
|
+
required: ['op'],
|
|
246
|
+
},
|
|
247
|
+
annotations: {
|
|
248
|
+
title: 'List / Activate Documents',
|
|
249
|
+
idempotentHint: true,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
handler: async (args) => documentOp(connection, snippetClient, args),
|
|
253
|
+
},
|
|
183
254
|
{
|
|
184
255
|
tool: {
|
|
185
256
|
name: 'ps_close_document',
|
|
186
|
-
description: 'Close the active
|
|
257
|
+
description: 'Close a Photoshop document — the active one by default, or a specific one by name or id. Destructive if save=false and the document has unsaved changes. If two open documents share the requested name the call fails rather than guessing. Returns the closed document name plus a fresh context block (which document, if any, is active afterwards).',
|
|
187
258
|
inputSchema: closeDocumentSchema,
|
|
188
259
|
outputSchema: {
|
|
189
260
|
type: 'object',
|
|
@@ -340,11 +411,80 @@ async function createDocument(connection, snippetClient, rawArgs) {
|
|
|
340
411
|
successText: (_result, args) => `Document created: ${args.width}x${args.height}px at ${args.resolution}dpi (${args.color_mode})`,
|
|
341
412
|
});
|
|
342
413
|
}
|
|
414
|
+
function documentTargetArgs(args) {
|
|
415
|
+
const out = {};
|
|
416
|
+
if (typeof args.name === 'string' && args.name !== '')
|
|
417
|
+
out.name = args.name;
|
|
418
|
+
if (typeof args.id === 'number')
|
|
419
|
+
out.id = args.id;
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
function emptySelectorError(args) {
|
|
423
|
+
if (typeof args.name === 'string' && args.name === '') {
|
|
424
|
+
return 'name was an empty string. Pass a real document name, or omit name entirely to act on the active document.';
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
async function documentOp(connection, snippetClient, rawArgs) {
|
|
429
|
+
let errorPrefix = 'Error reading documents';
|
|
430
|
+
try {
|
|
431
|
+
const args = validateArgs(documentSchema, rawArgs);
|
|
432
|
+
const op = args.op;
|
|
433
|
+
const target = documentTargetArgs(args);
|
|
434
|
+
if (op === 'activate') {
|
|
435
|
+
errorPrefix = 'Error activating document';
|
|
436
|
+
const empty = emptySelectorError(args);
|
|
437
|
+
if (empty !== null)
|
|
438
|
+
return toolErrorResult(errorPrefix, new Error(empty));
|
|
439
|
+
if (Object.keys(target).length === 0) {
|
|
440
|
+
return toolErrorResult(errorPrefix, new Error('op=activate needs a name or an id. Call op=list to see what is open.'));
|
|
441
|
+
}
|
|
442
|
+
const script = await snippetClient.build('activateDocument', target);
|
|
443
|
+
const result = (await runScript(connection, script));
|
|
444
|
+
return {
|
|
445
|
+
content: [{ type: 'text', text: `Activated "${result.name}" (id ${result.id}).` }],
|
|
446
|
+
structuredContent: {
|
|
447
|
+
op,
|
|
448
|
+
activated: true,
|
|
449
|
+
id: result.id,
|
|
450
|
+
name: result.name,
|
|
451
|
+
context: result.context,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
const script = await snippetClient.build('listDocuments', {});
|
|
456
|
+
const result = (await runScript(connection, script));
|
|
457
|
+
const docs = result.documents ?? [];
|
|
458
|
+
const summary = docs.length
|
|
459
|
+
? `${docs.length} open document(s): ${docs
|
|
460
|
+
.map((d) => `${d.name} (id ${d.id}${d.active ? ', ACTIVE' : ''}${d.saved === false ? ', unsaved changes' : ''})`)
|
|
461
|
+
.join('; ')}.`
|
|
462
|
+
: 'No documents are open in Photoshop. Open one with ps_open_document, or create one with ps_create_document.';
|
|
463
|
+
return {
|
|
464
|
+
content: [{ type: 'text', text: summary }],
|
|
465
|
+
structuredContent: {
|
|
466
|
+
op,
|
|
467
|
+
count: docs.length,
|
|
468
|
+
documents: docs,
|
|
469
|
+
context: result.context,
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
return toolErrorResult(errorPrefix, error);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
343
477
|
async function closeDocument(connection, snippetClient, rawArgs) {
|
|
344
478
|
try {
|
|
345
479
|
const args = validateArgs(closeDocumentSchema, rawArgs);
|
|
346
480
|
const save = args.save;
|
|
347
|
-
const
|
|
481
|
+
const empty = emptySelectorError(args);
|
|
482
|
+
if (empty !== null)
|
|
483
|
+
return toolErrorResult('Error closing document', new Error(empty));
|
|
484
|
+
const script = await snippetClient.build('closeDocument', {
|
|
485
|
+
save,
|
|
486
|
+
...documentTargetArgs(args),
|
|
487
|
+
});
|
|
348
488
|
const result = (await runScript(connection, script));
|
|
349
489
|
return {
|
|
350
490
|
content: [
|
|
@@ -29,7 +29,8 @@ const cropDocumentSchema = {
|
|
|
29
29
|
...PLACEMENT_SCHEMA,
|
|
30
30
|
description: 'ANCHOR-RELATIONAL crop (preferred over guessing pixels): a REGION relation (inside/gap) → the crop is the ' +
|
|
31
31
|
'resolved region bounding box, verified by the gate. Crops ONLY if the gate PASSES. When set, ' +
|
|
32
|
-
'left/top/right/bottom are ignored. See
|
|
32
|
+
'left/top/right/bottom are ignored. See the placement-resolver tool, when this build has one, for the ' +
|
|
33
|
+
'anchors + relation vocabulary.',
|
|
33
34
|
},
|
|
34
35
|
left: {
|
|
35
36
|
type: 'integer',
|
|
@@ -82,7 +82,8 @@ const moveLayerSchema = {
|
|
|
82
82
|
description: 'ANCHOR-RELATIONAL move (preferred over guessing a pixel): a POINT relation (centroid/midpoint/offset) → ' +
|
|
83
83
|
'the layer\'s CENTER is moved to the resolved, gate-verified point (e.g. "center this layer on the detected ' +
|
|
84
84
|
'subject" / "…in the gap between the two people"). Moves ONLY if the gate PASSES. When set, delta_*/' +
|
|
85
|
-
'absolute_*/center_on_* are ignored. See
|
|
85
|
+
'absolute_*/center_on_* are ignored. See the placement-resolver tool, when this build has one, for the ' +
|
|
86
|
+
'anchors + relation vocabulary.',
|
|
86
87
|
},
|
|
87
88
|
},
|
|
88
89
|
};
|
package/dist/tools/path-tools.js
CHANGED
|
@@ -73,7 +73,7 @@ const pathInputSchema = {
|
|
|
73
73
|
tool: {
|
|
74
74
|
type: 'string',
|
|
75
75
|
enum: SUPPORTED_BRUSH_TOOLS,
|
|
76
|
-
description: "stroke only: which brush-family tool paints the path
|
|
76
|
+
description: "stroke only: which brush-family tool paints the path (see this field's own enum for the full supported set). Default 'brush'.",
|
|
77
77
|
default: 'brush',
|
|
78
78
|
},
|
|
79
79
|
color: {
|
|
@@ -4,7 +4,7 @@ import { OnnxDetectionClient } from '../detection/detection-client.js';
|
|
|
4
4
|
import { ANNOTATED_PREVIEW_JPEG_QUALITY } from '../utils/jpeg-quality.js';
|
|
5
5
|
import { buildSceneModel } from '../perception/scene-model.js';
|
|
6
6
|
import { resolveSelection, SELECT_REFERENCE_TARGETS, } from '../perception/select-recipes.js';
|
|
7
|
-
import { precomputeRegions, loadPrecomputedRegion, saveSelectionAsSceneChannel, CHANNEL_PREFIX, } from '../perception/region-precompute.js';
|
|
7
|
+
import { precomputeRegions, candidateMenu, invalidateSceneChannelsIfStale, loadPrecomputedRegion, saveSelectionAsSceneChannel, CHANNEL_PREFIX, } from '../perception/region-precompute.js';
|
|
8
8
|
import { toolErrorResult } from '../utils/tool-helpers.js';
|
|
9
9
|
import { Logger } from '../utils/logger.js';
|
|
10
10
|
import { isProEntitled } from '../license/entitlement.js';
|
|
@@ -54,7 +54,6 @@ function faceMenuFor(model, hasPro) {
|
|
|
54
54
|
key: `${CHANNEL_PREFIX}${target}`,
|
|
55
55
|
target,
|
|
56
56
|
method: 'face_mesh',
|
|
57
|
-
confidence: 1,
|
|
58
57
|
bounds: null,
|
|
59
58
|
on_demand: true,
|
|
60
59
|
}));
|
|
@@ -82,8 +81,8 @@ const sceneSchema = {
|
|
|
82
81
|
},
|
|
83
82
|
save_regions: {
|
|
84
83
|
type: 'boolean',
|
|
85
|
-
default:
|
|
86
|
-
description: '
|
|
84
|
+
default: false,
|
|
85
|
+
description: 'EAGERLY derive every region (sky/ground/shadows/highlights/skin/subject/face) up front and SAVE each confident one as a managed `scene:*` alpha channel, so the returned menu carries a verified method + confidence for each. Costs one derive per target — measured at ~21s on a 4898x3265 layered document, against a 30s script timeout — so it is OFF by default. Leave it off unless you specifically need every region scored in one call: the default advertises the same menu as `on_demand` entries and ps_select_by_reference derives whichever region you actually ask for (then saves its channel, so repeats of THAT region are instant). 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.',
|
|
87
86
|
},
|
|
88
87
|
composition_context: {
|
|
89
88
|
type: 'object',
|
|
@@ -143,7 +142,7 @@ async function scene(connection, snippet, client, rawArgs, proRefine, hasPro = f
|
|
|
143
142
|
const args = validateArgs(sceneSchema, rawArgs);
|
|
144
143
|
const annotate = args.annotate ?? true;
|
|
145
144
|
const refresh = args.refresh ?? false;
|
|
146
|
-
const saveRegions = args.save_regions ??
|
|
145
|
+
const saveRegions = args.save_regions ?? false;
|
|
147
146
|
const built = await buildSceneModel(connection, snippet, client, {
|
|
148
147
|
useCache: !refresh,
|
|
149
148
|
maxDimension: args.max_dimension,
|
|
@@ -165,6 +164,14 @@ async function scene(connection, snippet, client, rawArgs, proRefine, hasPro = f
|
|
|
165
164
|
precomputeOk = false;
|
|
166
165
|
}
|
|
167
166
|
}
|
|
167
|
+
else {
|
|
168
|
+
try {
|
|
169
|
+
await invalidateSceneChannelsIfStale(connection, model.provenance.cache_key);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
}
|
|
173
|
+
regions = [...candidateMenu(model), ...faceMenuFor(model, hasPro)];
|
|
174
|
+
}
|
|
168
175
|
const content = [];
|
|
169
176
|
if (annotate && built.decoded) {
|
|
170
177
|
try {
|
|
@@ -181,19 +188,25 @@ async function scene(connection, snippet, client, rawArgs, proRefine, hasPro = f
|
|
|
181
188
|
catch {
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
const named = (r) => `${r.target}${r.label ? `:${r.label}` : ''}`;
|
|
192
|
+
const menuText = !regions.length
|
|
193
|
+
? saveRegions
|
|
194
|
+
? ' No confident named regions detected here.'
|
|
195
|
+
: ''
|
|
196
|
+
: saveRegions
|
|
187
197
|
? ` Confident regions (select by name): ${regions
|
|
188
|
-
.map((r) => `${r
|
|
198
|
+
.map((r) => `${named(r)}${r.confidence === undefined ? '' : ` ${r.confidence.toFixed(2)}`}`)
|
|
189
199
|
.join(', ')}.`
|
|
190
|
-
:
|
|
200
|
+
:
|
|
201
|
+
` Selectable by name (each resolved when you ask for it, not yet scored): ${regions
|
|
202
|
+
.map(named)
|
|
203
|
+
.join(', ')}.`;
|
|
191
204
|
content.push({ type: 'text', text: summarizeScene(model) + menuText });
|
|
192
205
|
return {
|
|
193
206
|
content,
|
|
194
207
|
structuredContent: {
|
|
195
208
|
...model,
|
|
196
|
-
regions: reconcileRegions(model, regions, saveRegions
|
|
209
|
+
regions: reconcileRegions(model, regions, saveRegions ? (precomputeOk ? 'resolved' : 'unresolved') : 'candidate'),
|
|
197
210
|
region_menu: regions,
|
|
198
211
|
},
|
|
199
212
|
};
|
|
@@ -202,15 +215,23 @@ async function scene(connection, snippet, client, rawArgs, proRefine, hasPro = f
|
|
|
202
215
|
return toolErrorResult('Error reading scene', error);
|
|
203
216
|
}
|
|
204
217
|
}
|
|
205
|
-
|
|
218
|
+
export const SELECTABLE_STATES = [
|
|
219
|
+
'selectable',
|
|
220
|
+
'not_selectable',
|
|
221
|
+
'candidate',
|
|
222
|
+
'not_resolved',
|
|
223
|
+
];
|
|
224
|
+
function reconcileRegions(model, menu, mode) {
|
|
206
225
|
return model.regions.map((r) => {
|
|
207
226
|
const base = r;
|
|
208
|
-
if (
|
|
227
|
+
if (mode !== 'resolved') {
|
|
228
|
+
const advertised = mode === 'candidate' && menu.some((m) => m.target === r.kind);
|
|
209
229
|
return {
|
|
210
230
|
...base,
|
|
211
231
|
coverage_is_estimate: true,
|
|
212
232
|
selectable: null,
|
|
213
|
-
selectable_state: 'not_resolved',
|
|
233
|
+
selectable_state: advertised ? 'candidate' : 'not_resolved',
|
|
234
|
+
...(advertised ? { selectable_via: 'on_demand' } : {}),
|
|
214
235
|
};
|
|
215
236
|
}
|
|
216
237
|
const hit = menu.find((m) => m.target === r.kind);
|
|
@@ -290,14 +311,17 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
|
|
|
290
311
|
const args = validateArgs(selectByReferenceSchema, rawArgs);
|
|
291
312
|
const target = args.target;
|
|
292
313
|
const refresh = args.refresh ?? false;
|
|
293
|
-
|
|
314
|
+
const discriminated = args.label !== undefined ||
|
|
315
|
+
args.instance !== undefined ||
|
|
316
|
+
args.composition_context !== undefined;
|
|
317
|
+
if (!refresh && !discriminated) {
|
|
294
318
|
const loaded = await loadPrecomputedRegion(connection, target);
|
|
295
319
|
if (loaded) {
|
|
296
320
|
return {
|
|
297
321
|
content: [
|
|
298
322
|
{
|
|
299
323
|
type: 'text',
|
|
300
|
-
text: `Selected "${target}" from the saved scene:${target} channel (
|
|
324
|
+
text: `Selected "${target}" from the saved scene:${target} channel (cached by an earlier derive). If the image changed since, re-run with refresh:true. Verify with ps_get_selection_preview.`,
|
|
301
325
|
},
|
|
302
326
|
],
|
|
303
327
|
structuredContent: {
|
|
@@ -326,7 +350,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
|
|
|
326
350
|
proRefine,
|
|
327
351
|
skyCtx: skyCtxFrom(built),
|
|
328
352
|
});
|
|
329
|
-
if (res.passed &&
|
|
353
|
+
if (res.passed && !discriminated) {
|
|
330
354
|
try {
|
|
331
355
|
await saveSelectionAsSceneChannel(connection, target);
|
|
332
356
|
}
|
|
@@ -392,12 +416,12 @@ export function createSceneTools(connection, snippetClient, opts = {}) {
|
|
|
392
416
|
},
|
|
393
417
|
selectable_state: {
|
|
394
418
|
type: 'string',
|
|
395
|
-
enum: [
|
|
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.',
|
|
419
|
+
enum: [...SELECTABLE_STATES],
|
|
420
|
+
description: '`selectable`: a precomputed channel is ready to load. `not_selectable`: resolution ran and this region did not pass the confidence gate. `candidate`: the DEFAULT read advertised this region without deriving it — ps_select_by_reference scores it when you ask, and it may still turn out not to pass. `not_resolved`: an eagerly-requested precompute did not run or failed, so absence here is NOT evidence the region is unavailable.',
|
|
397
421
|
},
|
|
398
422
|
selectable_via: {
|
|
399
423
|
type: 'string',
|
|
400
|
-
description:
|
|
424
|
+
description: "The method that resolved it, when one did. Reads 'on_demand' for a `candidate` — nothing has resolved it yet and the method is chosen at derive time.",
|
|
401
425
|
},
|
|
402
426
|
selectable_confidence: { type: 'number' },
|
|
403
427
|
},
|
|
@@ -421,7 +445,7 @@ export function createSceneTools(connection, snippetClient, opts = {}) {
|
|
|
421
445
|
{
|
|
422
446
|
tool: {
|
|
423
447
|
name: 'ps_select_by_reference',
|
|
424
|
-
description: 'Select a region by NAME instead of coordinates — the natural-mask alternative to a rectangle — with a CONFIDENCE GATE. target=sky/ground/foliage/subject/face/shadows/highlights/skin/above_horizon resolves through the right Photoshop-native method (threshold for sky, invert-sky−subjects for ground, luminance for shadows/highlights, skin-tone colour ∩ the subject box, the detected face/subject box) and is SCORED before it is offered: a clean region is left selected; an unconfident one is NOT selected and reported as honest absence (the city with no real sky gets no sky). Pro adds precise FACE-FEATURE targets backed by the face mesh — face_skin (the retouch mask: face minus eyes/brows/lips), face_eyes, face_brows, face_lips, face_teeth (mouth opening), face_nose, face_under_eye, face_cheeks — each a real geometry-following selection,
|
|
448
|
+
description: 'Select a region by NAME instead of coordinates — the natural-mask alternative to a rectangle — with a CONFIDENCE GATE. target=sky/ground/foliage/subject/face/shadows/highlights/skin/above_horizon resolves through the right Photoshop-native method (threshold for sky, invert-sky−subjects for ground, luminance for shadows/highlights, skin-tone colour ∩ the subject box, the detected face/subject box) and is SCORED before it is offered: a clean region is left selected; an unconfident one is NOT selected and reported as honest absence (the city with no real sky gets no sky). Pro adds precise FACE-FEATURE targets backed by the face mesh — face_skin (the retouch mask: face minus eyes/brows/lips), face_eyes, face_brows, face_lips, face_teeth (mouth opening), face_nose, face_under_eye, face_cheeks — each a real geometry-following selection, derived on first request and then saved as a scene:face_* channel so repeats load instantly. `passed`/`confidence` are returned. A region derived here is cached as a `scene:*` channel keyed by TARGET ONLY, so a later call for the same target loads it by name; pass `refresh:true` to force a fresh derive after an edit that changes what the region means, and note that narrowing a call with `label`/`instance`/`composition_context` always derives (it neither reads nor writes that shared channel). The structural floor (coherence, horizon alignment) is never tuned; for an artistic/non-standard shot pass `composition_context` (e.g. profile:big_sky) to relax the compositional priors so a legitimately large sky is not rejected. For target=subject with several present, pass `label` and/or `instance`. Build/inspect with ps_read_scene first; verify with ps_get_selection_preview (the red-overlay is the human/agent oversight view). Prefer this over a rectangle for any real-world region.',
|
|
425
449
|
inputSchema: selectByReferenceSchema,
|
|
426
450
|
outputSchema: {
|
|
427
451
|
type: 'object',
|
|
@@ -466,7 +466,7 @@ const SELECT_INPUT_SCHEMA = {
|
|
|
466
466
|
...selectPolygonSchema.properties,
|
|
467
467
|
placement: {
|
|
468
468
|
...PLACEMENT_SCHEMA,
|
|
469
|
-
description: 'Grounded coordinates (rectangle/ellipse/magic_wand): NAME anchors + a relation instead of guessing pixels. rectangle/ellipse ← a REGION relation (inside/gap) → the selection bounding box; magic_wand ← a POINT relation (centroid/extremum/grid) → the click. Verified by the objective gate; wins over the raw edges/x-y. See
|
|
469
|
+
description: 'Grounded coordinates (rectangle/ellipse/magic_wand): NAME anchors + a relation instead of guessing pixels. rectangle/ellipse ← a REGION relation (inside/gap) → the selection bounding box; magic_wand ← a POINT relation (centroid/extremum/grid) → the click. Verified by the objective gate; wins over the raw edges/x-y. See the placement-resolver tool, when this build has one, for the vocabulary.',
|
|
470
470
|
},
|
|
471
471
|
},
|
|
472
472
|
required: ['mode'],
|
|
@@ -31,7 +31,7 @@ const shapeInputSchema = {
|
|
|
31
31
|
'edge or a Pro face-mesh landmark curve) → a straight line between the resolved curve endpoints. The shape ' +
|
|
32
32
|
'is created ONLY if the gate PASSES (otherwise an error and no layer). When set, left/top/right/bottom and ' +
|
|
33
33
|
'start_x/start_y/end_x/end_y are ignored, but styling (fill_color/stroke/weight/corner_radius) still ' +
|
|
34
|
-
'applies. See
|
|
34
|
+
'applies. See the placement-resolver tool, when this build has one, for the anchors + relation vocabulary.',
|
|
35
35
|
},
|
|
36
36
|
left: {
|
|
37
37
|
type: 'number',
|
|
@@ -38,6 +38,19 @@ export const ERROR_CLASS_TABLE = [
|
|
|
38
38
|
pattern: /file not found|map not found|lut not found|could not open lut/i,
|
|
39
39
|
},
|
|
40
40
|
{ errorClass: 'face_not_found', pattern: /no face mesh|no face detected/i },
|
|
41
|
+
{
|
|
42
|
+
errorClass: 'perception_export_failed',
|
|
43
|
+
pattern: /saveAs reported success but no file/i,
|
|
44
|
+
},
|
|
45
|
+
{ errorClass: 'image_decode_failed', pattern: /failed to decode jpeg/i },
|
|
46
|
+
{
|
|
47
|
+
errorClass: 'detection_unavailable',
|
|
48
|
+
pattern: /onnxruntime|onnx runtime/i,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
errorClass: 'file_io',
|
|
52
|
+
pattern: /\b(ENOENT|EBUSY|EACCES|EPERM|EMFILE|ENOSPC)\b/,
|
|
53
|
+
},
|
|
41
54
|
{
|
|
42
55
|
errorClass: 'schema_validation',
|
|
43
56
|
pattern: /\bvalidat|required.*field|missing required argument|must be.*type|invalid (input|argument)/i,
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.
|
|
1
|
+
export const VERSION = '1.3.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editmamei",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Photoshop MCP server: natural-language AI photo editing with your own Photoshop (Community Edition)",
|
|
5
5
|
"mcpName": "io.github.editmamei/editmamei",
|
|
6
6
|
"editmamei": {
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"1.2.0": [
|
|
12
12
|
"ps_delete_layer",
|
|
13
13
|
"ps_select_layer"
|
|
14
|
+
],
|
|
15
|
+
"1.2.1": [
|
|
16
|
+
"ps_read_scene"
|
|
14
17
|
]
|
|
15
18
|
}
|
|
16
19
|
},
|