editmamei 0.19.0 → 0.22.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/NOTICES.md +17 -1
- package/README.md +9 -8
- package/dist/api/extendscript/_helpers.js +1 -1
- 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/config.js +3 -3
- package/dist/cli/help.js +4 -0
- package/dist/cli/install.js +2 -2
- package/dist/cli/report.js +11 -0
- package/dist/cli/router.js +20 -0
- package/dist/core/server.js +8 -8
- package/dist/core/settings.js +2 -2
- package/dist/core/tool-groups.js +92 -82
- package/dist/core/tool-tiers.js +91 -81
- package/dist/detection/landmark-detection-client.js +8 -0
- package/dist/detection/mesh-eye-correction.js +126 -0
- package/dist/detection/sam-segmenter.js +83 -0
- package/dist/diagnostics/collect.js +169 -0
- package/dist/index.js +2 -0
- package/dist/license/entitlement.js +52 -0
- package/dist/modules/ce/index.js +6 -4
- package/dist/perception/grounding-anchors.js +37 -0
- package/dist/perception/grounding-corners.js +93 -0
- package/dist/perception/grounding-edge-trace.js +90 -0
- package/dist/perception/grounding-extrema.js +41 -0
- package/dist/perception/grounding-gate.js +154 -0
- package/dist/perception/grounding-geometry.js +132 -0
- package/dist/perception/grounding-grid.js +34 -0
- package/dist/perception/grounding-landmarks.js +40 -0
- package/dist/perception/grounding-locate.js +341 -0
- package/dist/perception/grounding-resolver.js +202 -0
- package/dist/perception/grounding-review-crop.js +85 -0
- package/dist/perception/grounding-warp.js +182 -0
- package/dist/perception/sky-mask-transfer.js +50 -18
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +3 -3
- package/dist/tools/brush-tools.js +111 -23
- package/dist/tools/channel-compose-tools.js +241 -0
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/diagnostics-tools.js +66 -0
- package/dist/tools/document-tools.js +6 -6
- package/dist/tools/filter-tools.js +476 -19
- package/dist/tools/group-tools.js +10 -10
- package/dist/tools/guide-tools.js +4 -4
- package/dist/tools/history-tools.js +4 -4
- package/dist/tools/image-placement-tools.js +1 -1
- package/dist/tools/image-tools.js +62 -19
- package/dist/tools/inspect-tools.js +2 -2
- package/dist/tools/layer-ordering-tools.js +1 -1
- package/dist/tools/layer-properties-tools.js +42 -23
- package/dist/tools/layer-tools.js +10 -10
- package/dist/tools/layer-transform-tools.js +80 -41
- package/dist/tools/overview-tools.js +42 -42
- package/dist/tools/path-tools.js +53 -10
- package/dist/tools/portrait-tools.js +3 -3
- package/dist/tools/preview-tools.js +7 -82
- package/dist/tools/retouch-tools.js +93 -25
- package/dist/tools/scene-tools.js +13 -13
- package/dist/tools/selection-tools.js +319 -40
- package/dist/tools/shape-tools.js +216 -0
- package/dist/tools/text-tools.js +1 -1
- package/dist/tools/transform-canvas-tools.js +2 -2
- package/dist/tools/vector-mask-tools.js +23 -8
- package/dist/utils/log-buffer.js +41 -0
- package/dist/utils/logger.js +12 -1
- package/dist/utils/session-log-reader.js +27 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/preview/coordinate-grid.js +0 -250
- package/dist/tools/object-tools.js +0 -193
- package/dist/tools/text-on-object-tools.js +0 -225
|
@@ -4,7 +4,6 @@ import { jsLit, jsNum } from '../utils/jsx.js';
|
|
|
4
4
|
import { runScript } from '../utils/run-script.js';
|
|
5
5
|
import { TempDir, userOwnedTempRoot } from '../utils/temp.js';
|
|
6
6
|
import { validateArgs } from '../utils/validate.js';
|
|
7
|
-
import { overlayCoordinateGrid } from '../preview/coordinate-grid.js';
|
|
8
7
|
const COLOR_NAME_HEX = {
|
|
9
8
|
red: '#FF0000',
|
|
10
9
|
blue: '#0066FF',
|
|
@@ -129,47 +128,6 @@ const previewSchema = {
|
|
|
129
128
|
description: "Optional list of visual annotations to draw onto the preview before rendering — rectangles (by explicit bounds OR by layer name), guides (horizontal / vertical reference lines), point markers, and the current document selection. Use this to verify SPATIAL work (placement, alignment, scaling) — pass both your TARGET rect and the ACTUAL placement (e.g. the layer's bounds) in different colors, then visually compare in the returned image. Skips the spatial-self-evaluation trap where the LLM tries to estimate alignment from a plain preview.",
|
|
130
129
|
items: annotationSchema,
|
|
131
130
|
},
|
|
132
|
-
coordinate_grid: {
|
|
133
|
-
type: 'object',
|
|
134
|
-
description: 'Overlay a LABELED coordinate grid on the preview (drawn server-side) so you can read exact DOCUMENT-pixel coordinates straight off the lines instead of estimating. Reach for this BEFORE any tool that needs precise coordinates (rectangle/polygon selections, point-driven filters, future warp meshes). Each gridline is labeled with its document-pixel value (top-left origin, matching Photoshop coordinates). Either pass `step_px` (regular grid every N document px), OR `columns`/`rows` (even divisions), OR neither (auto ~10 divisions on the long edge). Composes with `annotations` (those draw first, in Photoshop; the grid is layered on top in Node).',
|
|
135
|
-
properties: {
|
|
136
|
-
step_px: {
|
|
137
|
-
type: 'number',
|
|
138
|
-
description: 'Regular grid: line spacing in DOCUMENT pixels. Wins over columns/rows.',
|
|
139
|
-
minimum: 1,
|
|
140
|
-
},
|
|
141
|
-
columns: {
|
|
142
|
-
type: 'integer',
|
|
143
|
-
description: 'Even-division grid: number of columns (draws columns−1 interior vertical lines). E.g. 3 = rule-of-thirds verticals.',
|
|
144
|
-
minimum: 2,
|
|
145
|
-
},
|
|
146
|
-
rows: {
|
|
147
|
-
type: 'integer',
|
|
148
|
-
description: 'Even-division grid: number of rows (draws rows−1 interior horizontal lines).',
|
|
149
|
-
minimum: 2,
|
|
150
|
-
},
|
|
151
|
-
label_every: {
|
|
152
|
-
type: 'integer',
|
|
153
|
-
description: 'Label every k-th line to reduce clutter on dense grids. Default 1 (every line).',
|
|
154
|
-
minimum: 1,
|
|
155
|
-
default: 1,
|
|
156
|
-
},
|
|
157
|
-
normalized: {
|
|
158
|
-
type: 'boolean',
|
|
159
|
-
description: 'Also print 0.0–1.0 normalized fraction labels along the bottom/right edges — for descriptors that take normalized coordinates (e.g. some filter centers). Default false.',
|
|
160
|
-
default: false,
|
|
161
|
-
},
|
|
162
|
-
color: {
|
|
163
|
-
type: 'string',
|
|
164
|
-
description: 'Hex #RRGGBB tint for the line/label bright component. Default white (with a black shadow/outline so it reads on any background).',
|
|
165
|
-
},
|
|
166
|
-
cell_labels: {
|
|
167
|
-
type: 'boolean',
|
|
168
|
-
description: 'Cell-label mode: divide the document into columns×rows cells (default 8×10) and label each A1/B2/… at its corner. Pick the CELL a feature is in (a reliable discrete choice) instead of reading a continuous coordinate — then zoom into that cell to refine. The legend gives the cell→pixel-region mapping. Default false.',
|
|
169
|
-
default: false,
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
131
|
},
|
|
174
132
|
};
|
|
175
133
|
const boundsDiffSchema = {
|
|
@@ -239,8 +197,8 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
239
197
|
return [
|
|
240
198
|
{
|
|
241
199
|
tool: {
|
|
242
|
-
name: '
|
|
243
|
-
description: 'Render the active Photoshop document as a flattened, downscaled image and return it inline so the calling AI can see the current visual state. Use this to verify edits visually, judge whether adjustments went too far or not far enough, and decide what to do next. Read-only with respect to the working document (renders a duplicate that is closed immediately). The optional `annotations` array draws overlays in document-space coordinates: `rectangle` / `guide` / `point` / `selection` for spatial verification (Bundle Q, 2026-06-02), and `grid` / `composition` for coordinate readout and compositional evaluation (added 2026-06-09 — grid styles `every` / `thirds` / `quarters` / `phi`, composition styles `diagonals` / `triangles` / `fibonacci_grid` / `golden_spiral`). Use spatial annotations to verify ("did the placed image land inside the frame interior?") — pass both target and actual bounds in different colors. Use grid
|
|
200
|
+
name: 'ps_get_preview',
|
|
201
|
+
description: 'Render the active Photoshop document as a flattened, downscaled image and return it inline so the calling AI can see the current visual state. Use this to verify edits visually, judge whether adjustments went too far or not far enough, and decide what to do next. Read-only with respect to the working document (renders a duplicate that is closed immediately). The optional `annotations` array draws overlays in document-space coordinates: `rectangle` / `guide` / `point` / `selection` for spatial verification (Bundle Q, 2026-06-02), and `grid` / `composition` for coordinate readout and compositional evaluation (added 2026-06-09 — grid styles `every` / `thirds` / `quarters` / `phi`, composition styles `diagonals` / `triangles` / `fibonacci_grid` / `golden_spiral`). Use spatial annotations to verify ("did the placed image land inside the frame interior?") — pass both target and actual bounds in different colors. Use grid / composition annotations to EVALUATE composition (balance, thirds, leading lines, focal-point placement) — not to read a coordinate for a tool call. To place or select precisely, do NOT read a pixel off a grid (unreliable) — NAME the intent to the on-device perception tools instead: `ps_detect` (COCO objects → real document-pixel boxes), `ps_select_by_reference` (a named target → a real selection, not a guessed rectangle), plus any spatial-grounding locator / selection tools present in `tools/list`. Without annotations, behaves identically to a plain preview.',
|
|
244
202
|
inputSchema: previewSchema,
|
|
245
203
|
outputSchema: {
|
|
246
204
|
type: 'object',
|
|
@@ -250,7 +208,6 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
250
208
|
dimensions: { type: 'string' },
|
|
251
209
|
bytes: { type: 'number' },
|
|
252
210
|
annotation_count: { type: 'number' },
|
|
253
|
-
coordinate_grid_applied: { type: 'boolean' },
|
|
254
211
|
context: { type: 'object' },
|
|
255
212
|
},
|
|
256
213
|
},
|
|
@@ -264,7 +221,7 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
264
221
|
},
|
|
265
222
|
{
|
|
266
223
|
tool: {
|
|
267
|
-
name: '
|
|
224
|
+
name: 'ps_get_layer_bounds_diff',
|
|
268
225
|
description: 'Numeric verification: compute the per-edge pixel deltas between a layer\'s actual bounds and a target rectangle. Returns left/top/right/bottom deltas (actual − target), scale ratio (actual_size / target_size), centroid offset, and a one-word verdict ("aligned", "shifted right", "layer too small", etc.). Use this AFTER place_image / scale_layer / move_layer to confirm the operation produced the intended result — far more reliable than eyeballing a preview for fine alignment. Read-only. Layers nested in groups are found by name recursively. Bundle Q (2026-06-02).',
|
|
269
226
|
inputSchema: boundsDiffSchema,
|
|
270
227
|
outputSchema: {
|
|
@@ -292,7 +249,7 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
292
249
|
},
|
|
293
250
|
{
|
|
294
251
|
tool: {
|
|
295
|
-
name: '
|
|
252
|
+
name: 'ps_get_histogram',
|
|
296
253
|
description: "Whole-image (or per-channel) histogram — the quantitative answer to questions get_preview can't reliably answer by eye. Returns 256 bin counts plus mean / stdev / median. Cheap (~50ms), read-only. **Reach for this when**: (a) clipping detection — bins 0 and 255 carry the count of crushed shadows / blown highlights; you can't see clipping reliably in a downsampled JPEG preview; (b) exposure verification after a levels/curves/exposure adjustment — confirm the histogram actually shifted; (c) \"is this image neutral-gray?\" — compare R / G / B channel means; (d) confirming a destructive op did anything (two identical histograms = no-op regardless of the tool's success message). Composite reads the visible flattened image (any color mode). Per-channel reads require the document have that channel.",
|
|
297
254
|
inputSchema: histogramSchema,
|
|
298
255
|
outputSchema: {
|
|
@@ -323,7 +280,7 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
323
280
|
},
|
|
324
281
|
{
|
|
325
282
|
tool: {
|
|
326
|
-
name: '
|
|
283
|
+
name: 'ps_compare_regions',
|
|
327
284
|
description: 'Quantitative region comparison — the NUMERIC verification answer when "does it look right?" via get_preview is too subjective. Captures the histogram of two rectangular regions on the same document and reports per-channel mean / stdev / median for each region plus the deltas. **Reach for this when**: (a) "did the placed image match the room\'s lighting?" — sample a region of the placed image vs a region of the surrounding wall; (b) "did this adjustment do anything visible in the shadows?" — sample the same dark region before and after; (c) "is region A noticeably warmer/cooler than region B?" — read the channel means directly. Single-pixel sampling via a 1×1 rect is supported. Read-only. Bundle Q (2026-06-02).',
|
|
328
285
|
inputSchema: compareRegionsSchema,
|
|
329
286
|
outputSchema: {
|
|
@@ -795,7 +752,6 @@ async function getPreview(connection, rawArgs) {
|
|
|
795
752
|
const maxDimension = args.max_dimension;
|
|
796
753
|
const quality = args.quality;
|
|
797
754
|
const annotations = args.annotations ?? [];
|
|
798
|
-
const coordinateGrid = args.coordinate_grid;
|
|
799
755
|
const annotationsScript = buildAnnotationsScript(annotations);
|
|
800
756
|
const dir = process.platform === 'darwin'
|
|
801
757
|
? await TempDir.createWithRoot(userOwnedTempRoot(), 'editmamei-preview-')
|
|
@@ -882,37 +838,7 @@ async function getPreview(connection, rawArgs) {
|
|
|
882
838
|
`;
|
|
883
839
|
const execTimeoutMs = annotations.length > 0 ? 90000 : undefined;
|
|
884
840
|
const result = (await runScript(connection, script, execTimeoutMs));
|
|
885
|
-
|
|
886
|
-
let gridNote = '';
|
|
887
|
-
let gridApplied = false;
|
|
888
|
-
if (coordinateGrid) {
|
|
889
|
-
const doc = result?.context
|
|
890
|
-
?.document;
|
|
891
|
-
if (typeof doc?.width === 'number' && typeof doc?.height === 'number') {
|
|
892
|
-
try {
|
|
893
|
-
const grid = overlayCoordinateGrid(bytes, {
|
|
894
|
-
docWidth: doc.width,
|
|
895
|
-
docHeight: doc.height,
|
|
896
|
-
stepPx: coordinateGrid.step_px,
|
|
897
|
-
columns: coordinateGrid.columns,
|
|
898
|
-
rows: coordinateGrid.rows,
|
|
899
|
-
labelEvery: coordinateGrid.label_every,
|
|
900
|
-
normalized: coordinateGrid.normalized,
|
|
901
|
-
color: coordinateGrid.color,
|
|
902
|
-
cellLabels: coordinateGrid.cell_labels,
|
|
903
|
-
});
|
|
904
|
-
bytes = grid.jpeg;
|
|
905
|
-
gridNote = ` ${grid.legend}`;
|
|
906
|
-
gridApplied = true;
|
|
907
|
-
}
|
|
908
|
-
catch (gridErr) {
|
|
909
|
-
gridNote = ` (coordinate grid skipped: ${gridErr instanceof Error ? gridErr.message : String(gridErr)})`;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
else {
|
|
913
|
-
gridNote = ' (coordinate grid skipped: document dimensions unavailable)';
|
|
914
|
-
}
|
|
915
|
-
}
|
|
841
|
+
const bytes = await readFile(tempPath);
|
|
916
842
|
const base64 = bytes.toString('base64');
|
|
917
843
|
const dims = result?.width_px && result?.height_px
|
|
918
844
|
? `${result.width_px}x${result.height_px}`
|
|
@@ -924,7 +850,7 @@ async function getPreview(connection, rawArgs) {
|
|
|
924
850
|
{ type: 'image', data: base64, mimeType: 'image/jpeg' },
|
|
925
851
|
{
|
|
926
852
|
type: 'text',
|
|
927
|
-
text: `Preview rendered (JPEG, ${dims}, ${formatBytes(bytes.length)}${annNote}). This is the current flattened state of the active document
|
|
853
|
+
text: `Preview rendered (JPEG, ${dims}, ${formatBytes(bytes.length)}${annNote}). This is the current flattened state of the active document.`,
|
|
928
854
|
},
|
|
929
855
|
],
|
|
930
856
|
structuredContent: {
|
|
@@ -933,7 +859,6 @@ async function getPreview(connection, rawArgs) {
|
|
|
933
859
|
dimensions: dims,
|
|
934
860
|
bytes: bytes.length,
|
|
935
861
|
annotation_count: annotationCount,
|
|
936
|
-
coordinate_grid_applied: gridApplied,
|
|
937
862
|
context: result?.context,
|
|
938
863
|
},
|
|
939
864
|
};
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { runScript } from '../utils/run-script.js';
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
|
+
import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
4
|
+
import { OnnxDetectionClient } from '../detection/detection-client.js';
|
|
5
|
+
async function groundedOffset(connection, snippetClient, detClient, placement, label) {
|
|
6
|
+
const rp = await resolveExpectedPlacement(connection, detClient, placement, 'point', label);
|
|
7
|
+
const sel = (await runScript(connection, await snippetClient.build('getSelectionState')));
|
|
8
|
+
if (!sel.has_selection || !sel.bounds)
|
|
9
|
+
throw new Error(`${label} needs an active selection to move relative to — none found. Make the selection first.`);
|
|
10
|
+
const center = {
|
|
11
|
+
x: (sel.bounds.left + sel.bounds.right) / 2,
|
|
12
|
+
y: (sel.bounds.top + sel.bounds.bottom) / 2,
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
offsetX: Math.round(rp.point.x - center.x),
|
|
16
|
+
offsetY: Math.round(rp.point.y - center.y),
|
|
17
|
+
point: rp.point,
|
|
18
|
+
center: { x: Math.round(center.x), y: Math.round(center.y) },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
3
21
|
const APPLY_TO_ACTIVE_LAYER_PROP = {
|
|
4
22
|
type: 'boolean',
|
|
5
23
|
description: 'If false (default), the retouch op is applied to a duplicate of the active layer named "<OpName> (<Original Name>)" — the original is preserved and the LLM can undo by deleting the copy. If true, the op bakes directly into the active layer.',
|
|
@@ -62,13 +80,17 @@ const contentAwareFillSchema = {
|
|
|
62
80
|
const patchSchema = {
|
|
63
81
|
type: 'object',
|
|
64
82
|
properties: {
|
|
83
|
+
source_placement: {
|
|
84
|
+
...PLACEMENT_SCHEMA,
|
|
85
|
+
description: 'Grounded alternative to offset_x/offset_y: NAME the patch SOURCE (resolves to a POINT via the grounding resolver + objective gate — e.g. an extremum for the cleanest nearby skin, a grid intersection). The offset from the selection center to that point is computed for you. Wins over offset_x/offset_y. Provide THIS or offset_x + offset_y.',
|
|
86
|
+
},
|
|
65
87
|
offset_x: {
|
|
66
88
|
type: 'number',
|
|
67
|
-
description: 'Horizontal pixel offset from the selection to the patch source. Positive = source is to the right. Required.',
|
|
89
|
+
description: 'Horizontal pixel offset from the selection to the patch source. Positive = source is to the right. Required unless source_placement is given.',
|
|
68
90
|
},
|
|
69
91
|
offset_y: {
|
|
70
92
|
type: 'number',
|
|
71
|
-
description: 'Vertical pixel offset from the selection to the patch source. Positive = source is below. Required.',
|
|
93
|
+
description: 'Vertical pixel offset from the selection to the patch source. Positive = source is below. Required unless source_placement is given.',
|
|
72
94
|
},
|
|
73
95
|
patch_structure: {
|
|
74
96
|
type: 'integer',
|
|
@@ -108,18 +130,21 @@ const patchSchema = {
|
|
|
108
130
|
},
|
|
109
131
|
apply_to_active_layer: APPLY_TO_ACTIVE_LAYER_PROP,
|
|
110
132
|
},
|
|
111
|
-
required: ['offset_x', 'offset_y'],
|
|
112
133
|
};
|
|
113
134
|
const contentAwareMoveSchema = {
|
|
114
135
|
type: 'object',
|
|
115
136
|
properties: {
|
|
137
|
+
destination_placement: {
|
|
138
|
+
...PLACEMENT_SCHEMA,
|
|
139
|
+
description: 'Grounded alternative to offset_x/offset_y: NAME where to move the selection (resolves to a POINT via the grounding resolver + objective gate — a grid intersection, an object centroid, an extremum). The selection center is moved ONTO that point; the offset is computed for you. Wins over offset_x/offset_y. Provide THIS or offset_x + offset_y.',
|
|
140
|
+
},
|
|
116
141
|
offset_x: {
|
|
117
142
|
type: 'number',
|
|
118
|
-
description: 'Horizontal pixel delta to move the selected content. Positive = right.',
|
|
143
|
+
description: 'Horizontal pixel delta to move the selected content. Positive = right. Required unless destination_placement is given.',
|
|
119
144
|
},
|
|
120
145
|
offset_y: {
|
|
121
146
|
type: 'number',
|
|
122
|
-
description: 'Vertical pixel delta to move the selected content. Positive = down.',
|
|
147
|
+
description: 'Vertical pixel delta to move the selected content. Positive = down. Required unless destination_placement is given.',
|
|
123
148
|
},
|
|
124
149
|
patch_structure: {
|
|
125
150
|
type: 'integer',
|
|
@@ -159,7 +184,6 @@ const contentAwareMoveSchema = {
|
|
|
159
184
|
},
|
|
160
185
|
apply_to_active_layer: APPLY_TO_ACTIVE_LAYER_PROP,
|
|
161
186
|
},
|
|
162
|
-
required: ['offset_x', 'offset_y'],
|
|
163
187
|
};
|
|
164
188
|
const RETOUCH_OUTPUT_SCHEMA = {
|
|
165
189
|
type: 'object',
|
|
@@ -217,11 +241,21 @@ async function applyContentAwareFill(connection, snippetClient, rawArgs) {
|
|
|
217
241
|
};
|
|
218
242
|
}
|
|
219
243
|
}
|
|
220
|
-
async function applyPatch(connection, snippetClient, rawArgs) {
|
|
244
|
+
async function applyPatch(connection, snippetClient, detClient, rawArgs) {
|
|
221
245
|
try {
|
|
222
246
|
const args = validateArgs(patchSchema, rawArgs);
|
|
223
|
-
|
|
224
|
-
|
|
247
|
+
let offsetX = args.offset_x;
|
|
248
|
+
let offsetY = args.offset_y;
|
|
249
|
+
let grounded;
|
|
250
|
+
if (args.source_placement) {
|
|
251
|
+
const g = await groundedOffset(connection, snippetClient, detClient, args.source_placement, 'patch source');
|
|
252
|
+
offsetX = g.offsetX;
|
|
253
|
+
offsetY = g.offsetY;
|
|
254
|
+
grounded = { point: g.point, center: g.center };
|
|
255
|
+
}
|
|
256
|
+
else if (offsetX === undefined || offsetY === undefined) {
|
|
257
|
+
throw new Error('provide offset_x + offset_y, or source_placement to NAME the patch source region.');
|
|
258
|
+
}
|
|
225
259
|
const patchStructure = args.patch_structure ?? 5;
|
|
226
260
|
const patchColor = args.patch_color ?? 5;
|
|
227
261
|
const healSmoothFactor = args.heal_smooth_factor ?? 5;
|
|
@@ -245,14 +279,26 @@ async function applyPatch(connection, snippetClient, rawArgs) {
|
|
|
245
279
|
const target = r.target_was_copy
|
|
246
280
|
? `new copy "${r.target_layer_name ?? '?'}"`
|
|
247
281
|
: 'active layer (in place)';
|
|
282
|
+
const groundedNote = grounded
|
|
283
|
+
? ` — source at (${Math.round(grounded.point.x)},${Math.round(grounded.point.y)}) via placement (gate PASS)`
|
|
284
|
+
: '';
|
|
285
|
+
const structured = result;
|
|
286
|
+
if (grounded) {
|
|
287
|
+
structured.source_placement = {
|
|
288
|
+
gate: { pass: true },
|
|
289
|
+
point: { x: Math.round(grounded.point.x), y: Math.round(grounded.point.y) },
|
|
290
|
+
selection_center: grounded.center,
|
|
291
|
+
offset: { x: offsetX, y: offsetY },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
248
294
|
return {
|
|
249
295
|
content: [
|
|
250
296
|
{
|
|
251
297
|
type: 'text',
|
|
252
|
-
text: `Patch applied to ${target} (offset ${offsetX}, ${offsetY}; structure ${patchStructure}, color ${patchColor}).`,
|
|
298
|
+
text: `Patch applied to ${target} (offset ${offsetX}, ${offsetY}; structure ${patchStructure}, color ${patchColor})${groundedNote}.`,
|
|
253
299
|
},
|
|
254
300
|
],
|
|
255
|
-
structuredContent:
|
|
301
|
+
structuredContent: structured,
|
|
256
302
|
};
|
|
257
303
|
}
|
|
258
304
|
catch (error) {
|
|
@@ -267,11 +313,21 @@ async function applyPatch(connection, snippetClient, rawArgs) {
|
|
|
267
313
|
};
|
|
268
314
|
}
|
|
269
315
|
}
|
|
270
|
-
async function applyContentAwareMove(connection, snippetClient, rawArgs) {
|
|
316
|
+
async function applyContentAwareMove(connection, snippetClient, detClient, rawArgs) {
|
|
271
317
|
try {
|
|
272
318
|
const args = validateArgs(contentAwareMoveSchema, rawArgs);
|
|
273
|
-
|
|
274
|
-
|
|
319
|
+
let offsetX = args.offset_x;
|
|
320
|
+
let offsetY = args.offset_y;
|
|
321
|
+
let grounded;
|
|
322
|
+
if (args.destination_placement) {
|
|
323
|
+
const g = await groundedOffset(connection, snippetClient, detClient, args.destination_placement, 'content-aware move');
|
|
324
|
+
offsetX = g.offsetX;
|
|
325
|
+
offsetY = g.offsetY;
|
|
326
|
+
grounded = { point: g.point, center: g.center };
|
|
327
|
+
}
|
|
328
|
+
else if (offsetX === undefined || offsetY === undefined) {
|
|
329
|
+
throw new Error('provide offset_x + offset_y, or destination_placement to NAME where to move the selection.');
|
|
330
|
+
}
|
|
275
331
|
const patchStructure = args.patch_structure ?? 4;
|
|
276
332
|
const patchColor = args.patch_color ?? 5;
|
|
277
333
|
const healSmoothFactor = args.heal_smooth_factor ?? 5;
|
|
@@ -295,14 +351,26 @@ async function applyContentAwareMove(connection, snippetClient, rawArgs) {
|
|
|
295
351
|
const target = r.target_was_copy
|
|
296
352
|
? `new copy "${r.target_layer_name ?? '?'}"`
|
|
297
353
|
: 'active layer (in place)';
|
|
354
|
+
const groundedNote = grounded
|
|
355
|
+
? ` — moved onto (${Math.round(grounded.point.x)},${Math.round(grounded.point.y)}) via placement (gate PASS)`
|
|
356
|
+
: '';
|
|
357
|
+
const structured = result;
|
|
358
|
+
if (grounded) {
|
|
359
|
+
structured.destination_placement = {
|
|
360
|
+
gate: { pass: true },
|
|
361
|
+
point: { x: Math.round(grounded.point.x), y: Math.round(grounded.point.y) },
|
|
362
|
+
selection_center: grounded.center,
|
|
363
|
+
offset: { x: offsetX, y: offsetY },
|
|
364
|
+
};
|
|
365
|
+
}
|
|
298
366
|
return {
|
|
299
367
|
content: [
|
|
300
368
|
{
|
|
301
369
|
type: 'text',
|
|
302
|
-
text: `Content-Aware Move applied to ${target} (delta ${offsetX}, ${offsetY}).`,
|
|
370
|
+
text: `Content-Aware Move applied to ${target} (delta ${offsetX}, ${offsetY})${groundedNote}.`,
|
|
303
371
|
},
|
|
304
372
|
],
|
|
305
|
-
structuredContent:
|
|
373
|
+
structuredContent: structured,
|
|
306
374
|
};
|
|
307
375
|
}
|
|
308
376
|
catch (error) {
|
|
@@ -326,8 +394,8 @@ const RETOUCH_INPUT_SCHEMA = {
|
|
|
326
394
|
enum: [...RETOUCH_METHODS],
|
|
327
395
|
description: 'Retouch technique (all require an active selection). ' +
|
|
328
396
|
'content_aware_fill: synthesize a fill from surrounding content (color_adaptation, opacity, blend_mode, rotate/scale/mirror). ' +
|
|
329
|
-
'patch: offset_x + offset_y
|
|
330
|
-
'content_aware_move: offset_x + offset_y
|
|
397
|
+
'patch: sample replacement pixels — offset_x + offset_y, OR source_placement to NAME the clean source region (patch_structure 1-7, patch_color 0-10, use_source). ' +
|
|
398
|
+
'content_aware_move: relocate the selection and fill the source — offset_x + offset_y, OR destination_placement to NAME where to move it (patch_structure, reshuffle).',
|
|
331
399
|
},
|
|
332
400
|
...contentAwareFillSchema.properties,
|
|
333
401
|
...patchSchema.properties,
|
|
@@ -335,12 +403,12 @@ const RETOUCH_INPUT_SCHEMA = {
|
|
|
335
403
|
},
|
|
336
404
|
required: ['method'],
|
|
337
405
|
};
|
|
338
|
-
export function createRetouchTools(connection, snippetClient) {
|
|
406
|
+
export function createRetouchTools(connection, snippetClient, detClient = new OnnxDetectionClient()) {
|
|
339
407
|
return [
|
|
340
408
|
{
|
|
341
409
|
tool: {
|
|
342
|
-
name: '
|
|
343
|
-
description: 'Selection-driven content-aware retouch — choose the technique with `method`. Requires an active selection (throws if none, so Content-Aware Fill never silently fills the whole layer). Auto-duplicates the active layer by default so the original is preserved (auto-duplicate-first per Bundle O); pass `apply_to_active_layer: true` to bake into the original. `content_aware_fill` synthesizes a fill from surroundings (remove wires/objects/blemishes); `patch` samples replacement pixels from a chosen offset
|
|
410
|
+
name: 'ps_retouch',
|
|
411
|
+
description: 'Selection-driven content-aware retouch — choose the technique with `method`. Requires an active selection (throws if none, so Content-Aware Fill never silently fills the whole layer). Auto-duplicates the active layer by default so the original is preserved (auto-duplicate-first per Bundle O); pass `apply_to_active_layer: true` to bake into the original. `content_aware_fill` synthesizes a fill from surroundings (remove wires/objects/blemishes); `patch` samples replacement pixels from a chosen offset — or NAME the source region with `source_placement` (grounded + gated); `content_aware_move` relocates the selection and fills the source — offset, or NAME where with `destination_placement`.',
|
|
344
412
|
inputSchema: RETOUCH_INPUT_SCHEMA,
|
|
345
413
|
outputSchema: RETOUCH_OUTPUT_SCHEMA,
|
|
346
414
|
annotations: {
|
|
@@ -349,20 +417,20 @@ export function createRetouchTools(connection, snippetClient) {
|
|
|
349
417
|
idempotentHint: false,
|
|
350
418
|
},
|
|
351
419
|
},
|
|
352
|
-
handler: async (args) => retouch(connection, snippetClient, args),
|
|
420
|
+
handler: async (args) => retouch(connection, snippetClient, detClient, args),
|
|
353
421
|
},
|
|
354
422
|
];
|
|
355
423
|
}
|
|
356
|
-
async function retouch(connection, snippetClient, rawArgs) {
|
|
424
|
+
async function retouch(connection, snippetClient, detClient, rawArgs) {
|
|
357
425
|
const method = rawArgs.method;
|
|
358
426
|
const { method: _omit, ...rest } = rawArgs;
|
|
359
427
|
switch (method) {
|
|
360
428
|
case 'content_aware_fill':
|
|
361
429
|
return applyContentAwareFill(connection, snippetClient, rest);
|
|
362
430
|
case 'patch':
|
|
363
|
-
return applyPatch(connection, snippetClient, rest);
|
|
431
|
+
return applyPatch(connection, snippetClient, detClient, rest);
|
|
364
432
|
case 'content_aware_move':
|
|
365
|
-
return applyContentAwareMove(connection, snippetClient, rest);
|
|
433
|
+
return applyContentAwareMove(connection, snippetClient, detClient, rest);
|
|
366
434
|
default:
|
|
367
435
|
return {
|
|
368
436
|
content: [
|
|
@@ -9,7 +9,7 @@ function makeProRefine(invokeTool) {
|
|
|
9
9
|
try {
|
|
10
10
|
if (req.kind === 'subject') {
|
|
11
11
|
const which = typeof req.instance === 'number' ? String(req.instance) : 'largest';
|
|
12
|
-
const res = await invokeTool('
|
|
12
|
+
const res = await invokeTool('ps_select_subject_instance', {
|
|
13
13
|
label: req.label,
|
|
14
14
|
which,
|
|
15
15
|
save_as: 'selection',
|
|
@@ -17,7 +17,7 @@ function makeProRefine(invokeTool) {
|
|
|
17
17
|
return res.isError !== true;
|
|
18
18
|
}
|
|
19
19
|
const feature = req.kind === 'face' ? 'face' : req.feature;
|
|
20
|
-
const res = await invokeTool('
|
|
20
|
+
const res = await invokeTool('ps_select_face_feature', {
|
|
21
21
|
feature,
|
|
22
22
|
which: 'best',
|
|
23
23
|
save_as: 'selection',
|
|
@@ -32,7 +32,7 @@ function makeProRefine(invokeTool) {
|
|
|
32
32
|
function makePrecomputeFace(invokeTool) {
|
|
33
33
|
return async () => {
|
|
34
34
|
try {
|
|
35
|
-
const res = await invokeTool('
|
|
35
|
+
const res = await invokeTool('ps_select_face_feature', { precompute_all: true });
|
|
36
36
|
if (res.isError)
|
|
37
37
|
return [];
|
|
38
38
|
const saved = res.structuredContent?.saved ?? [];
|
|
@@ -73,11 +73,11 @@ const sceneSchema = {
|
|
|
73
73
|
save_regions: {
|
|
74
74
|
type: 'boolean',
|
|
75
75
|
default: true,
|
|
76
|
-
description: 'Precompute every confident region (sky/ground/shadows/highlights/skin/subject/face — and, on a Pro host with a face, the face-feature set scene:face_skin/_eyes/_brows/_lips/_teeth/_nose/_under_eye/_cheeks) and SAVE each as a managed `scene:*` alpha channel, returning the `regions` MENU of what is confidently selectable (each with its method + confidence).
|
|
76
|
+
description: 'Precompute every confident region (sky/ground/shadows/highlights/skin/subject/face — and, on a Pro host with a face, the face-feature set scene:face_skin/_eyes/_brows/_lips/_teeth/_nose/_under_eye/_cheeks) and SAVE each as a managed `scene:*` alpha channel, returning the `regions` MENU of what is confidently selectable (each with its method + confidence). ps_select_by_reference then loads the saved channel instantly. Set false for a light read with no channels.',
|
|
77
77
|
},
|
|
78
78
|
composition_context: {
|
|
79
79
|
type: 'object',
|
|
80
|
-
description: 'Optional: tune the confidence gate for an artistic/non-standard composition (e.g. profile:big_sky) so a legitimately large region is not rejected. Same shape as
|
|
80
|
+
description: 'Optional: tune the confidence gate for an artistic/non-standard composition (e.g. profile:big_sky) so a legitimately large region is not rejected. Same shape as ps_select_by_reference.',
|
|
81
81
|
properties: {
|
|
82
82
|
profile: {
|
|
83
83
|
type: 'string',
|
|
@@ -149,7 +149,7 @@ function summarizeScene(model) {
|
|
|
149
149
|
];
|
|
150
150
|
if (main && cell)
|
|
151
151
|
parts.push(`main subject in the ${cell.row}-${cell.col} third`);
|
|
152
|
-
return `Scene (${model.doc.width}×${model.doc.height}): ${parts.join('; ')}. Select named regions with
|
|
152
|
+
return `Scene (${model.doc.width}×${model.doc.height}): ${parts.join('; ')}. Select named regions with ps_select_by_reference.`;
|
|
153
153
|
}
|
|
154
154
|
async function scene(connection, snippet, client, rawArgs, proRefine, precomputeFace) {
|
|
155
155
|
try {
|
|
@@ -284,7 +284,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
|
|
|
284
284
|
content: [
|
|
285
285
|
{
|
|
286
286
|
type: 'text',
|
|
287
|
-
text: `Selected "${target}" from the saved scene:${target} channel (precomputed by
|
|
287
|
+
text: `Selected "${target}" from the saved scene:${target} channel (precomputed by ps_read_scene). Verify with ps_get_selection_preview.`,
|
|
288
288
|
},
|
|
289
289
|
],
|
|
290
290
|
structuredContent: {
|
|
@@ -314,7 +314,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
|
|
|
314
314
|
});
|
|
315
315
|
const conf = res.confidence.toFixed(2);
|
|
316
316
|
const summary = res.passed
|
|
317
|
-
? `Selected "${target}" via ${res.method} (confidence ${conf}). Verify with
|
|
317
|
+
? `Selected "${target}" via ${res.method} (confidence ${conf}). Verify with ps_get_selection_preview.`
|
|
318
318
|
: `No confident "${target}" in this image (confidence ${conf}, below the gate) — nothing selected. ${res.reasons.join('; ')}. If this is an artistic/non-standard composition, pass composition_context to relax the prior.`;
|
|
319
319
|
return {
|
|
320
320
|
content: [{ type: 'text', text: summary }],
|
|
@@ -334,7 +334,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
|
|
|
334
334
|
catch (error) {
|
|
335
335
|
const msg = error instanceof Error ? error.message : String(error);
|
|
336
336
|
return {
|
|
337
|
-
content: [{ type: 'text', text: `Error in
|
|
337
|
+
content: [{ type: 'text', text: `Error in ps_select_by_reference: ${msg}` }],
|
|
338
338
|
isError: true,
|
|
339
339
|
};
|
|
340
340
|
}
|
|
@@ -345,8 +345,8 @@ export function createSceneTools(connection, snippetClient, client = new OnnxDet
|
|
|
345
345
|
return [
|
|
346
346
|
{
|
|
347
347
|
tool: {
|
|
348
|
-
name: '
|
|
349
|
-
description: 'Build a structured SCENE MODEL of the active document using LOCAL on-device vision + classical CV — the "look before you select" read. Returns 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), and composition geometry (which thirds cell the subject sits in, balance, headroom), plus an annotated preview. The image never leaves the machine. Read-only: renders a throwaway duplicate. Run this first to SEE the menu of selectable named regions, then select them by name with
|
|
348
|
+
name: 'ps_read_scene',
|
|
349
|
+
description: 'Build a structured SCENE MODEL of the active document using LOCAL on-device vision + classical CV — the "look before you select" read. Returns 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), and composition geometry (which thirds cell the subject sits in, balance, headroom), plus an annotated preview. The image never leaves the machine. Read-only: renders a throwaway duplicate. Run this first to SEE the menu of selectable named regions, then select them by name with ps_select_by_reference instead of guessing a rectangle. Perception is cached per document state, so repeated reads are cheap.',
|
|
350
350
|
inputSchema: sceneSchema,
|
|
351
351
|
outputSchema: {
|
|
352
352
|
type: 'object',
|
|
@@ -372,8 +372,8 @@ export function createSceneTools(connection, snippetClient, client = new OnnxDet
|
|
|
372
372
|
},
|
|
373
373
|
{
|
|
374
374
|
tool: {
|
|
375
|
-
name: '
|
|
376
|
-
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, loaded instantly from the scene:face_* channel
|
|
375
|
+
name: 'ps_select_by_reference',
|
|
376
|
+
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, loaded instantly from the scene:face_* channel ps_read_scene precomputes. `passed`/`confidence` are returned. 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.',
|
|
377
377
|
inputSchema: selectByReferenceSchema,
|
|
378
378
|
outputSchema: {
|
|
379
379
|
type: 'object',
|