editmamei 0.23.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICES.md +6 -30
- package/README.md +47 -13
- package/dist/api/extendscript/_helpers.js +1 -101
- package/dist/api/photoshop-api.js +23 -16
- package/dist/api/snippet-client.js +38 -2
- 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/core/server.js +60 -184
- package/dist/core/session.js +9 -10
- package/dist/core/tool-registry.js +4 -4
- package/dist/delivery/client.js +50 -1
- package/dist/delivery/provision.js +18 -1
- package/dist/delivery/signing.js +38 -1
- package/dist/delivery/store.js +157 -16
- package/dist/detection/detect-active-doc.js +42 -11
- package/dist/detection/detection-client.js +3 -3
- package/dist/detection/face-detector.js +2 -2
- package/dist/detection/geometry.js +14 -0
- package/dist/detection/landmark-detection-client.js +3 -3
- package/dist/detection/mesh-face.js +2 -3
- package/dist/detection/object-detector.js +2 -2
- package/dist/detection/runtime.js +14 -3
- package/dist/detection/sam-segmenter.js +69 -26
- package/dist/kernel/kernel.js +5 -9
- package/dist/kernel/module-lifecycle.js +187 -0
- package/dist/license/entitlement.js +13 -3
- package/dist/license/store.js +8 -1
- package/dist/modules/ce/index.js +1 -1
- package/dist/perception/facets.js +19 -1
- package/dist/perception/grounding-anchors.js +2 -12
- package/dist/perception/grounding-locate.js +1 -9
- package/dist/perception/grounding-review-crop.js +4 -29
- package/dist/perception/overlay.js +62 -0
- package/dist/perception/pixel-identity.js +41 -0
- package/dist/perception/region-precompute.js +96 -20
- package/dist/perception/scene-model.js +93 -50
- package/dist/perception/select-recipes.js +109 -68
- package/dist/perception/sky-ground-flood.js +0 -5
- package/dist/perception/sky-mask-transfer.js +73 -14
- package/dist/platform/connection.js +60 -43
- package/dist/platform/host-platform.js +16 -0
- package/dist/platform/macos-detector.js +41 -70
- package/dist/platform/macos-runner.js +94 -0
- package/dist/platform/script-queue.js +68 -0
- package/dist/platform/script-result.js +39 -0
- package/dist/platform/windows-detector.js +49 -66
- package/dist/platform/windows-runner.js +84 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +80 -132
- package/dist/tools/brush-tools.js +4 -15
- package/dist/tools/channel-compose-tools.js +4 -23
- package/dist/tools/detection-tools.js +59 -47
- package/dist/tools/document-tools.js +92 -127
- package/dist/tools/filter-tools.js +490 -731
- package/dist/tools/group-tools.js +75 -156
- package/dist/tools/guide-tools.js +52 -83
- package/dist/tools/history-tools.js +29 -63
- package/dist/tools/image-placement-tools.js +18 -37
- package/dist/tools/image-tools.js +23 -51
- package/dist/tools/layer-ordering-tools.js +2 -9
- package/dist/tools/layer-properties-tools.js +207 -395
- package/dist/tools/layer-tools.js +224 -190
- package/dist/tools/layer-transform-tools.js +41 -106
- package/dist/tools/metadata-tools.js +2 -9
- package/dist/tools/path-tools.js +4 -15
- package/dist/tools/portrait-tools.js +2 -5
- package/dist/tools/preview-tools.js +59 -72
- package/dist/tools/retouch-tools.js +33 -71
- package/dist/tools/scene-tools.js +123 -75
- package/dist/tools/selection-tools.js +386 -566
- package/dist/tools/shape-tools.js +2 -9
- package/dist/tools/text-tools.js +24 -57
- package/dist/tools/transform-canvas-tools.js +21 -54
- package/dist/tools/vector-mask-tools.js +2 -9
- package/dist/update/check.js +1 -1
- package/dist/utils/jpeg-quality.js +1 -0
- package/dist/utils/logger.js +55 -45
- package/dist/utils/operation-timeouts.js +1 -0
- package/dist/utils/run-script.js +7 -2
- package/dist/utils/session-log.js +134 -12
- package/dist/utils/template-storage.js +5 -6
- package/dist/utils/tool-helpers.js +43 -0
- package/dist/version.js +1 -1
- package/package.json +10 -3
- package/LICENSE +0 -81
- package/dist/platform/detector.js +0 -32
- package/dist/platform/macos-executor.js +0 -151
- package/dist/platform/windows-executor.js +0 -160
- /package/dist/platform/{script-executor.js → ports.js} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runScript } from '../utils/run-script.js';
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
|
+
import { toolErrorResult, runSnippetTool, applyToActiveLayerProp } from '../utils/tool-helpers.js';
|
|
3
4
|
const addAdjustmentLayerSchema = {
|
|
4
5
|
type: 'object',
|
|
5
6
|
properties: {
|
|
@@ -422,9 +423,24 @@ const addAdjustmentLayerSchema = {
|
|
|
422
423
|
gm_preset: {
|
|
423
424
|
type: 'string',
|
|
424
425
|
enum: ['black_to_white', 'sepia', 'tint'],
|
|
425
|
-
description: 'For type=gradient_map. Preset gradient. "black_to_white" is the canonical tonal B&W via gradient. "sepia" is a warm vintage tone. "tint" maps black→tint_color→white using gm_tint_color_hex.
|
|
426
|
+
description: 'For type=gradient_map. Preset gradient. "black_to_white" is the canonical tonal B&W via gradient. "sepia" is a warm vintage tone. "tint" maps black→tint_color→white using gm_tint_color_hex. For arbitrary color mappings pass gm_stops instead (overrides the preset).',
|
|
426
427
|
default: 'black_to_white',
|
|
427
428
|
},
|
|
429
|
+
gm_stops: {
|
|
430
|
+
type: 'array',
|
|
431
|
+
description: 'For type=gradient_map. Custom color stops overriding gm_preset — each {red,green,blue (0-255), location (0-100 shadows→highlights), midpoint (5-95, default 50)}. At least 2; sorted by location. E.g. a blue→orange stylized grade: [{red:20,green:40,blue:120,location:0},{red:250,green:150,blue:50,location:100}].',
|
|
432
|
+
items: {
|
|
433
|
+
type: 'object',
|
|
434
|
+
properties: {
|
|
435
|
+
red: { type: 'integer', minimum: 0, maximum: 255 },
|
|
436
|
+
green: { type: 'integer', minimum: 0, maximum: 255 },
|
|
437
|
+
blue: { type: 'integer', minimum: 0, maximum: 255 },
|
|
438
|
+
location: { type: 'number', minimum: 0, maximum: 100 },
|
|
439
|
+
midpoint: { type: 'number', minimum: 5, maximum: 95, default: 50 },
|
|
440
|
+
},
|
|
441
|
+
required: ['red', 'green', 'blue', 'location'],
|
|
442
|
+
},
|
|
443
|
+
},
|
|
428
444
|
gm_tint_color_hex: {
|
|
429
445
|
type: 'string',
|
|
430
446
|
description: 'For type=gradient_map with gm_preset=tint. Mid-tone tint color as a hex string like "#5588CC". Only used when gm_preset=tint.',
|
|
@@ -510,22 +526,14 @@ const colorLookupBakeSchema = {
|
|
|
510
526
|
type: 'string',
|
|
511
527
|
description: 'Leaf name of a 3DLUT file in PS\'s Presets/3DLUTs/ folder (e.g. "TealOrangePlusContrast.3DL", "FallColors.look", "Kodak 5205 Fuji 3510 (by Adobe).cube") OR an absolute path to a .3DL / .look / .cube file. Extension-agnostic leaf-name resolution (the same base name with a different extension still matches).',
|
|
512
528
|
},
|
|
513
|
-
apply_to_active_layer:
|
|
514
|
-
type: 'boolean',
|
|
515
|
-
description: 'If false (default), runs on a DUPLICATE of the active layer; the original is preserved. If true, bakes into the active layer in place.',
|
|
516
|
-
default: false,
|
|
517
|
-
},
|
|
529
|
+
apply_to_active_layer: applyToActiveLayerProp('the color lookup'),
|
|
518
530
|
},
|
|
519
531
|
required: ['cl_lut_name'],
|
|
520
532
|
};
|
|
521
533
|
const equalizeSchema = {
|
|
522
534
|
type: 'object',
|
|
523
535
|
properties: {
|
|
524
|
-
apply_to_active_layer:
|
|
525
|
-
type: 'boolean',
|
|
526
|
-
description: 'If false (default), the operation runs on a DUPLICATE of the active layer and the original is preserved. If true, bakes into the active layer in place. Use false unless you specifically want the destructive-in-place behavior.',
|
|
527
|
-
default: false,
|
|
528
|
-
},
|
|
536
|
+
apply_to_active_layer: applyToActiveLayerProp('the equalize'),
|
|
529
537
|
},
|
|
530
538
|
};
|
|
531
539
|
const shadowsHighlightsSchema = {
|
|
@@ -601,11 +609,7 @@ const shadowsHighlightsSchema = {
|
|
|
601
609
|
maximum: 50,
|
|
602
610
|
default: 0.01,
|
|
603
611
|
},
|
|
604
|
-
apply_to_active_layer:
|
|
605
|
-
type: 'boolean',
|
|
606
|
-
description: 'If false (default), the op is applied to a duplicate of the active layer named "Shadows/Highlights (<Original Name>)" — original preserved. If true, bakes directly into the active layer.',
|
|
607
|
-
default: false,
|
|
608
|
-
},
|
|
612
|
+
apply_to_active_layer: applyToActiveLayerProp('the op'),
|
|
609
613
|
},
|
|
610
614
|
};
|
|
611
615
|
const APPLY_ADJUSTMENT_TYPES = ['shadows_highlights', 'equalize', 'color_lookup'];
|
|
@@ -632,7 +636,7 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
632
636
|
{
|
|
633
637
|
tool: {
|
|
634
638
|
name: 'ps_apply_adjustment',
|
|
635
|
-
description: "Apply a DESTRUCTIVE tonal adjustment that Photoshop does NOT offer as an adjustment layer — chosen via `type`. Runs on a DUPLICATE of the active layer by default (auto-duplicate-first
|
|
639
|
+
description: "Apply a DESTRUCTIVE tonal adjustment that Photoshop does NOT offer as an adjustment layer — chosen via `type`. Runs on a DUPLICATE of the active layer by default (auto-duplicate-first — the original is preserved; revert by deleting the copy); pass `apply_to_active_layer: true` to bake in place. Auto-rasterizes text/smart-object layers. `shadows_highlights` recovers blown highlights + crushed shadows in one pass (defaults match Adobe's dialog: 35 shadow amount, +20 color correction); `equalize` stretches/flattens the histogram (parameter-free); `color_lookup` bakes a 3DLUT grade (cl_lut_name required — leaf name of a file in Presets/3DLUTs/ or an absolute .cube/.3dl/.look path). For EDITABLE tonal/color work, prefer ps_add_adjustment_layer.",
|
|
636
640
|
inputSchema: APPLY_ADJUSTMENT_INPUT_SCHEMA,
|
|
637
641
|
outputSchema: {
|
|
638
642
|
type: 'object',
|
|
@@ -665,7 +669,7 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
665
669
|
{
|
|
666
670
|
tool: {
|
|
667
671
|
name: 'ps_add_adjustment_layer',
|
|
668
|
-
description: "Create a non-destructive adjustment layer above the active layer — hoisted out of the active layer's group by default even though Photoshop's own Mk-AdjL placement rule would otherwise nest it INSIDE that group (pass into_active_group:true to keep that native nesting). Supports the full real-Photoshop tonal/color surface: Curves (with S-curve presets), Levels, Hue/Saturation, Brightness/Contrast, Black & White (with optional tint), Color Balance, Photo Filter (preset or custom color), Vibrance, Channel Mixer, Selective Color, Gradient Map (preset), Exposure (stops + offset + gamma), Color Lookup (3DLUT presets or custom file path), and Invert. Values are editable, maskable, and removable. This is the canonical entry point for ALL tonal/color adjustments; the old destructive bake tools (auto_levels / auto_contrast / desaturate / invert) were removed on 2026-05-31 — if you genuinely need a pixel bake, follow this call with `
|
|
672
|
+
description: "Create a non-destructive adjustment layer above the active layer — hoisted out of the active layer's group by default even though Photoshop's own Mk-AdjL placement rule would otherwise nest it INSIDE that group (pass into_active_group:true to keep that native nesting). Supports the full real-Photoshop tonal/color surface: Curves (with S-curve presets), Levels, Hue/Saturation, Brightness/Contrast, Black & White (with optional tint), Color Balance, Photo Filter (preset or custom color), Vibrance, Channel Mixer, Selective Color, Gradient Map (preset), Exposure (stops + offset + gamma), Color Lookup (3DLUT presets or custom file path), and Invert. Values are editable, maskable, and removable. This is the canonical entry point for ALL tonal/color adjustments; the old destructive bake tools (auto_levels / auto_contrast / desaturate / invert) were removed on 2026-05-31 — if you genuinely need a pixel bake, follow this call with `ps_merge` (mode=visible). Optionally clips the adjustment to only affect the layer directly below it. If a selection is active at call time, the new layer is automatically masked by it (toggle with mask_from_selection / mask_inverted). For destructive ops that don't have an adjustment-layer equivalent in Photoshop (Shadows/Highlights — single-pass shadow/highlight recovery), use `ps_apply_adjustment` (type=shadows_highlights) which auto-duplicates the active layer to keep the original intact. Returns context (the new adjustment layer becomes active) plus parent_path — the actual containing-group chain, so placement is never silent.",
|
|
669
673
|
inputSchema: addAdjustmentLayerSchema,
|
|
670
674
|
outputSchema: {
|
|
671
675
|
type: 'object',
|
|
@@ -783,6 +787,7 @@ const FORWARDED_KEYS = new Set([
|
|
|
783
787
|
'gm_tint_color_hex',
|
|
784
788
|
'gm_reverse',
|
|
785
789
|
'gm_dither',
|
|
790
|
+
'gm_stops',
|
|
786
791
|
'exp_exposure',
|
|
787
792
|
'exp_offset',
|
|
788
793
|
'exp_gamma',
|
|
@@ -839,125 +844,68 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
|
839
844
|
};
|
|
840
845
|
}
|
|
841
846
|
catch (error) {
|
|
842
|
-
return
|
|
843
|
-
content: [
|
|
844
|
-
{
|
|
845
|
-
type: 'text',
|
|
846
|
-
text: `Error adding adjustment layer: ${error instanceof Error ? error.message : String(error)}`,
|
|
847
|
-
},
|
|
848
|
-
],
|
|
849
|
-
isError: true,
|
|
850
|
-
};
|
|
847
|
+
return toolErrorResult('Error adding adjustment layer', error);
|
|
851
848
|
}
|
|
852
849
|
}
|
|
853
850
|
async function applyColorLookup(connection, snippetClient, rawArgs) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
structuredContent: result,
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
catch (error) {
|
|
871
|
-
return {
|
|
872
|
-
content: [
|
|
873
|
-
{
|
|
874
|
-
type: 'text',
|
|
875
|
-
text: `Error applying color lookup: ${error instanceof Error ? error.message : String(error)}`,
|
|
876
|
-
},
|
|
877
|
-
],
|
|
878
|
-
isError: true,
|
|
879
|
-
};
|
|
880
|
-
}
|
|
851
|
+
return runSnippetTool({
|
|
852
|
+
connection,
|
|
853
|
+
snippetClient,
|
|
854
|
+
rawArgs,
|
|
855
|
+
schema: colorLookupBakeSchema,
|
|
856
|
+
snippet: 'applyColorLookup',
|
|
857
|
+
errorPrefix: 'Error applying color lookup',
|
|
858
|
+
params: (args) => ({
|
|
859
|
+
lutName: args.cl_lut_name,
|
|
860
|
+
applyToActiveLayer: args.apply_to_active_layer ?? false,
|
|
861
|
+
}),
|
|
862
|
+
successText: (result) => `Color Lookup applied (BAKE, experimental):\n${JSON.stringify(result, null, 2)}`,
|
|
863
|
+
});
|
|
881
864
|
}
|
|
882
865
|
async function applyEqualize(connection, snippetClient, rawArgs) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
structuredContent: result,
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
catch (error) {
|
|
899
|
-
return {
|
|
900
|
-
content: [
|
|
901
|
-
{
|
|
902
|
-
type: 'text',
|
|
903
|
-
text: `Error applying equalize: ${error instanceof Error ? error.message : String(error)}`,
|
|
904
|
-
},
|
|
905
|
-
],
|
|
906
|
-
isError: true,
|
|
907
|
-
};
|
|
908
|
-
}
|
|
866
|
+
return runSnippetTool({
|
|
867
|
+
connection,
|
|
868
|
+
snippetClient,
|
|
869
|
+
rawArgs,
|
|
870
|
+
schema: equalizeSchema,
|
|
871
|
+
snippet: 'applyEqualize',
|
|
872
|
+
errorPrefix: 'Error applying equalize',
|
|
873
|
+
params: (args) => ({
|
|
874
|
+
applyToActiveLayer: args.apply_to_active_layer ?? false,
|
|
875
|
+
}),
|
|
876
|
+
successText: (result) => `Equalize applied:\n${JSON.stringify(result, null, 2)}`,
|
|
877
|
+
});
|
|
909
878
|
}
|
|
910
879
|
async function applyShadowsHighlights(connection, snippetClient, rawArgs) {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
return {
|
|
943
|
-
content: [
|
|
944
|
-
{
|
|
945
|
-
type: 'text',
|
|
946
|
-
text: `Shadows/Highlights applied to ${target} (shadow ${shadowAmount}, highlight ${highlightAmount}, color ${colorCorrection}).`,
|
|
947
|
-
},
|
|
948
|
-
],
|
|
949
|
-
structuredContent: result,
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
catch (error) {
|
|
953
|
-
return {
|
|
954
|
-
content: [
|
|
955
|
-
{
|
|
956
|
-
type: 'text',
|
|
957
|
-
text: `Error applying shadows/highlights: ${error instanceof Error ? error.message : String(error)}`,
|
|
958
|
-
},
|
|
959
|
-
],
|
|
960
|
-
isError: true,
|
|
961
|
-
};
|
|
962
|
-
}
|
|
880
|
+
return runSnippetTool({
|
|
881
|
+
connection,
|
|
882
|
+
snippetClient,
|
|
883
|
+
rawArgs,
|
|
884
|
+
schema: shadowsHighlightsSchema,
|
|
885
|
+
snippet: 'applyShadowsHighlights',
|
|
886
|
+
errorPrefix: 'Error applying shadows/highlights',
|
|
887
|
+
params: (args) => ({
|
|
888
|
+
shadowAmount: args.shadow_amount ?? 35,
|
|
889
|
+
shadowWidth: args.shadow_width ?? 50,
|
|
890
|
+
shadowRadius: args.shadow_radius ?? 30,
|
|
891
|
+
highlightAmount: args.highlight_amount ?? 0,
|
|
892
|
+
highlightWidth: args.highlight_width ?? 50,
|
|
893
|
+
highlightRadius: args.highlight_radius ?? 30,
|
|
894
|
+
colorCorrection: args.color_correction ?? 20,
|
|
895
|
+
midtoneContrast: args.midtone_contrast ?? 0,
|
|
896
|
+
blackClip: args.black_clip ?? 0.01,
|
|
897
|
+
whiteClip: args.white_clip ?? 0.01,
|
|
898
|
+
applyToActiveLayer: args.apply_to_active_layer ?? false,
|
|
899
|
+
}),
|
|
900
|
+
successText: (result, args) => {
|
|
901
|
+
const r = result;
|
|
902
|
+
const target = r.target_was_copy
|
|
903
|
+
? `new copy "${r.target_layer_name ?? '?'}"`
|
|
904
|
+
: 'active layer (in place)';
|
|
905
|
+
const shadowAmount = args.shadow_amount ?? 35;
|
|
906
|
+
const highlightAmount = args.highlight_amount ?? 0;
|
|
907
|
+
const colorCorrection = args.color_correction ?? 20;
|
|
908
|
+
return `Shadows/Highlights applied to ${target} (shadow ${shadowAmount}, highlight ${highlightAmount}, color ${colorCorrection}).`;
|
|
909
|
+
},
|
|
910
|
+
});
|
|
963
911
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SUPPORTED_BRUSH_TOOLS } from '../api/brush-tool-names.js';
|
|
2
2
|
import { runScript } from '../utils/run-script.js';
|
|
3
3
|
import { validateArgs } from '../utils/validate.js';
|
|
4
|
+
import { toolErrorResult, applyToActiveLayerProp } from '../utils/tool-helpers.js';
|
|
4
5
|
import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
|
|
5
6
|
import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
6
7
|
const TOOLS_REQUIRING_SOURCE = ['clone_stamp', 'healing_brush'];
|
|
@@ -109,11 +110,7 @@ const applyBrushStrokeSchema = {
|
|
|
109
110
|
maximum: 50,
|
|
110
111
|
default: 0,
|
|
111
112
|
},
|
|
112
|
-
apply_to_active_layer:
|
|
113
|
-
type: 'boolean',
|
|
114
|
-
description: 'If false (default), the stroke runs on a duplicate named "Brush Stroke (<original name>)" — the original is preserved, undo is delete-the-copy (Bundle O pattern). If true, the stroke bakes into the active layer directly.',
|
|
115
|
-
default: false,
|
|
116
|
-
},
|
|
113
|
+
apply_to_active_layer: applyToActiveLayerProp('the stroke'),
|
|
117
114
|
},
|
|
118
115
|
required: ['tool', 'brush_size'],
|
|
119
116
|
};
|
|
@@ -122,7 +119,7 @@ export function createBrushTools(connection, snippetClient, client = new OnnxLan
|
|
|
122
119
|
{
|
|
123
120
|
tool: {
|
|
124
121
|
name: 'ps_apply_brush_stroke',
|
|
125
|
-
description: 'Paint along a path with one of PS\'s 16 brush-family tools — supply the path EITHER as an anchor-relational `placement` (preferred: a path relation → the stroke traces a resolved, gate-verified curve along a traced edge / landmark / between two anchors, no pixel-guessing) OR as an explicit `path` list of anchor points — the retouch tools (`healing_brush`, `clone_stamp`, `burn`, `dodge`, `blur`, `sharpen`, `smudge`), the paint family (`brush`, `pencil`, `eraser`), and the specialty tools (`pattern_stamp`, `art_history_brush`, `history_brush`, `color_replacement`, `background_eraser`, `sponge`). The `path` parameter takes a list of anchor points with optional bezier handles, so the same tool handles straight-line strokes, freeform curves, and closed shapes — all by varying the path geometry. **Reach for this when**: (a) cloning out a distraction along a specific shape (clone_stamp with a source_point + a path tracing the unwanted edge); (b) healing a scratch or seam (healing_brush with source_point); (c) dodging / burning to redirect tonal balance along a contour; (d) painting a freehand line into the canvas (brush + foreground_color). Active layer must be a normal pixel layer (background auto-promotes); rasterize adjustment/shape/text/smart-object layers first. Auto-duplicates
|
|
122
|
+
description: 'Paint along a path with one of PS\'s 16 brush-family tools — supply the path EITHER as an anchor-relational `placement` (preferred: a path relation → the stroke traces a resolved, gate-verified curve along a traced edge / landmark / between two anchors, no pixel-guessing) OR as an explicit `path` list of anchor points — the retouch tools (`healing_brush`, `clone_stamp`, `burn`, `dodge`, `blur`, `sharpen`, `smudge`), the paint family (`brush`, `pencil`, `eraser`), and the specialty tools (`pattern_stamp`, `art_history_brush`, `history_brush`, `color_replacement`, `background_eraser`, `sponge`). The `path` parameter takes a list of anchor points with optional bezier handles, so the same tool handles straight-line strokes, freeform curves, and closed shapes — all by varying the path geometry. **Reach for this when**: (a) cloning out a distraction along a specific shape (clone_stamp with a source_point + a path tracing the unwanted edge); (b) healing a scratch or seam (healing_brush with source_point); (c) dodging / burning to redirect tonal balance along a contour; (d) painting a freehand line into the canvas (brush + foreground_color). Active layer must be a normal pixel layer (background auto-promotes); rasterize adjustment/shape/text/smart-object layers first. Auto-duplicates — the original is preserved and a "Brush Stroke (<name>)" copy receives the paint. **Brush dynamics**: hardness, opacity, and flow are independently settable via `hardness_pct` / `opacity_pct` / `flow_pct` (the live tool options are mutated before stroking and restored to the user\'s prior state in `finally`). Sampled brushes (custom shape-stamp presets) silently ignore hardness/diameter mutations — vary their character via `brush_preset` instead.',
|
|
126
123
|
inputSchema: applyBrushStrokeSchema,
|
|
127
124
|
outputSchema: {
|
|
128
125
|
type: 'object',
|
|
@@ -292,15 +289,7 @@ async function applyBrushStroke(connection, snippetClient, client, rawArgs) {
|
|
|
292
289
|
};
|
|
293
290
|
}
|
|
294
291
|
catch (error) {
|
|
295
|
-
return
|
|
296
|
-
content: [
|
|
297
|
-
{
|
|
298
|
-
type: 'text',
|
|
299
|
-
text: `Error applying brush stroke: ${error instanceof Error ? error.message : String(error)}`,
|
|
300
|
-
},
|
|
301
|
-
],
|
|
302
|
-
isError: true,
|
|
303
|
-
};
|
|
292
|
+
return toolErrorResult('Error applying brush stroke', error);
|
|
304
293
|
}
|
|
305
294
|
}
|
|
306
295
|
function deterministicNoise(i, anchorSeed, axisSalt) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runScript } from '../utils/run-script.js';
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
|
+
import { toolErrorResult, applyToActiveLayerProp } from '../utils/tool-helpers.js';
|
|
3
4
|
const BLEND_MODES = [
|
|
4
5
|
'normal',
|
|
5
6
|
'multiply',
|
|
@@ -45,11 +46,7 @@ const applyImageSchema = {
|
|
|
45
46
|
maximum: 100,
|
|
46
47
|
default: 100,
|
|
47
48
|
},
|
|
48
|
-
apply_to_active_layer:
|
|
49
|
-
type: 'boolean',
|
|
50
|
-
description: 'If false (default) the composite is baked onto a DUPLICATE of the active layer (original preserved — delete the copy to revert). Set true to bake destructively in place.',
|
|
51
|
-
default: false,
|
|
52
|
-
},
|
|
49
|
+
apply_to_active_layer: applyToActiveLayerProp('the composite'),
|
|
53
50
|
},
|
|
54
51
|
required: ['blend'],
|
|
55
52
|
};
|
|
@@ -182,15 +179,7 @@ async function applyImage(connection, snippetClient, rawArgs) {
|
|
|
182
179
|
};
|
|
183
180
|
}
|
|
184
181
|
catch (error) {
|
|
185
|
-
return
|
|
186
|
-
content: [
|
|
187
|
-
{
|
|
188
|
-
type: 'text',
|
|
189
|
-
text: `Error in ps_apply_image: ${error instanceof Error ? error.message : String(error)}`,
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
isError: true,
|
|
193
|
-
};
|
|
182
|
+
return toolErrorResult('Error in ps_apply_image', error);
|
|
194
183
|
}
|
|
195
184
|
}
|
|
196
185
|
async function calculations(connection, snippetClient, rawArgs) {
|
|
@@ -228,14 +217,6 @@ async function calculations(connection, snippetClient, rawArgs) {
|
|
|
228
217
|
};
|
|
229
218
|
}
|
|
230
219
|
catch (error) {
|
|
231
|
-
return
|
|
232
|
-
content: [
|
|
233
|
-
{
|
|
234
|
-
type: 'text',
|
|
235
|
-
text: `Error in ps_calculations: ${error instanceof Error ? error.message : String(error)}`,
|
|
236
|
-
},
|
|
237
|
-
],
|
|
238
|
-
isError: true,
|
|
239
|
-
};
|
|
220
|
+
return toolErrorResult('Error in ps_calculations', error);
|
|
240
221
|
}
|
|
241
222
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { encode } from 'jpeg-js';
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
3
|
import { detectActiveDoc } from '../detection/detect-active-doc.js';
|
|
4
|
+
import { ANNOTATED_PREVIEW_JPEG_QUALITY } from '../utils/jpeg-quality.js';
|
|
4
5
|
import { OnnxDetectionClient, } from '../detection/detection-client.js';
|
|
6
|
+
import { toolErrorResult } from '../utils/tool-helpers.js';
|
|
7
|
+
import { computePixelIdentity, docKeyFrom, samePixelIdentity, } from '../perception/pixel-identity.js';
|
|
8
|
+
import { ANNOTATION_RGB, annotationThickness, drawBoxOutline, } from '../perception/overlay.js';
|
|
5
9
|
const detectSchema = {
|
|
6
10
|
type: 'object',
|
|
7
11
|
properties: {
|
|
@@ -46,36 +50,15 @@ const detectSchema = {
|
|
|
46
50
|
},
|
|
47
51
|
},
|
|
48
52
|
};
|
|
49
|
-
function drawBoxes(
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
if (x < 0 || y < 0 || x >= w || y >= h)
|
|
55
|
-
return;
|
|
56
|
-
const i = (y * w + x) * 4;
|
|
57
|
-
data[i] = r;
|
|
58
|
-
data[i + 1] = g;
|
|
59
|
-
data[i + 2] = b;
|
|
60
|
-
data[i + 3] = 255;
|
|
61
|
-
};
|
|
53
|
+
export function drawBoxes(img, boxes) {
|
|
54
|
+
const { width: w, height: h } = img;
|
|
55
|
+
const data = Uint8Array.from(img.data);
|
|
56
|
+
const out = { data, width: w, height: h };
|
|
57
|
+
const thickness = annotationThickness(out);
|
|
62
58
|
for (const { bbox, rgb } of boxes) {
|
|
63
|
-
|
|
64
|
-
const y1 = Math.round(bbox[1]);
|
|
65
|
-
const x2 = Math.round(bbox[2]);
|
|
66
|
-
const y2 = Math.round(bbox[3]);
|
|
67
|
-
for (let x = x1; x <= x2; x++)
|
|
68
|
-
for (let k = 0; k < thickness; k++) {
|
|
69
|
-
setPx(x, y1 + k, rgb[0], rgb[1], rgb[2]);
|
|
70
|
-
setPx(x, y2 - k, rgb[0], rgb[1], rgb[2]);
|
|
71
|
-
}
|
|
72
|
-
for (let y = y1; y <= y2; y++)
|
|
73
|
-
for (let k = 0; k < thickness; k++) {
|
|
74
|
-
setPx(x1 + k, y, rgb[0], rgb[1], rgb[2]);
|
|
75
|
-
setPx(x2 - k, y, rgb[0], rgb[1], rgb[2]);
|
|
76
|
-
}
|
|
59
|
+
drawBoxOutline(out, bbox, rgb, thickness);
|
|
77
60
|
}
|
|
78
|
-
return encode({ data, width: w, height: h },
|
|
61
|
+
return encode({ data, width: w, height: h }, ANNOTATED_PREVIEW_JPEG_QUALITY).data;
|
|
79
62
|
}
|
|
80
63
|
function summarizeObjects(objects) {
|
|
81
64
|
if (!objects || objects.length === 0)
|
|
@@ -85,13 +68,27 @@ function summarizeObjects(objects) {
|
|
|
85
68
|
counts.set(o.label, (counts.get(o.label) ?? 0) + 1);
|
|
86
69
|
return [...counts.entries()].map(([l, n]) => (n > 1 ? `${l}×${n}` : l)).join(', ');
|
|
87
70
|
}
|
|
88
|
-
|
|
71
|
+
let detectCache = null;
|
|
72
|
+
export function __clearDetectCache() {
|
|
73
|
+
detectCache = null;
|
|
74
|
+
}
|
|
75
|
+
async function detect(connection, client, rawArgs, detectDeps) {
|
|
89
76
|
try {
|
|
90
77
|
const args = validateArgs(detectSchema, rawArgs);
|
|
91
78
|
const target = args.target ?? 'both';
|
|
92
79
|
const annotate = args.annotate ?? true;
|
|
93
80
|
const wantFaces = target === 'faces' || target === 'both';
|
|
94
81
|
const wantObjects = target === 'objects' || target === 'both';
|
|
82
|
+
const detectKey = JSON.stringify({
|
|
83
|
+
faces: wantFaces,
|
|
84
|
+
objects: wantObjects,
|
|
85
|
+
maxDimension: args.max_dimension ?? null,
|
|
86
|
+
faceThreshold: args.face_threshold ?? null,
|
|
87
|
+
objectThreshold: args.object_threshold ?? null,
|
|
88
|
+
maxObjects: args.max_objects ?? null,
|
|
89
|
+
});
|
|
90
|
+
let hit = null;
|
|
91
|
+
let pendingIdentity = null;
|
|
95
92
|
const det = await detectActiveDoc(connection, client, {
|
|
96
93
|
faces: wantFaces,
|
|
97
94
|
objects: wantObjects,
|
|
@@ -99,22 +96,41 @@ async function detect(connection, client, rawArgs) {
|
|
|
99
96
|
faceThreshold: args.face_threshold,
|
|
100
97
|
objectThreshold: args.object_threshold,
|
|
101
98
|
maxObjects: args.max_objects,
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
shouldDetect: ({ decoded, context, docWidth, docHeight }) => {
|
|
100
|
+
if (!decoded)
|
|
101
|
+
return true;
|
|
102
|
+
const identity = computePixelIdentity(decoded, docKeyFrom(context, docWidth, docHeight));
|
|
103
|
+
if (detectCache &&
|
|
104
|
+
detectCache.optionsKey === detectKey &&
|
|
105
|
+
samePixelIdentity(detectCache.identity, identity)) {
|
|
106
|
+
hit = detectCache;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
pendingIdentity = identity;
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
}, detectDeps);
|
|
113
|
+
const mapped = hit ? hit.result : det.result;
|
|
114
|
+
const rawDetections = hit ? hit.raw : det.raw;
|
|
115
|
+
if (!hit && pendingIdentity) {
|
|
116
|
+
detectCache = {
|
|
117
|
+
identity: pendingIdentity,
|
|
118
|
+
optionsKey: detectKey,
|
|
119
|
+
result: det.result,
|
|
120
|
+
raw: det.raw,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
104
123
|
const content = [];
|
|
105
|
-
if (annotate) {
|
|
124
|
+
if (annotate && det.decoded) {
|
|
106
125
|
try {
|
|
107
126
|
const boxes = [
|
|
108
|
-
...(
|
|
109
|
-
|
|
110
|
-
rgb: [0, 220, 255],
|
|
111
|
-
})),
|
|
112
|
-
...(det.raw.objects ?? []).map((o) => ({
|
|
127
|
+
...(rawDetections.faces ?? []).map((f) => ({ bbox: f.bbox, rgb: ANNOTATION_RGB.face })),
|
|
128
|
+
...(rawDetections.objects ?? []).map((o) => ({
|
|
113
129
|
bbox: o.bbox,
|
|
114
|
-
rgb:
|
|
130
|
+
rgb: ANNOTATION_RGB.object,
|
|
115
131
|
})),
|
|
116
132
|
];
|
|
117
|
-
const annotated = drawBoxes(det.
|
|
133
|
+
const annotated = drawBoxes(det.decoded, boxes);
|
|
118
134
|
content.push({
|
|
119
135
|
type: 'image',
|
|
120
136
|
data: annotated.toString('base64'),
|
|
@@ -150,14 +166,10 @@ async function detect(connection, client, rawArgs) {
|
|
|
150
166
|
};
|
|
151
167
|
}
|
|
152
168
|
catch (error) {
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
content: [{ type: 'text', text: `Error during detection: ${msg}` }],
|
|
156
|
-
isError: true,
|
|
157
|
-
};
|
|
169
|
+
return toolErrorResult('Error during detection', error);
|
|
158
170
|
}
|
|
159
171
|
}
|
|
160
|
-
export function createDetectionTools(connection, _snippetClient, client = new OnnxDetectionClient()) {
|
|
172
|
+
export function createDetectionTools(connection, _snippetClient, client = new OnnxDetectionClient(), detectDeps) {
|
|
161
173
|
return [
|
|
162
174
|
{
|
|
163
175
|
tool: {
|
|
@@ -203,7 +215,7 @@ export function createDetectionTools(connection, _snippetClient, client = new On
|
|
|
203
215
|
idempotentHint: true,
|
|
204
216
|
},
|
|
205
217
|
},
|
|
206
|
-
handler: async (args) => detect(connection, client, args),
|
|
218
|
+
handler: async (args) => detect(connection, client, args, detectDeps),
|
|
207
219
|
},
|
|
208
220
|
];
|
|
209
221
|
}
|