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.
Files changed (72) hide show
  1. package/NOTICES.md +17 -1
  2. package/README.md +9 -8
  3. package/dist/api/extendscript/_helpers.js +1 -1
  4. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  5. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  6. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  7. package/dist/cli/config.js +3 -3
  8. package/dist/cli/help.js +4 -0
  9. package/dist/cli/install.js +2 -2
  10. package/dist/cli/report.js +11 -0
  11. package/dist/cli/router.js +20 -0
  12. package/dist/core/server.js +8 -8
  13. package/dist/core/settings.js +2 -2
  14. package/dist/core/tool-groups.js +92 -82
  15. package/dist/core/tool-tiers.js +91 -81
  16. package/dist/detection/landmark-detection-client.js +8 -0
  17. package/dist/detection/mesh-eye-correction.js +126 -0
  18. package/dist/detection/sam-segmenter.js +83 -0
  19. package/dist/diagnostics/collect.js +169 -0
  20. package/dist/index.js +2 -0
  21. package/dist/license/entitlement.js +52 -0
  22. package/dist/modules/ce/index.js +6 -4
  23. package/dist/perception/grounding-anchors.js +37 -0
  24. package/dist/perception/grounding-corners.js +93 -0
  25. package/dist/perception/grounding-edge-trace.js +90 -0
  26. package/dist/perception/grounding-extrema.js +41 -0
  27. package/dist/perception/grounding-gate.js +154 -0
  28. package/dist/perception/grounding-geometry.js +132 -0
  29. package/dist/perception/grounding-grid.js +34 -0
  30. package/dist/perception/grounding-landmarks.js +40 -0
  31. package/dist/perception/grounding-locate.js +341 -0
  32. package/dist/perception/grounding-resolver.js +202 -0
  33. package/dist/perception/grounding-review-crop.js +85 -0
  34. package/dist/perception/grounding-warp.js +182 -0
  35. package/dist/perception/sky-mask-transfer.js +50 -18
  36. package/dist/skills/editmamei-skill.zip +0 -0
  37. package/dist/tools/adjustment-tools.js +3 -3
  38. package/dist/tools/brush-tools.js +111 -23
  39. package/dist/tools/channel-compose-tools.js +241 -0
  40. package/dist/tools/detection-tools.js +1 -1
  41. package/dist/tools/diagnostics-tools.js +66 -0
  42. package/dist/tools/document-tools.js +6 -6
  43. package/dist/tools/filter-tools.js +476 -19
  44. package/dist/tools/group-tools.js +10 -10
  45. package/dist/tools/guide-tools.js +4 -4
  46. package/dist/tools/history-tools.js +4 -4
  47. package/dist/tools/image-placement-tools.js +1 -1
  48. package/dist/tools/image-tools.js +62 -19
  49. package/dist/tools/inspect-tools.js +2 -2
  50. package/dist/tools/layer-ordering-tools.js +1 -1
  51. package/dist/tools/layer-properties-tools.js +42 -23
  52. package/dist/tools/layer-tools.js +10 -10
  53. package/dist/tools/layer-transform-tools.js +80 -41
  54. package/dist/tools/overview-tools.js +42 -42
  55. package/dist/tools/path-tools.js +53 -10
  56. package/dist/tools/portrait-tools.js +3 -3
  57. package/dist/tools/preview-tools.js +7 -82
  58. package/dist/tools/retouch-tools.js +93 -25
  59. package/dist/tools/scene-tools.js +13 -13
  60. package/dist/tools/selection-tools.js +319 -40
  61. package/dist/tools/shape-tools.js +216 -0
  62. package/dist/tools/text-tools.js +1 -1
  63. package/dist/tools/transform-canvas-tools.js +2 -2
  64. package/dist/tools/vector-mask-tools.js +23 -8
  65. package/dist/utils/log-buffer.js +41 -0
  66. package/dist/utils/logger.js +12 -1
  67. package/dist/utils/session-log-reader.js +27 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +1 -1
  70. package/dist/preview/coordinate-grid.js +0 -250
  71. package/dist/tools/object-tools.js +0 -193
  72. package/dist/tools/text-on-object-tools.js +0 -225
@@ -1,250 +0,0 @@
1
- import { decode, encode } from 'jpeg-js';
2
- const FONT_5x7 = {
3
- '0': [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
4
- '1': [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
5
- '2': [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
6
- '3': [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110],
7
- '4': [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
8
- '5': [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
9
- '6': [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
10
- '7': [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
11
- '8': [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
12
- '9': [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
13
- '-': [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000],
14
- '.': [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100],
15
- A: [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
16
- B: [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
17
- C: [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
18
- D: [0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110],
19
- E: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
20
- F: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
21
- G: [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111],
22
- H: [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
23
- I: [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
24
- J: [0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100],
25
- K: [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
26
- L: [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],
27
- M: [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001],
28
- N: [0b10001, 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001],
29
- O: [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
30
- P: [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
31
- Q: [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],
32
- R: [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],
33
- S: [0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110],
34
- T: [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
35
- U: [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
36
- V: [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],
37
- W: [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001],
38
- X: [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001],
39
- Y: [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100],
40
- Z: [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],
41
- };
42
- const GLYPH_W = 5;
43
- const GLYPH_H = 7;
44
- function hexToRgb(hex, fallback) {
45
- if (!hex || hex[0] !== '#' || hex.length !== 7)
46
- return fallback;
47
- const r = parseInt(hex.slice(1, 3), 16);
48
- const g = parseInt(hex.slice(3, 5), 16);
49
- const b = parseInt(hex.slice(5, 7), 16);
50
- if ([r, g, b].some((v) => Number.isNaN(v)))
51
- return fallback;
52
- return { r, g, b };
53
- }
54
- const WHITE = { r: 255, g: 255, b: 255 };
55
- const BLACK = { r: 0, g: 0, b: 0 };
56
- function setPx(data, w, h, x, y, c) {
57
- if (x < 0 || y < 0 || x >= w || y >= h)
58
- return;
59
- const i = (y * w + x) * 4;
60
- data[i] = c.r;
61
- data[i + 1] = c.g;
62
- data[i + 2] = c.b;
63
- data[i + 3] = 255;
64
- }
65
- function drawVLine(data, w, h, x, core) {
66
- for (let y = 0; y < h; y++) {
67
- setPx(data, w, h, x, y, core);
68
- setPx(data, w, h, x + 1, y, BLACK);
69
- }
70
- }
71
- function drawHLine(data, w, h, y, core) {
72
- for (let x = 0; x < w; x++) {
73
- setPx(data, w, h, x, y, core);
74
- setPx(data, w, h, x, y + 1, BLACK);
75
- }
76
- }
77
- function drawGlyph(data, w, h, glyph, px, py, scale, text) {
78
- for (let row = 0; row < GLYPH_H; row++) {
79
- const bits = glyph[row];
80
- for (let col = 0; col < GLYPH_W; col++) {
81
- if (!(bits & (1 << (GLYPH_W - 1 - col))))
82
- continue;
83
- const bx = px + col * scale;
84
- const by = py + row * scale;
85
- for (let oy = -1; oy <= scale; oy++) {
86
- for (let ox = -1; ox <= scale; ox++) {
87
- setPx(data, w, h, bx + ox, by + oy, BLACK);
88
- }
89
- }
90
- for (let sy = 0; sy < scale; sy++) {
91
- for (let sx = 0; sx < scale; sx++) {
92
- setPx(data, w, h, bx + sx, by + sy, text);
93
- }
94
- }
95
- }
96
- }
97
- }
98
- function textWidth(str, scale) {
99
- return str.length * (GLYPH_W + 1) * scale;
100
- }
101
- function drawText(data, w, h, str, px, py, scale, text) {
102
- let cx = px;
103
- for (const ch of str) {
104
- const glyph = FONT_5x7[ch];
105
- if (glyph)
106
- drawGlyph(data, w, h, glyph, cx, py, scale, text);
107
- cx += (GLYPH_W + 1) * scale;
108
- }
109
- }
110
- export function niceStep(target) {
111
- if (!(target > 0))
112
- return 1;
113
- const mag = Math.pow(10, Math.floor(Math.log10(target)));
114
- const norm = target / mag;
115
- const candidates = [1, 2, 5, 10];
116
- let best = 1;
117
- let bestDist = Infinity;
118
- for (const c of candidates) {
119
- const d = Math.abs(norm - c);
120
- if (d <= bestDist) {
121
- bestDist = d;
122
- best = c;
123
- }
124
- }
125
- return Math.max(1, Math.round(best * mag));
126
- }
127
- export function colLabel(i) {
128
- let n = Math.floor(i) + 1;
129
- let s = '';
130
- while (n > 0) {
131
- const r = (n - 1) % 26;
132
- s = String.fromCharCode(65 + r) + s;
133
- n = Math.floor((n - 1) / 26);
134
- }
135
- return s;
136
- }
137
- function computeLines(opts) {
138
- const { docWidth, docHeight } = opts;
139
- const vertical = [];
140
- const horizontal = [];
141
- if (typeof opts.columns === 'number' && opts.columns >= 2) {
142
- for (let i = 1; i < opts.columns; i++)
143
- vertical.push(Math.round((docWidth * i) / opts.columns));
144
- }
145
- if (typeof opts.rows === 'number' && opts.rows >= 2) {
146
- for (let j = 1; j < opts.rows; j++)
147
- horizontal.push(Math.round((docHeight * j) / opts.rows));
148
- }
149
- if (vertical.length > 0 || horizontal.length > 0) {
150
- return { vertical, horizontal, step: null };
151
- }
152
- const longEdge = Math.max(docWidth, docHeight);
153
- const requested = typeof opts.stepPx === 'number' && opts.stepPx > 0 ? opts.stepPx : niceStep(longEdge / 10);
154
- const MAX_LINES_PER_AXIS = 80;
155
- const step = Math.max(requested, Math.ceil(longEdge / MAX_LINES_PER_AXIS));
156
- for (let x = step; x < docWidth; x += step)
157
- vertical.push(x);
158
- for (let y = step; y < docHeight; y += step)
159
- horizontal.push(y);
160
- return { vertical, horizontal, step };
161
- }
162
- export function overlayCoordinateGrid(jpegBuffer, opts) {
163
- const img = decode(jpegBuffer, { useTArray: true, formatAsRGBA: true });
164
- const w = img.width;
165
- const h = img.height;
166
- const data = img.data;
167
- const scaleX = w / opts.docWidth;
168
- const scaleY = h / opts.docHeight;
169
- const core = hexToRgb(opts.color, WHITE);
170
- const labelEvery = Math.max(1, Math.floor(opts.labelEvery ?? 1));
171
- const glyphScale = Math.max(2, Math.round(Math.max(w, h) / 600));
172
- const labelH = GLYPH_H * glyphScale;
173
- if (opts.cellLabels) {
174
- const numCols = Math.max(1, Math.floor(opts.columns ?? 8));
175
- const numRows = Math.max(1, Math.floor(opts.rows ?? 10));
176
- const cellW = opts.docWidth / numCols;
177
- const cellH = opts.docHeight / numRows;
178
- for (let i = 1; i < numCols; i++)
179
- drawVLine(data, w, h, Math.round(i * cellW * scaleX), core);
180
- for (let j = 1; j < numRows; j++)
181
- drawHLine(data, w, h, Math.round(j * cellH * scaleY), core);
182
- for (let i = 0; i < numCols; i++) {
183
- for (let j = 0; j < numRows; j++) {
184
- const px = Math.round(i * cellW * scaleX) + 3;
185
- const py = Math.round(j * cellH * scaleY) + 3;
186
- drawText(data, w, h, colLabel(i) + (j + 1), px, py, glyphScale, core);
187
- }
188
- }
189
- const out = encode({ data, width: w, height: h }, 85);
190
- const cw = Math.round(cellW);
191
- const ch = Math.round(cellH);
192
- const legend = `Cell grid: ${numCols} columns (A–${colLabel(numCols - 1)}) × ${numRows} rows (1–${numRows}) ` +
193
- `over a ${opts.docWidth}×${opts.docHeight} px document; each cell ≈ ${cw}×${ch} doc px. ` +
194
- `Pick the CELL a feature sits in. Cell <Col><Row> covers x=[(col−1)·${cw} … col·${cw}], ` +
195
- `y=[(row−1)·${ch} … row·${ch}] (col A=1…${colLabel(numCols - 1)}=${numCols}); ` +
196
- `the cell center is x=(col−0.5)·${cw}, y=(row−0.5)·${ch}.`;
197
- return {
198
- jpeg: out.data,
199
- legend,
200
- verticalLines: Array.from({ length: numCols - 1 }, (_, i) => Math.round((i + 1) * cellW)),
201
- horizontalLines: Array.from({ length: numRows - 1 }, (_, j) => Math.round((j + 1) * cellH)),
202
- previewWidth: w,
203
- previewHeight: h,
204
- };
205
- }
206
- const { vertical, horizontal, step } = computeLines(opts);
207
- for (const docX of vertical)
208
- drawVLine(data, w, h, Math.round(docX * scaleX), core);
209
- for (const docY of horizontal)
210
- drawHLine(data, w, h, Math.round(docY * scaleY), core);
211
- for (let i = 0; i < vertical.length; i++) {
212
- if (i % labelEvery !== 0)
213
- continue;
214
- const docX = vertical[i];
215
- const px = Math.round(docX * scaleX);
216
- drawText(data, w, h, String(docX), px + 3, 2, glyphScale, core);
217
- if (opts.normalized) {
218
- const frac = (docX / opts.docWidth).toFixed(2);
219
- drawText(data, w, h, frac, px + 3, h - labelH - 2, glyphScale, core);
220
- }
221
- }
222
- for (let j = 0; j < horizontal.length; j++) {
223
- if (j % labelEvery !== 0)
224
- continue;
225
- const docY = horizontal[j];
226
- const py = Math.round(docY * scaleY);
227
- drawText(data, w, h, String(docY), 2, py + 3, glyphScale, core);
228
- if (opts.normalized) {
229
- const frac = (docY / opts.docHeight).toFixed(2);
230
- const s = frac;
231
- drawText(data, w, h, s, w - textWidth(s, glyphScale) - 2, py + 3, glyphScale, core);
232
- }
233
- }
234
- const out = encode({ data, width: w, height: h }, 85);
235
- const spacingNote = step !== null
236
- ? `lines every ${step} px`
237
- : `${vertical.length + 1}×${horizontal.length + 1} divisions`;
238
- const normNote = opts.normalized ? '; normalized 0–1 labels on bottom/right edges' : '';
239
- const legend = `Coordinate grid: ${spacingNote}, labeled in DOCUMENT pixels, top-left origin. ` +
240
- `Document is ${opts.docWidth}×${opts.docHeight} px (preview ${w}×${h}). ` +
241
- `Read the x/y value off a labeled line and pass it straight to a tool${normNote}.`;
242
- return {
243
- jpeg: out.data,
244
- legend,
245
- verticalLines: vertical,
246
- horizontalLines: horizontal,
247
- previewWidth: w,
248
- previewHeight: h,
249
- };
250
- }
@@ -1,193 +0,0 @@
1
- import { runScript } from '../utils/run-script.js';
2
- import { validateArgs } from '../utils/validate.js';
3
- import { detectActiveDoc } from '../detection/detect-active-doc.js';
4
- import { OnnxDetectionClient, } from '../detection/detection-client.js';
5
- const OBJECT_OPS = ['remove', 'blur_background', 'spotlight'];
6
- const objectSchema = {
7
- type: 'object',
8
- properties: {
9
- op: {
10
- type: 'string',
11
- enum: [...OBJECT_OPS],
12
- description: 'remove: erase the matched object(s) via Content-Aware Fill (background person, photobomber, sign). blur_background: keep the matched object(s) sharp, blur everything else (fake depth of field). spotlight: keep the matched object(s) lit, darken everything else.',
13
- },
14
- label: {
15
- type: 'string',
16
- description: "COCO class to target (e.g. 'person', 'dog', 'car', 'chair'). For remove this is what gets erased; for blur_background/spotlight it is the SUBJECT to keep.",
17
- },
18
- all: {
19
- type: 'boolean',
20
- default: false,
21
- description: 'Act on ALL detected instances of the label. Default false = the single highest-confidence one.',
22
- },
23
- amount: {
24
- type: 'number',
25
- minimum: 1,
26
- maximum: 250,
27
- description: 'blur_background: blur radius in px (default 25). spotlight: darken strength 1–100 (default 45). Ignored by remove.',
28
- },
29
- object_threshold: {
30
- type: 'number',
31
- default: 0.4,
32
- minimum: 0,
33
- maximum: 1,
34
- description: 'Minimum detection confidence (0–1).',
35
- },
36
- max_dimension: {
37
- type: 'number',
38
- default: 1024,
39
- minimum: 256,
40
- maximum: 4096,
41
- description: 'Long-edge px of the detection export. Coordinates always return in document px.',
42
- },
43
- },
44
- required: ['op', 'label'],
45
- };
46
- function pickObjects(objects, label, all) {
47
- const matches = objects
48
- .filter((o) => o.label === label)
49
- .sort((a, b) => b.confidence - a.confidence);
50
- if (matches.length === 0)
51
- return [];
52
- return all ? matches : [matches[0]];
53
- }
54
- function subjectFeather(targets) {
55
- const minDim = Math.min(...targets.map((t) => Math.min(t.bbox[2] - t.bbox[0], t.bbox[3] - t.bbox[1])));
56
- return Math.max(8, Math.round(minDim * 0.03));
57
- }
58
- async function selectBoxes(connection, snippet, targets, featherPx) {
59
- for (let i = 0; i < targets.length; i++) {
60
- const [l, t, r, b] = targets[i].bbox;
61
- await runScript(connection, await snippet.build('selectRectangle', {
62
- left: l,
63
- top: t,
64
- right: r,
65
- bottom: b,
66
- featherPx,
67
- selectionType: i === 0 ? 'replace' : 'add',
68
- }));
69
- }
70
- }
71
- async function runObjectOp(connection, snippet, client, rawArgs) {
72
- try {
73
- const args = validateArgs(objectSchema, rawArgs);
74
- const op = args.op;
75
- const label = args.label;
76
- const all = args.all ?? false;
77
- const det = await detectActiveDoc(connection, client, {
78
- objects: true,
79
- maxDimension: args.max_dimension,
80
- objectThreshold: args.object_threshold,
81
- });
82
- const targets = pickObjects(det.result.objects ?? [], label, all);
83
- if (targets.length === 0) {
84
- const seen = [...new Set((det.result.objects ?? []).map((o) => o.label))].sort();
85
- return {
86
- content: [
87
- {
88
- type: 'text',
89
- text: `No "${label}" detected. Found: ${seen.length ? seen.join(', ') : '(nothing)'}. Try a different label or lower object_threshold.`,
90
- },
91
- ],
92
- isError: true,
93
- };
94
- }
95
- let summary = '';
96
- try {
97
- if (op === 'remove') {
98
- await selectBoxes(connection, snippet, targets, 0);
99
- const minDim = Math.min(...targets.map((t) => Math.min(t.bbox[2] - t.bbox[0], t.bbox[3] - t.bbox[1])));
100
- const expand = Math.max(8, Math.round(minDim * 0.02));
101
- await runScript(connection, await snippet.build('modifySelectionEdge', {
102
- mode: 'expand',
103
- amount: expand,
104
- atCanvasBounds: false,
105
- }));
106
- const r = (await runScript(connection, await snippet.build('applyContentAwareFill', {
107
- colorAdaptation: true,
108
- rotate: false,
109
- scale: false,
110
- mirror: false,
111
- opacity: 100,
112
- blendMode: 'normal',
113
- applyToActiveLayer: false,
114
- })));
115
- summary = `Removed ${targets.length} "${label}" via Content-Aware Fill on a new layer (${r.target_layer_name ?? 'copy'}). Original preserved.`;
116
- }
117
- else if (op === 'blur_background') {
118
- const radius = args.amount ?? 25;
119
- await selectBoxes(connection, snippet, targets, subjectFeather(targets));
120
- await runScript(connection, await snippet.build('invertSelection'));
121
- const r = (await runScript(connection, await snippet.build('applyGaussianBlur', { radius, applyToActiveLayer: false })));
122
- summary = `Blurred the background (radius ${radius}px) behind ${targets.length} "${label}" on a new layer (${r.target_layer_name ?? 'copy'}).`;
123
- }
124
- else {
125
- const strength = args.amount ?? 45;
126
- await selectBoxes(connection, snippet, targets, subjectFeather(targets));
127
- await runScript(connection, await snippet.build('invertSelection'));
128
- await runScript(connection, await snippet.build('addAdjustmentLayer', {
129
- type: 'brightness_contrast',
130
- brightness: -Math.abs(strength),
131
- mask_from_selection: true,
132
- mask_inverted: false,
133
- clip_to_below: false,
134
- name: 'Spotlight',
135
- }));
136
- summary = `Darkened the background (−${Math.abs(strength)}) around ${targets.length} "${label}" with a masked adjustment layer.`;
137
- }
138
- }
139
- finally {
140
- try {
141
- await runScript(connection, await snippet.build('deselect'));
142
- }
143
- catch {
144
- }
145
- }
146
- return {
147
- content: [{ type: 'text', text: summary }],
148
- structuredContent: {
149
- op,
150
- label,
151
- targeted: targets.length,
152
- boxes: targets.map((t) => t.bbox),
153
- image: det.result.image,
154
- context: det.context,
155
- },
156
- };
157
- }
158
- catch (error) {
159
- const msg = error instanceof Error ? error.message : String(error);
160
- return {
161
- content: [{ type: 'text', text: `Error in photoshop_object: ${msg}` }],
162
- isError: true,
163
- };
164
- }
165
- }
166
- export function createObjectTools(connection, snippetClient, client = new OnnxDetectionClient()) {
167
- return [
168
- {
169
- tool: {
170
- name: 'photoshop_object',
171
- description: 'Detection-driven object edits keyed on a COCO label (person, dog, car, chair, …), using LOCAL on-device vision. op=remove erases the matched object(s) with Content-Aware Fill (great for a background person / photobomber / sign on a reconstructable background like sky, water, wall, foliage; the original layer is preserved). op=blur_background keeps the matched subject sharp and blurs everything else (fake depth of field). op=spotlight keeps the subject lit and darkens the rest. All ops work from the detection bounding box. Pair with photoshop_detect first if you want to see what is detectable.',
172
- inputSchema: objectSchema,
173
- outputSchema: {
174
- type: 'object',
175
- properties: {
176
- op: { type: 'string' },
177
- label: { type: 'string' },
178
- targeted: { type: 'number' },
179
- boxes: { type: 'array', items: { type: 'array', items: { type: 'number' } } },
180
- image: { type: 'object' },
181
- context: { type: 'object' },
182
- },
183
- },
184
- annotations: {
185
- title: 'Object Edit (detection-driven)',
186
- destructiveHint: true,
187
- openWorldHint: false,
188
- },
189
- },
190
- handler: async (args) => runObjectOp(connection, snippetClient, client, args),
191
- },
192
- ];
193
- }
@@ -1,225 +0,0 @@
1
- import { runScript } from '../utils/run-script.js';
2
- import { validateArgs } from '../utils/validate.js';
3
- import { detectActiveDoc } from '../detection/detect-active-doc.js';
4
- import { OnnxDetectionClient, } from '../detection/detection-client.js';
5
- const TEXT_WARP_STYLES = [
6
- 'warpArc',
7
- 'warpArcUpper',
8
- 'warpArcLower',
9
- 'warpArch',
10
- 'warpFlag',
11
- 'warpWave',
12
- 'warpRise',
13
- 'warpBulge',
14
- 'warpInflate',
15
- 'none',
16
- ];
17
- const NOMINAL_FONT_PT = 72;
18
- const SCALE_MIN_PCT = 10;
19
- const SCALE_MAX_PCT = 1000;
20
- const textOnObjectSchema = {
21
- type: 'object',
22
- properties: {
23
- text: {
24
- type: 'string',
25
- description: 'The words to place on the object (e.g. "GOOD DOG").',
26
- },
27
- label: {
28
- type: 'string',
29
- description: "COCO class to anchor the text on (e.g. 'person', 'dog', 'car', 'chair'). The text is fitted to this object's bounding box.",
30
- },
31
- warp_style: {
32
- type: 'string',
33
- enum: [...TEXT_WARP_STYLES],
34
- default: 'warpArc',
35
- description: "Arc/warp shape applied to the editable text. 'none' = flat text fitted to the box (no warp).",
36
- },
37
- bend: {
38
- type: 'number',
39
- default: 30,
40
- minimum: -100,
41
- maximum: 100,
42
- description: 'Warp bend amount, roughly -100..100. Positive arcs one way, negative the other.',
43
- },
44
- fill: {
45
- type: 'number',
46
- default: 0.9,
47
- minimum: 0.1,
48
- maximum: 1,
49
- description: 'Fraction of the object box WIDTH the text should span (default 0.9).',
50
- },
51
- font: {
52
- type: 'string',
53
- description: 'Optional font family ("Arial") or PostScript name ("ArialMT"). Throws clearly if no installed font matches.',
54
- },
55
- color: {
56
- type: 'object',
57
- description: 'Optional RGB text color, each component 0–255.',
58
- properties: {
59
- red: { type: 'integer', minimum: 0, maximum: 255 },
60
- green: { type: 'integer', minimum: 0, maximum: 255 },
61
- blue: { type: 'integer', minimum: 0, maximum: 255 },
62
- },
63
- required: ['red', 'green', 'blue'],
64
- },
65
- object_threshold: {
66
- type: 'number',
67
- default: 0.4,
68
- minimum: 0,
69
- maximum: 1,
70
- description: 'Minimum detection confidence (0–1).',
71
- },
72
- max_dimension: {
73
- type: 'number',
74
- default: 1024,
75
- minimum: 256,
76
- maximum: 4096,
77
- description: 'Long-edge px of the detection export. Coordinates always return in document px.',
78
- },
79
- },
80
- required: ['text', 'label'],
81
- };
82
- function pickObject(objects, label) {
83
- return objects.filter((o) => o.label === label).sort((a, b) => b.confidence - a.confidence)[0];
84
- }
85
- function readBounds(result) {
86
- const nb = result?.new_bounds;
87
- if (!nb ||
88
- typeof nb.left !== 'number' ||
89
- typeof nb.right !== 'number' ||
90
- typeof nb.top !== 'number' ||
91
- typeof nb.bottom !== 'number') {
92
- return undefined;
93
- }
94
- return { left: nb.left, top: nb.top, right: nb.right, bottom: nb.bottom };
95
- }
96
- async function runTextOnObject(connection, snippet, client, rawArgs) {
97
- try {
98
- const args = validateArgs(textOnObjectSchema, rawArgs);
99
- const text = args.text;
100
- const label = args.label;
101
- const warpStyle = args.warp_style ?? 'warpArc';
102
- const bend = args.bend ?? 30;
103
- const fill = args.fill ?? 0.9;
104
- const font = args.font;
105
- const color = args.color;
106
- const det = await detectActiveDoc(connection, client, {
107
- objects: true,
108
- maxDimension: args.max_dimension,
109
- objectThreshold: args.object_threshold,
110
- });
111
- const target = pickObject(det.result.objects ?? [], label);
112
- if (!target) {
113
- const seen = [...new Set((det.result.objects ?? []).map((o) => o.label))].sort();
114
- return {
115
- content: [
116
- {
117
- type: 'text',
118
- text: `No "${label}" detected. Found: ${seen.length ? seen.join(', ') : '(nothing)'}. Try a different label or lower object_threshold.`,
119
- },
120
- ],
121
- isError: true,
122
- };
123
- }
124
- const [l, t, r, b] = target.bbox;
125
- const cx = Math.round((l + r) / 2);
126
- const cy = Math.round((t + b) / 2);
127
- const boxW = r - l;
128
- await runScript(connection, await snippet.build('createTextLayer', { text, x: cx, y: cy, fontSize: NOMINAL_FONT_PT }));
129
- await runScript(connection, await snippet.build('setTextAlignment', { alignment: 'CENTER' }));
130
- if (font) {
131
- await runScript(connection, await snippet.build('setTextFont', { fontName: font }));
132
- }
133
- if (color) {
134
- await runScript(connection, await snippet.build('setTextColor', {
135
- red: color.red,
136
- green: color.green,
137
- blue: color.blue,
138
- }));
139
- }
140
- const moved = await runScript(connection, await snippet.build('moveLayer', {
141
- deltaX: 0,
142
- deltaY: 0,
143
- mode: 'center',
144
- centerOnX: cx,
145
- centerOnY: cy,
146
- }));
147
- const bounds = readBounds(moved);
148
- let scalePercent = null;
149
- if (bounds) {
150
- const textW = bounds.right - bounds.left;
151
- if (textW > 0) {
152
- const raw = ((boxW * fill) / textW) * 100;
153
- scalePercent = Math.max(SCALE_MIN_PCT, Math.min(SCALE_MAX_PCT, raw));
154
- await runScript(connection, await snippet.build('scaleLayer', { scalePercent, centerAnchor: true }));
155
- }
156
- }
157
- if (warpStyle !== 'none') {
158
- await runScript(connection, await snippet.build('warpLayer', {
159
- style: warpStyle,
160
- bend,
161
- hDistort: 0,
162
- vDistort: 0,
163
- orientation: 'horizontal',
164
- }));
165
- }
166
- const warpNote = warpStyle === 'none' ? 'flat' : `warped (${warpStyle}, bend ${bend})`;
167
- const summary = `Placed editable text "${text}" ${warpNote} over the "${label}" (confidence ${target.confidence.toFixed(2)}), fitted to ${Math.round(fill * 100)}% of its width.`;
168
- return {
169
- content: [{ type: 'text', text: summary }],
170
- structuredContent: {
171
- op: 'text_on_object',
172
- label,
173
- text,
174
- warp_style: warpStyle,
175
- bend,
176
- fill,
177
- box: target.bbox,
178
- confidence: target.confidence,
179
- scale_percent: scalePercent,
180
- image: det.result.image,
181
- context: det.context,
182
- },
183
- };
184
- }
185
- catch (error) {
186
- const msg = error instanceof Error ? error.message : String(error);
187
- return {
188
- content: [{ type: 'text', text: `Error in photoshop_text_on_object: ${msg}` }],
189
- isError: true,
190
- };
191
- }
192
- }
193
- export function createTextOnObjectTools(connection, snippetClient, client = new OnnxDetectionClient()) {
194
- return [
195
- {
196
- tool: {
197
- name: 'photoshop_text_on_object',
198
- description: 'Place editable text on a detected object using LOCAL on-device vision. Detects the object by COCO label (person, dog, car, chair, …), drops a TEXT layer fitted to its bounding box, and arcs it (warp_style, default warpArc; use "none" for flat fitted text). The text stays a fully editable type layer. Adds a layer — never edits the object\'s pixels. Anchors to the bounding box (precise text-on-shape via segmentation is a Pro feature). Pair with photoshop_detect first to see what is detectable.',
199
- inputSchema: textOnObjectSchema,
200
- outputSchema: {
201
- type: 'object',
202
- properties: {
203
- op: { type: 'string' },
204
- label: { type: 'string' },
205
- text: { type: 'string' },
206
- warp_style: { type: 'string' },
207
- bend: { type: 'number' },
208
- fill: { type: 'number' },
209
- box: { type: 'array', items: { type: 'number' } },
210
- confidence: { type: 'number' },
211
- scale_percent: { type: ['number', 'null'] },
212
- image: { type: 'object' },
213
- context: { type: 'object' },
214
- },
215
- },
216
- annotations: {
217
- title: 'Text on Object (detection-driven)',
218
- destructiveHint: false,
219
- openWorldHint: false,
220
- },
221
- },
222
- handler: async (args) => runTextOnObject(connection, snippetClient, client, args),
223
- },
224
- ];
225
- }