editmamei 1.2.1 → 1.4.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/README.md +6 -5
- 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/repair.js +11 -3
- package/dist/core/server.js +3 -3
- package/dist/core/tool-groups.js +3 -3
- package/dist/core/tool-registry.js +3 -1
- package/dist/core/tool-tiers.js +3 -3
- package/dist/delivery/provision.js +19 -1
- package/dist/kernel/kernel.js +3 -1
- package/dist/kernel/module-lifecycle.js +24 -4
- package/dist/modules/ce/index.js +8 -1
- package/dist/perception/facets.js +4 -3
- package/dist/perception/grounding-locate.js +7 -5
- package/dist/perception/region-scorer.js +1 -1
- package/dist/perception/scene-model.js +1 -1
- package/dist/perception/select-recipes.js +14 -3
- package/dist/platform/launch-readiness.js +25 -0
- package/dist/platform/macos-runner.js +18 -8
- package/dist/platform/script-queue.js +5 -19
- package/dist/platform/windows-runner.js +18 -8
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/client.js +11 -4
- package/dist/telemetry/events.js +5 -2
- package/dist/tools/detection-tools.js +4 -4
- package/dist/tools/document-tools.js +0 -1
- 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/preview-tools.js +1 -1
- package/dist/tools/scene-tools.js +8 -6
- package/dist/tools/selection-tools.js +156 -110
- package/dist/tools/sequence-tools.js +435 -0
- package/dist/tools/shape-tools.js +1 -1
- package/dist/utils/operation-timeouts.js +92 -0
- package/dist/utils/run-script.js +25 -1
- package/dist/utils/session-log.js +15 -1
- package/dist/utils/tool-budget-context.js +14 -0
- package/dist/version.js +1 -1
- package/package.json +8 -2
package/dist/telemetry/client.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Logger } from '../utils/logger.js';
|
|
|
2
2
|
import { EDITION } from '../edition.js';
|
|
3
3
|
import { VERSION } from '../version.js';
|
|
4
4
|
import { resolveInstallChannel } from '../install-channel.js';
|
|
5
|
-
import { buildDiagnosticEvent, buildModuleStatus, buildSessionStart, buildSessionSummary, buildUsageEvent, dayBucket, isContentSafe, PS_VERSION_UNKNOWN, } from './events.js';
|
|
5
|
+
import { buildDiagnosticEvent, buildModuleStatus, buildSessionStart, buildSessionSummary, buildUsageEvent, dayBucket, normalizeDayBucket, isContentSafe, PS_VERSION_UNKNOWN, } from './events.js';
|
|
6
6
|
import { sanitizeMessage, sanitizeSnippet, sanitizeStderrTail } from './sanitize.js';
|
|
7
7
|
import { httpTransport, resolveEndpoint } from './transport.js';
|
|
8
8
|
import { appendOutboxSync, clearOutbox, clearSessionState, readOutbox, readSessionState, writeSessionStateSync, } from './outbox.js';
|
|
@@ -32,6 +32,7 @@ export class TelemetryClient {
|
|
|
32
32
|
distinctTools = new Set();
|
|
33
33
|
anyFailures = false;
|
|
34
34
|
lastSessionPersistMs = 0;
|
|
35
|
+
startDayBucket = null;
|
|
35
36
|
constructor(opts) {
|
|
36
37
|
this.settings = opts.settings;
|
|
37
38
|
this.transport = opts.transport ?? httpTransport();
|
|
@@ -70,6 +71,7 @@ export class TelemetryClient {
|
|
|
70
71
|
recordCall(call) {
|
|
71
72
|
if (!this.active || !this.settings.telemetry.usage)
|
|
72
73
|
return;
|
|
74
|
+
this.ensureStartDayBucket();
|
|
73
75
|
this.toolCallCount += 1;
|
|
74
76
|
this.distinctTools.add(call.tool);
|
|
75
77
|
if (!call.success)
|
|
@@ -81,6 +83,11 @@ export class TelemetryClient {
|
|
|
81
83
|
const v = this.dims.getPsVersion();
|
|
82
84
|
return v && v.length > 0 ? v : PS_VERSION_UNKNOWN;
|
|
83
85
|
}
|
|
86
|
+
ensureStartDayBucket() {
|
|
87
|
+
if (this.startDayBucket === null)
|
|
88
|
+
this.startDayBucket = dayBucket(this.now());
|
|
89
|
+
return this.startDayBucket;
|
|
90
|
+
}
|
|
84
91
|
persistSessionStateThrottled() {
|
|
85
92
|
const nowMs = this.now().getTime();
|
|
86
93
|
if (this.lastSessionPersistMs !== 0 &&
|
|
@@ -90,7 +97,7 @@ export class TelemetryClient {
|
|
|
90
97
|
this.lastSessionPersistMs = nowMs;
|
|
91
98
|
const state = {
|
|
92
99
|
install_id: this.dims.install_id,
|
|
93
|
-
ts_bucket:
|
|
100
|
+
ts_bucket: this.ensureStartDayBucket(),
|
|
94
101
|
editmamei_version: this.dims.editmamei_version,
|
|
95
102
|
edition: this.dims.edition,
|
|
96
103
|
platform: this.dims.platform,
|
|
@@ -170,7 +177,7 @@ export class TelemetryClient {
|
|
|
170
177
|
tool_call_count: this.toolCallCount,
|
|
171
178
|
distinct_tools: this.distinctTools.size,
|
|
172
179
|
any_failures: this.anyFailures,
|
|
173
|
-
}, this.now()));
|
|
180
|
+
}, this.ensureStartDayBucket(), this.now()));
|
|
174
181
|
}
|
|
175
182
|
if (this.queue.length > 0) {
|
|
176
183
|
appendOutboxSync(this.restampPsVersion(this.queue.splice(0)).filter(isContentSafe), this.outboxOpts);
|
|
@@ -225,7 +232,7 @@ function summaryFromState(s) {
|
|
|
225
232
|
v: 2,
|
|
226
233
|
type: 'session_summary',
|
|
227
234
|
install_id: s.install_id,
|
|
228
|
-
ts_bucket: s.ts_bucket,
|
|
235
|
+
ts_bucket: normalizeDayBucket(s.ts_bucket, new Date()),
|
|
229
236
|
editmamei_version: s.editmamei_version,
|
|
230
237
|
edition: s.edition,
|
|
231
238
|
platform: s.platform,
|
package/dist/telemetry/events.js
CHANGED
|
@@ -10,6 +10,9 @@ export function normalizeErrorClass(value) {
|
|
|
10
10
|
.slice(0, 48);
|
|
11
11
|
return cleaned.length > 0 ? cleaned : 'other';
|
|
12
12
|
}
|
|
13
|
+
export function normalizeDayBucket(value, now) {
|
|
14
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : dayBucket(now);
|
|
15
|
+
}
|
|
13
16
|
function psVersionOf(dims) {
|
|
14
17
|
const v = dims.getPsVersion();
|
|
15
18
|
return v && v.length > 0 ? v : PS_VERSION_UNKNOWN;
|
|
@@ -30,12 +33,12 @@ export function buildUsageEvent(dims, call, now) {
|
|
|
30
33
|
duration_ms: call.duration_ms,
|
|
31
34
|
};
|
|
32
35
|
}
|
|
33
|
-
export function buildSessionSummary(dims, summary, now) {
|
|
36
|
+
export function buildSessionSummary(dims, summary, tsBucket, now) {
|
|
34
37
|
return {
|
|
35
38
|
v: TELEMETRY_SCHEMA_VERSION,
|
|
36
39
|
type: 'session_summary',
|
|
37
40
|
install_id: dims.install_id,
|
|
38
|
-
ts_bucket:
|
|
41
|
+
ts_bucket: normalizeDayBucket(tsBucket, now),
|
|
39
42
|
editmamei_version: dims.editmamei_version,
|
|
40
43
|
edition: dims.edition,
|
|
41
44
|
platform: dims.platform,
|
|
@@ -45,8 +45,8 @@ const detectSchema = {
|
|
|
45
45
|
},
|
|
46
46
|
annotate: {
|
|
47
47
|
type: 'boolean',
|
|
48
|
-
default:
|
|
49
|
-
description: '
|
|
48
|
+
default: false,
|
|
49
|
+
description: 'Also return an annotated preview JPEG with the detected boxes drawn (faces cyan, objects magenta). Default false: the labeled boxes returned by this call are already complete on their own — ask for the image only when you actually need to visually confirm a detection.',
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
};
|
|
@@ -76,7 +76,7 @@ async function detect(connection, client, rawArgs, detectDeps) {
|
|
|
76
76
|
try {
|
|
77
77
|
const args = validateArgs(detectSchema, rawArgs);
|
|
78
78
|
const target = args.target ?? 'both';
|
|
79
|
-
const annotate = args.annotate ??
|
|
79
|
+
const annotate = args.annotate ?? false;
|
|
80
80
|
const wantFaces = target === 'faces' || target === 'both';
|
|
81
81
|
const wantObjects = target === 'objects' || target === 'both';
|
|
82
82
|
const detectKey = JSON.stringify({
|
|
@@ -174,7 +174,7 @@ export function createDetectionTools(connection, _snippetClient, client = new On
|
|
|
174
174
|
{
|
|
175
175
|
tool: {
|
|
176
176
|
name: 'ps_detect',
|
|
177
|
-
description: 'The cheap, narrow read: labeled bounding boxes only — faces and/or COCO-80 objects (person, dog, car, chair, sofa, …) in DOCUMENT-pixel space
|
|
177
|
+
description: 'The cheap, narrow read: labeled bounding boxes only — faces and/or COCO-80 objects (person, dog, car, chair, sofa, …) in DOCUMENT-pixel space. LOCAL on-device computer vision; the image is never sent anywhere. Use this for real coordinates before a spatially-targeted edit when boxes are all you need — far more reliable than estimating positions from a preview. For the full scene model (regions, horizon, tonal zones, composition, and a menu of selectable named regions), use ps_read_scene instead. `target` selects faces / objects / both. Read-only: renders a throwaway duplicate, never modifies the working document. Boxes are [x1, y1, x2, y2]. Pass `annotate:true` for an annotated preview JPEG (faces cyan, objects magenta) when you need to visually confirm a surprising result.',
|
|
178
178
|
inputSchema: detectSchema,
|
|
179
179
|
outputSchema: {
|
|
180
180
|
type: 'object',
|
|
@@ -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: {
|
|
@@ -162,7 +162,7 @@ const histogramSchema = {
|
|
|
162
162
|
channel: {
|
|
163
163
|
type: 'string',
|
|
164
164
|
enum: ['composite', 'red', 'green', 'blue', 'luminosity', 'gray'],
|
|
165
|
-
description: 'Which channel to read. "composite" (default) is the visible flattened image; if the active layer is an adjustment/fill/shape layer the tool transparently switches to a pixel layer to read it. "red"/"green"/"blue" require an RGB doc; "gray" a grayscale doc. "luminosity" dispatches per doc mode — Lab uses the Lightness channel (exact), Grayscale uses Gray (exact), RGB
|
|
165
|
+
description: 'Which channel to read. "composite" (default) is the visible flattened image; if the active layer is an adjustment/fill/shape layer the tool transparently switches to a pixel layer to read it. "red"/"green"/"blue" require an RGB doc; "gray" a grayscale doc. "luminosity" dispatches per doc mode — Lab uses the Lightness channel (exact), Grayscale uses Gray (exact), and RGB reads the per-pixel luminance Photoshop reports, weighted 0.30/0.59/0.11, so its shape and any clipping or percentile read taken from it are sound. Note that weighting is not Rec.709, so a luminosity mean will not match one computed as 0.2126/0.7152/0.0722. A channel value naming a marginal mixture means the document histogram was unavailable and the read fell back to combining the channel histograms: that mean is still sound, its shape is not. The result\'s `channel` field annotates which path landed when a fallback was used.',
|
|
166
166
|
default: 'composite',
|
|
167
167
|
},
|
|
168
168
|
},
|
|
@@ -71,8 +71,8 @@ const sceneSchema = {
|
|
|
71
71
|
properties: {
|
|
72
72
|
annotate: {
|
|
73
73
|
type: 'boolean',
|
|
74
|
-
default:
|
|
75
|
-
description: '
|
|
74
|
+
default: false,
|
|
75
|
+
description: 'Also return an annotated preview JPEG with subject boxes (magenta), faces (cyan), and the horizon line (yellow) drawn. Default false: the structured scene model returned by this call is already complete on its own — ask for the image only when you actually need to see the annotation drawn.',
|
|
76
76
|
},
|
|
77
77
|
refresh: {
|
|
78
78
|
type: 'boolean',
|
|
@@ -130,7 +130,9 @@ function summarizeScene(model) {
|
|
|
130
130
|
const parts = [
|
|
131
131
|
`${model.subjects.length} subject(s)${subjStr ? ` (${subjStr})` : ''}`,
|
|
132
132
|
`${model.faces.length} face(s)`,
|
|
133
|
-
|
|
133
|
+
model.horizon.detected
|
|
134
|
+
? `horizon at y=${model.horizon.y} (${Math.round(model.horizon.placement * 100)}% down, conf ${model.horizon.confidence.toFixed(2)})`
|
|
135
|
+
: `no horizon measured (${model.horizon.reason})`,
|
|
134
136
|
`sky ~${Math.round((model.regions.find((r) => r.kind === 'sky')?.coverage ?? 0) * 100)}%`,
|
|
135
137
|
];
|
|
136
138
|
if (main && cell)
|
|
@@ -140,7 +142,7 @@ function summarizeScene(model) {
|
|
|
140
142
|
async function scene(connection, snippet, client, rawArgs, proRefine, hasPro = false, detectDeps) {
|
|
141
143
|
try {
|
|
142
144
|
const args = validateArgs(sceneSchema, rawArgs);
|
|
143
|
-
const annotate = args.annotate ??
|
|
145
|
+
const annotate = args.annotate ?? false;
|
|
144
146
|
const refresh = args.refresh ?? false;
|
|
145
147
|
const saveRegions = args.save_regions ?? false;
|
|
146
148
|
const built = await buildSceneModel(connection, snippet, client, {
|
|
@@ -177,7 +179,7 @@ async function scene(connection, snippet, client, rawArgs, proRefine, hasPro = f
|
|
|
177
179
|
try {
|
|
178
180
|
const exportH = built.exportImage.height || 0;
|
|
179
181
|
const sy = model.doc.height > 0 ? exportH / model.doc.height : 1;
|
|
180
|
-
const horizonExportY = Math.round(model.horizon.y * sy);
|
|
182
|
+
const horizonExportY = model.horizon.detected ? Math.round(model.horizon.y * sy) : null;
|
|
181
183
|
const annotated = annotateScene(built.decoded, built.rawFaces, built.rawObjects.map((o) => o.bbox), horizonExportY);
|
|
182
184
|
content.push({
|
|
183
185
|
type: 'image',
|
|
@@ -391,7 +393,7 @@ export function createSceneTools(connection, snippetClient, opts = {}) {
|
|
|
391
393
|
{
|
|
392
394
|
tool: {
|
|
393
395
|
name: 'ps_read_scene',
|
|
394
|
-
description: 'The full scene model — run this before a spatially-targeted edit, not the cheaper ps_detect: detected subjects (with the main one flagged) and faces in document pixels, a coarse sky/ground region map, the horizon line (y + placement + confidence), tonal zones (shadow/midtone/highlight bands + coverage), composition geometry (which thirds cell the subject sits in, balance, headroom),
|
|
396
|
+
description: 'The full scene model — run this before a spatially-targeted edit, not the cheaper ps_detect: detected subjects (with the main one flagged) and faces in document pixels, a coarse sky/ground region map, the horizon line (y + placement + confidence), tonal zones (shadow/midtone/highlight bands + coverage), composition geometry (which thirds cell the subject sits in, balance, headroom), and the menu of selectable named regions. The structured model is complete on its own — pass `annotate:true` for an annotated preview JPEG when you actually need to see it drawn. Built using LOCAL on-device vision + classical CV; the image never leaves the machine. Select regions by name with ps_select_by_reference instead of guessing a rectangle. Read-only: renders a throwaway duplicate. Perception is cached per document state, so repeated reads are cheap.',
|
|
395
397
|
inputSchema: sceneSchema,
|
|
396
398
|
outputSchema: {
|
|
397
399
|
type: 'object',
|
|
@@ -316,6 +316,24 @@ const selectPolygonSchema = {
|
|
|
316
316
|
},
|
|
317
317
|
required: ['points'],
|
|
318
318
|
};
|
|
319
|
+
const selectFocusAreaSchema = {
|
|
320
|
+
type: 'object',
|
|
321
|
+
properties: {
|
|
322
|
+
in_focus_radius: {
|
|
323
|
+
type: 'number',
|
|
324
|
+
description: 'How much blur still counts as "in focus", in pixels. Higher pulls more of the soft transition zone into the selection; lower keeps only the crisply resolved plane. 4.07 is the Photoshop dialog default and a sane starting point. The useful band is narrow, and a radius well above the default selects the entire frame — so move in small steps and CHECK the returned area_percent and whole_canvas_selected: a selection covering essentially everything means the radius is too high and the result is worthless, even though the call reports success.',
|
|
325
|
+
default: 4.07,
|
|
326
|
+
minimum: 0.1,
|
|
327
|
+
maximum: 15,
|
|
328
|
+
},
|
|
329
|
+
soft_mask: {
|
|
330
|
+
type: 'boolean',
|
|
331
|
+
description: 'False (default) yields a hard-edged selection — every pixel fully in or fully out, which is what you want before ps_modify_selection feathering. True lets Photoshop feather the focus falloff itself, useful when the subject edge is genuinely gradual (hair, fur, motion).',
|
|
332
|
+
default: false,
|
|
333
|
+
},
|
|
334
|
+
selection_type: selectionTypeFragment,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
319
337
|
const modifyEdgeSchema = {
|
|
320
338
|
type: 'object',
|
|
321
339
|
properties: {
|
|
@@ -376,6 +394,12 @@ const selectionPreviewSchema = {
|
|
|
376
394
|
maximum: 4096,
|
|
377
395
|
default: 800,
|
|
378
396
|
},
|
|
397
|
+
image: {
|
|
398
|
+
type: 'string',
|
|
399
|
+
enum: ['overlay', 'mask', 'both'],
|
|
400
|
+
default: 'overlay',
|
|
401
|
+
description: "Which rendered image(s) to return inline. 'overlay' (default): a 50% red wash over the selected area (Quick Mask-style — most intuitive). 'mask': a B/W mask (black=selected, white=not) instead. 'both': the overlay followed by the mask, for when you need to compare them side by side. selection_info is returned regardless of this choice.",
|
|
402
|
+
},
|
|
379
403
|
},
|
|
380
404
|
};
|
|
381
405
|
const saveSelectionToChannelSchema = {
|
|
@@ -437,6 +461,7 @@ const SELECT_MODES = [
|
|
|
437
461
|
'skin_tones',
|
|
438
462
|
'out_of_gamut',
|
|
439
463
|
'polygon',
|
|
464
|
+
'focus_area',
|
|
440
465
|
];
|
|
441
466
|
const SELECT_INPUT_SCHEMA = {
|
|
442
467
|
type: 'object',
|
|
@@ -455,8 +480,9 @@ const SELECT_INPUT_SCHEMA = {
|
|
|
455
480
|
'skin_tones: select skin-coloured pixels (+fuzziness; use_faces=true adds face-aware refinement). ' +
|
|
456
481
|
'out_of_gamut: select colours outside the printable CMYK gamut (no params). ' +
|
|
457
482
|
'polygon: points [{x,y},...] in ABSOLUTE document pixels (min 3, auto-closes) — covers polygonal/freehand lasso. Coordinate-driven: you must know the pixel positions (use ps_inspect / ps_get_preview to aim, or ps_path create_from_placement → load_as_selection for a grounded outline). ' +
|
|
483
|
+
'focus_area: select what the lens rendered SHARP by depth of field, not by subject or colour (+in_focus_radius, soft_mask) — takes no coordinates; check whole_canvas_selected in the result before trusting it. ' +
|
|
458
484
|
'rectangle/ellipse/magic_wand also take a grounded `placement` instead of raw coords (region → the bbox; point → the wand click). ' +
|
|
459
|
-
'rectangle/ellipse/polygon/color_range/luminance_range/magic_wand/skin_tones/out_of_gamut also take selection_type to combine with an existing selection.',
|
|
485
|
+
'rectangle/ellipse/polygon/focus_area/color_range/luminance_range/magic_wand/skin_tones/out_of_gamut also take selection_type to combine with an existing selection.',
|
|
460
486
|
},
|
|
461
487
|
...selectRectangleSchema.properties,
|
|
462
488
|
...selectColorRangeSchema.properties,
|
|
@@ -464,9 +490,10 @@ const SELECT_INPUT_SCHEMA = {
|
|
|
464
490
|
...magicWandSchema.properties,
|
|
465
491
|
...colorPresetSchema.properties,
|
|
466
492
|
...selectPolygonSchema.properties,
|
|
493
|
+
...selectFocusAreaSchema.properties,
|
|
467
494
|
placement: {
|
|
468
495
|
...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
|
|
496
|
+
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
497
|
},
|
|
471
498
|
},
|
|
472
499
|
required: ['mode'],
|
|
@@ -580,7 +607,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
580
607
|
{
|
|
581
608
|
tool: {
|
|
582
609
|
name: 'ps_select',
|
|
583
|
-
description: 'Create a NEW selection — choose with `mode`. (To edit the CURRENT selection instead — including growing it by colour similarity — use ps_modify_selection.) `all` selects the canvas; `none` deselects; `inverse` inverts the current selection (e.g. select the subject, then inverse to act on the background). `rectangle` (left/top/right/bottom, optional feather_px to avoid hard block-edges in smooth sky). `ellipse` (left/top/right/bottom bounding box + anti_alias — circles/ovals). `color_range` (target red/green/blue + fuzziness — "select all the red / skin tones"). `luminance_range` (highlights/shadows/midtones — foundation for glow / dodge-burn). `magic_wand` (click x/y + tolerance, contiguous). `grow` / `similar` are DEPRECATED here (they act on the CURRENT selection, not a new one) — use ps_modify_selection(op=grow|similar) instead; kept for one release for backward compatibility, identical behaviour. rectangle/ellipse/magic_wand also accept a grounded `placement` (NAME a region/point instead of guessing pixels — resolved + gate-verified). The geometric/color/wand modes take selection_type (replace|add|subtract|intersect) to combine with an existing selection and return a rich selection_info bundle — verify it (or ps_get_selection_preview) before committing to a mask.',
|
|
610
|
+
description: 'Create a NEW selection — choose with `mode`. (To edit the CURRENT selection instead — including growing it by colour similarity — use ps_modify_selection.) `all` selects the canvas; `none` deselects; `inverse` inverts the current selection (e.g. select the subject, then inverse to act on the background). `rectangle` (left/top/right/bottom, optional feather_px to avoid hard block-edges in smooth sky). `ellipse` (left/top/right/bottom bounding box + anti_alias — circles/ovals). `color_range` (target red/green/blue + fuzziness — "select all the red / skin tones"). `luminance_range` (highlights/shadows/midtones — foundation for glow / dodge-burn). `magic_wand` (click x/y + tolerance, contiguous). `focus_area` selects by depth of field rather than subject or colour (+in_focus_radius, soft_mask) — check whole_canvas_selected/warning in the result before trusting it. `grow` / `similar` are DEPRECATED here (they act on the CURRENT selection, not a new one) — use ps_modify_selection(op=grow|similar) instead; kept for one release for backward compatibility, identical behaviour. rectangle/ellipse/magic_wand also accept a grounded `placement` (NAME a region/point instead of guessing pixels — resolved + gate-verified). The geometric/color/wand modes take selection_type (replace|add|subtract|intersect) to combine with an existing selection and return a rich selection_info bundle — verify it (or ps_get_selection_preview) before committing to a mask.',
|
|
584
611
|
inputSchema: SELECT_INPUT_SCHEMA,
|
|
585
612
|
outputSchema: {
|
|
586
613
|
type: 'object',
|
|
@@ -606,6 +633,24 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
606
633
|
preset: { type: 'string' },
|
|
607
634
|
point_count: { type: 'number' },
|
|
608
635
|
placement: { type: 'object' },
|
|
636
|
+
strategy_used: {
|
|
637
|
+
type: 'string',
|
|
638
|
+
description: 'mode=focus_area: "executeAction:focusMask".',
|
|
639
|
+
},
|
|
640
|
+
in_focus_radius: { type: 'number', description: 'mode=focus_area: radius used.' },
|
|
641
|
+
soft_mask: { type: 'boolean', description: 'mode=focus_area: soft_mask used.' },
|
|
642
|
+
active_layer_temporarily_changed: {
|
|
643
|
+
type: 'boolean',
|
|
644
|
+
description: 'mode=focus_area: true if the active layer was not an ordinary pixel layer and detection was temporarily retargeted to the bottom layer. Restored before return.',
|
|
645
|
+
},
|
|
646
|
+
whole_canvas_selected: {
|
|
647
|
+
type: 'boolean',
|
|
648
|
+
description: 'mode=focus_area: true when the RAW detection (before any selection_type combine) covered essentially the entire canvas — usually a non-result. selection_info reports the FINAL, post-combine selection and the two can legitimately disagree.',
|
|
649
|
+
},
|
|
650
|
+
warning: {
|
|
651
|
+
type: ['string', 'null'],
|
|
652
|
+
description: 'mode=focus_area: set when whole_canvas_selected is true.',
|
|
653
|
+
},
|
|
609
654
|
selection_info: selectionInfoFragment,
|
|
610
655
|
},
|
|
611
656
|
},
|
|
@@ -660,7 +705,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
660
705
|
{
|
|
661
706
|
tool: {
|
|
662
707
|
name: 'ps_get_selection_preview',
|
|
663
|
-
description:
|
|
708
|
+
description: "Render an inline JPEG so the agent can visually verify what is currently selected: by default a red-wash OVERLAY (50% red over the selected area, Quick Mask-style — most intuitive); pass `image:'mask'` for a B/W MASK (black = selected, white = not) instead, or `image:'both'` for both. selection_info is always returned regardless of `image`. Heavier than the selection_info bundle alone (~2-4s) — call this when the stats look off or before committing a mask. Does NOT modify the source document.",
|
|
664
709
|
inputSchema: selectionPreviewSchema,
|
|
665
710
|
outputSchema: {
|
|
666
711
|
type: 'object',
|
|
@@ -673,7 +718,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
673
718
|
},
|
|
674
719
|
},
|
|
675
720
|
annotations: {
|
|
676
|
-
title: 'Get Selection Preview
|
|
721
|
+
title: 'Get Selection Preview',
|
|
677
722
|
readOnlyHint: true,
|
|
678
723
|
idempotentHint: true,
|
|
679
724
|
},
|
|
@@ -817,42 +862,6 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
817
862
|
},
|
|
818
863
|
handler: async (args) => selectSky(connection, snippetClient, args),
|
|
819
864
|
},
|
|
820
|
-
{
|
|
821
|
-
tool: {
|
|
822
|
-
name: 'ps_select_focus_area',
|
|
823
|
-
description: "Run Photoshop's \"Focus Area\" — select what the lens rendered SHARP, by depth of field rather than by subject or colour. Use it when the thing you want is defined by focus and not by what it is: lifting a subject off a bokeh background, masking the in-focus plane of a macro shot, or grabbing a shallow-depth foreground that Select Subject splits badly. Takes NO coordinates. in_focus_radius widens (higher) or narrows (lower) what counts as sharp; soft_mask=true gives feathered edges instead of a hard boundary. Analyses the ACTIVE layer, so target the photographic pixels you mean. If the active layer is not a raster layer at all (adjustment, smart object, text, shape), detection is retargeted to the bottom layer and active_layer_temporarily_changed comes back true — check it, because the analysed layer was then NOT the one you selected. An EMPTY raster layer cannot be told apart by kind, so it is not retargeted; it surfaces instead as an error or as whole_canvas_selected. whole_canvas_selected and warning diagnose Focus Area's RAW detection, measured BEFORE any selection_type combine — a uniformly sharp image (or too high an in_focus_radius) trips them even when combining then folds the result down to something small. Check whole_canvas_selected first; selection_info separately reports the FINAL, post-combine selection and can disagree with it by design (e.g. selection_type='subtract' against an existing selection), so read selection_info for what actually got selected, not as a substitute for whole_canvas_selected.",
|
|
824
|
-
inputSchema: selectFocusAreaSchema,
|
|
825
|
-
outputSchema: {
|
|
826
|
-
type: 'object',
|
|
827
|
-
properties: {
|
|
828
|
-
selected: { type: 'boolean' },
|
|
829
|
-
method: { type: 'string' },
|
|
830
|
-
strategy_used: { type: 'string' },
|
|
831
|
-
in_focus_radius: { type: 'number' },
|
|
832
|
-
soft_mask: { type: 'boolean' },
|
|
833
|
-
active_layer_temporarily_changed: {
|
|
834
|
-
type: 'boolean',
|
|
835
|
-
description: 'True if the active layer was not an ordinary pixel layer and detection was temporarily retargeted to the bottom layer. The original active layer is restored before return.',
|
|
836
|
-
},
|
|
837
|
-
whole_canvas_selected: {
|
|
838
|
-
type: 'boolean',
|
|
839
|
-
description: "True when Focus Area's RAW detection covered essentially the entire canvas — usually a non-result (radius too high, or nothing photographic to analyse). Measured BEFORE combining with any prior selection, so it describes the detection step, not the final selection: selection_info reports the FINAL, post-combine result and the two can legitimately disagree, e.g. selection_type='subtract' against an existing selection can leave this true while selection_info.area_percent is well under 100.",
|
|
840
|
-
},
|
|
841
|
-
warning: {
|
|
842
|
-
type: ['string', 'null'],
|
|
843
|
-
description: 'Set when whole_canvas_selected is true. Also describes the RAW detection, not the final post-combine selection.',
|
|
844
|
-
},
|
|
845
|
-
selection_type: { type: 'string' },
|
|
846
|
-
selection_info: selectionInfoFragment,
|
|
847
|
-
},
|
|
848
|
-
},
|
|
849
|
-
annotations: {
|
|
850
|
-
title: 'Select Focus Area',
|
|
851
|
-
idempotentHint: true,
|
|
852
|
-
},
|
|
853
|
-
},
|
|
854
|
-
handler: async (args) => selectFocusArea(connection, snippetClient, args),
|
|
855
|
-
},
|
|
856
865
|
];
|
|
857
866
|
}
|
|
858
867
|
const selectSubjectSchema = {
|
|
@@ -877,72 +886,77 @@ const selectSkySchema = {
|
|
|
877
886
|
selection_type: selectionTypeFragment,
|
|
878
887
|
},
|
|
879
888
|
};
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
in_focus_radius: {
|
|
884
|
-
type: 'number',
|
|
885
|
-
description: 'How much blur still counts as "in focus", in pixels. Higher pulls more of the soft transition zone into the selection; lower keeps only the crisply resolved plane. 4.07 is the Photoshop dialog default and a sane starting point. The useful band is narrow, and a radius well above the default selects the entire frame — so move in small steps and CHECK the returned area_percent and whole_canvas_selected: a selection covering essentially everything means the radius is too high and the result is worthless, even though the call reports success.',
|
|
886
|
-
default: 4.07,
|
|
887
|
-
minimum: 0.1,
|
|
888
|
-
maximum: 15,
|
|
889
|
-
},
|
|
890
|
-
soft_mask: {
|
|
891
|
-
type: 'boolean',
|
|
892
|
-
description: 'False (default) yields a hard-edged selection — every pixel fully in or fully out, which is what you want before ps_modify_selection feathering. True lets Photoshop feather the focus falloff itself, useful when the subject edge is genuinely gradual (hair, fur, motion).',
|
|
893
|
-
default: false,
|
|
894
|
-
},
|
|
895
|
-
selection_type: selectionTypeFragment,
|
|
896
|
-
},
|
|
897
|
-
};
|
|
889
|
+
function selectionInfoOf(result) {
|
|
890
|
+
return result.selection_info;
|
|
891
|
+
}
|
|
898
892
|
async function selectFocusArea(connection, snippetClient, rawArgs) {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
schema: selectFocusAreaSchema,
|
|
904
|
-
snippet: 'selectFocusArea',
|
|
905
|
-
errorPrefix: 'Error running Focus Area selection',
|
|
906
|
-
timeoutMs: SELECT_FOCUS_AREA_TIMEOUT_MS,
|
|
907
|
-
params: (args) => ({
|
|
893
|
+
try {
|
|
894
|
+
const args = validateArgs(selectFocusAreaSchema, rawArgs);
|
|
895
|
+
const selectionType = normalizeSelectionType(args.selection_type);
|
|
896
|
+
const script = await snippetClient.build('selectFocusArea', {
|
|
908
897
|
inFocusRadius: args.in_focus_radius ?? 4.07,
|
|
909
898
|
softMask: args.soft_mask ?? false,
|
|
910
|
-
selectionType
|
|
911
|
-
})
|
|
912
|
-
|
|
913
|
-
|
|
899
|
+
selectionType,
|
|
900
|
+
});
|
|
901
|
+
const result = (await runScript(connection, script, SELECT_FOCUS_AREA_TIMEOUT_MS));
|
|
902
|
+
let text = describeSelectionFacts(`Focus Area selection (${selectionType})`, selectionInfoOf(result));
|
|
903
|
+
if (result.warning) {
|
|
904
|
+
text += ` WARNING: ${String(result.warning)}`;
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
content: [{ type: 'text', text }],
|
|
908
|
+
structuredContent: result,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
return toolErrorResult('Error running Focus Area selection', error);
|
|
913
|
+
}
|
|
914
914
|
}
|
|
915
915
|
async function selectSubject(connection, snippetClient, rawArgs) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
schema: selectSubjectSchema,
|
|
921
|
-
snippet: 'selectSubject',
|
|
922
|
-
errorPrefix: 'Error running Select Subject',
|
|
923
|
-
timeoutMs: SELECT_SUBJECT_TIMEOUT_MS,
|
|
924
|
-
params: (args) => ({
|
|
916
|
+
try {
|
|
917
|
+
const args = validateArgs(selectSubjectSchema, rawArgs);
|
|
918
|
+
const selectionType = normalizeSelectionType(args.selection_type);
|
|
919
|
+
const script = await snippetClient.build('selectSubject', {
|
|
925
920
|
sampleAllLayers: args.sample_all_layers ?? true,
|
|
926
|
-
selectionType
|
|
927
|
-
})
|
|
928
|
-
|
|
929
|
-
|
|
921
|
+
selectionType,
|
|
922
|
+
});
|
|
923
|
+
const result = (await runScript(connection, script, SELECT_SUBJECT_TIMEOUT_MS));
|
|
924
|
+
return {
|
|
925
|
+
content: [
|
|
926
|
+
{
|
|
927
|
+
type: 'text',
|
|
928
|
+
text: describeSelectionFacts(`Select Subject (${selectionType})`, selectionInfoOf(result)),
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
structuredContent: result,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
return toolErrorResult('Error running Select Subject', error);
|
|
936
|
+
}
|
|
930
937
|
}
|
|
931
938
|
async function selectSky(connection, snippetClient, rawArgs) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
schema: selectSkySchema,
|
|
937
|
-
snippet: 'selectSky',
|
|
938
|
-
errorPrefix: 'Error running Select Sky',
|
|
939
|
-
timeoutMs: SELECT_SKY_TIMEOUT_MS,
|
|
940
|
-
params: (args) => ({
|
|
939
|
+
try {
|
|
940
|
+
const args = validateArgs(selectSkySchema, rawArgs);
|
|
941
|
+
const selectionType = normalizeSelectionType(args.selection_type);
|
|
942
|
+
const script = await snippetClient.build('selectSky', {
|
|
941
943
|
sampleAllLayers: args.sample_all_layers ?? true,
|
|
942
|
-
selectionType
|
|
943
|
-
})
|
|
944
|
-
|
|
945
|
-
|
|
944
|
+
selectionType,
|
|
945
|
+
});
|
|
946
|
+
const result = (await runScript(connection, script, SELECT_SKY_TIMEOUT_MS));
|
|
947
|
+
return {
|
|
948
|
+
content: [
|
|
949
|
+
{
|
|
950
|
+
type: 'text',
|
|
951
|
+
text: describeSelectionFacts(`Select Sky (${selectionType})`, selectionInfoOf(result)),
|
|
952
|
+
},
|
|
953
|
+
],
|
|
954
|
+
structuredContent: result,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
return toolErrorResult('Error running Select Sky', error);
|
|
959
|
+
}
|
|
946
960
|
}
|
|
947
961
|
async function select(connection, snippetClient, detClient, rawArgs) {
|
|
948
962
|
const mode = rawArgs.mode;
|
|
@@ -974,6 +988,8 @@ async function select(connection, snippetClient, detClient, rawArgs) {
|
|
|
974
988
|
return selectColorPreset(connection, snippetClient, 'out_of_gamut', rest);
|
|
975
989
|
case 'polygon':
|
|
976
990
|
return selectPolygon(connection, snippetClient, rest);
|
|
991
|
+
case 'focus_area':
|
|
992
|
+
return selectFocusArea(connection, snippetClient, rest);
|
|
977
993
|
default:
|
|
978
994
|
return unknownDiscriminator('select mode', mode, SELECT_MODES);
|
|
979
995
|
}
|
|
@@ -1038,6 +1054,25 @@ export function normalizeSelectionType(raw) {
|
|
|
1038
1054
|
? v
|
|
1039
1055
|
: 'replace';
|
|
1040
1056
|
}
|
|
1057
|
+
async function readSelectionInfo(connection, snippetClient) {
|
|
1058
|
+
const script = await snippetClient.build('getSelectionState');
|
|
1059
|
+
return (await runScript(connection, script));
|
|
1060
|
+
}
|
|
1061
|
+
function describeSelectionFacts(label, info) {
|
|
1062
|
+
if (!info || typeof info.has_selection !== 'boolean') {
|
|
1063
|
+
return `${label} complete — selection facts unknown (the result carried no readable selection_info).`;
|
|
1064
|
+
}
|
|
1065
|
+
if (!info.has_selection) {
|
|
1066
|
+
return `${label} completed but the resulting selection is empty.`;
|
|
1067
|
+
}
|
|
1068
|
+
if (!info.bounds) {
|
|
1069
|
+
const reason = typeof info.error === 'string' ? ` (${info.error})` : '';
|
|
1070
|
+
return `${label} complete — a selection exists but its bounds/coverage could not be measured${reason}.`;
|
|
1071
|
+
}
|
|
1072
|
+
const b = info.bounds;
|
|
1073
|
+
const pct = (info.area_percent ?? 0).toFixed(1);
|
|
1074
|
+
return `${label} complete — selection (${b.left}, ${b.top}) to (${b.right}, ${b.bottom}), ${pct}% of canvas.`;
|
|
1075
|
+
}
|
|
1041
1076
|
function describeMaskOutcome(result) {
|
|
1042
1077
|
if (result.maskCreated)
|
|
1043
1078
|
return 'Layer mask created from selection';
|
|
@@ -1395,8 +1430,7 @@ async function magicWand(connection, snippetClient, detClient, rawArgs) {
|
|
|
1395
1430
|
}
|
|
1396
1431
|
export async function getSelectionInfoHandler(connection, snippetClient) {
|
|
1397
1432
|
try {
|
|
1398
|
-
const
|
|
1399
|
-
const result = (await runScript(connection, script));
|
|
1433
|
+
const result = (await readSelectionInfo(connection, snippetClient));
|
|
1400
1434
|
const summary = result.has_selection
|
|
1401
1435
|
? `Active selection: ${(result.area_percent ?? 0).toFixed(1)}% of canvas (${(result.pixel_count ?? 0).toLocaleString()} px), bounds-fill ratio ${(result.bounds_fill_ratio ?? 0).toFixed(2)}, edge complexity ${(result.edge_complexity ?? 0).toFixed(2)}`
|
|
1402
1436
|
: 'No active selection.';
|
|
@@ -1413,6 +1447,7 @@ async function getSelectionPreview(connection, snippetClient, rawArgs) {
|
|
|
1413
1447
|
try {
|
|
1414
1448
|
const args = validateArgs(selectionPreviewSchema, rawArgs);
|
|
1415
1449
|
const maxDimension = args.max_dimension ?? 800;
|
|
1450
|
+
const image = args.image ?? 'overlay';
|
|
1416
1451
|
const dir = await TempDir.create('editmamei-sel-preview-');
|
|
1417
1452
|
try {
|
|
1418
1453
|
const overlayPath = dir.path('overlay.jpg');
|
|
@@ -1436,17 +1471,28 @@ async function getSelectionPreview(connection, snippetClient, rawArgs) {
|
|
|
1436
1471
|
}
|
|
1437
1472
|
const overlayBytes = await readFile(overlayPath);
|
|
1438
1473
|
const maskBytes = await readFile(maskPath);
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1474
|
+
const text = image === 'both'
|
|
1475
|
+
? `Selection preview rendered. Image 1 = overlay (red wash on selected area). Image 2 = mask (black=selected, white=not).`
|
|
1476
|
+
: image === 'mask'
|
|
1477
|
+
? `Selection preview rendered. Image 1 = mask (black=selected, white=not).`
|
|
1478
|
+
: `Selection preview rendered. Image 1 = overlay (red wash on selected area).`;
|
|
1479
|
+
const content = [{ type: 'text', text }];
|
|
1480
|
+
if (image === 'overlay' || image === 'both') {
|
|
1481
|
+
content.push({
|
|
1482
|
+
type: 'image',
|
|
1483
|
+
data: overlayBytes.toString('base64'),
|
|
1484
|
+
mimeType: 'image/jpeg',
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
if (image === 'mask' || image === 'both') {
|
|
1488
|
+
content.push({
|
|
1489
|
+
type: 'image',
|
|
1490
|
+
data: maskBytes.toString('base64'),
|
|
1491
|
+
mimeType: 'image/jpeg',
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1441
1494
|
return {
|
|
1442
|
-
content
|
|
1443
|
-
{
|
|
1444
|
-
type: 'text',
|
|
1445
|
-
text: `Selection preview rendered. Image 1 = overlay (red wash on selected area). Image 2 = mask (black=selected, white=not).`,
|
|
1446
|
-
},
|
|
1447
|
-
{ type: 'image', data: overlayB64, mimeType: 'image/jpeg' },
|
|
1448
|
-
{ type: 'image', data: maskB64, mimeType: 'image/jpeg' },
|
|
1449
|
-
],
|
|
1495
|
+
content,
|
|
1450
1496
|
structuredContent: {
|
|
1451
1497
|
rendered: true,
|
|
1452
1498
|
max_dimension: result.max_dimension ?? maxDimension,
|