editmamei 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/sky-tools.js +71 -21
- package/dist/utils/sky-presets.js +114 -0
- package/dist/version.js +1 -1
- package/package.json +4 -5
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/tools/sky-tools.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { runSnippetTool } from '../utils/tool-helpers.js';
|
|
2
2
|
import { requireLocalPath } from '../utils/local-path.js';
|
|
3
3
|
import { SKY_REPLACEMENT_TIMEOUT_MS } from '../utils/operation-timeouts.js';
|
|
4
|
+
import { defaultSkyPreset, loadSkyPresets, matchSkyPreset, } from '../utils/sky-presets.js';
|
|
4
5
|
const PLACEHOLDER_SKY_ID = '00000000-0000-0000-0000-000000000000';
|
|
5
6
|
const replaceSkyInputSchema = {
|
|
6
7
|
type: 'object',
|
|
7
8
|
properties: {
|
|
9
|
+
sky: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Name of one of Photoshop\'s built-in skies, e.g. "Blue Sky 002", "Spectacular 004" or "Sunset Sky 003" (skies the user saved in Photoshop work too). The server finds the image itself, so no file or folder access is needed. Pass "list" to get the installed skies\' names without changing the document. Omit both sky and sky_file to use Photoshop\'s Blue Sky 001 (or the first installed sky).',
|
|
12
|
+
},
|
|
8
13
|
sky_file: {
|
|
9
14
|
type: 'string',
|
|
10
|
-
description: 'ABSOLUTE path to
|
|
15
|
+
description: 'ABSOLUTE path to any image to use as the sky instead of a built-in one; it does not need to be a Photoshop sky preset. Takes precedence over sky.',
|
|
11
16
|
},
|
|
12
17
|
sky_name: {
|
|
13
18
|
type: 'string',
|
|
14
|
-
description: 'Label recorded on the operation. Cosmetic — it does not select the sky, sky_file
|
|
15
|
-
default: 'Custom Sky',
|
|
19
|
+
description: 'Label recorded on the operation. Cosmetic — it does not select the sky. Defaults to the built-in sky\'s name, or "Custom Sky" for sky_file.',
|
|
16
20
|
},
|
|
17
21
|
shift_edge: {
|
|
18
22
|
type: 'number',
|
|
@@ -64,9 +68,44 @@ const replaceSkyInputSchema = {
|
|
|
64
68
|
maximum: 100,
|
|
65
69
|
},
|
|
66
70
|
},
|
|
67
|
-
required: ['sky_file'],
|
|
68
71
|
};
|
|
69
|
-
|
|
72
|
+
function resolveSky(args, presets) {
|
|
73
|
+
if (args.sky_file !== undefined) {
|
|
74
|
+
return {
|
|
75
|
+
kind: 'file',
|
|
76
|
+
path: requireLocalPath('sky_file', args.sky_file),
|
|
77
|
+
label: 'Custom Sky',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const installed = presets();
|
|
81
|
+
if (installed.length === 0) {
|
|
82
|
+
throw new Error("Photoshop's built-in skies were not found on this computer (the Sky_Presets folder in Photoshop's settings). Pass sky_file with the absolute path to any image to use as the sky.");
|
|
83
|
+
}
|
|
84
|
+
const query = typeof args.sky === 'string' && args.sky.trim() !== '' ? args.sky : undefined;
|
|
85
|
+
const preset = query === undefined ? defaultSkyPreset(installed) : matchSkyPreset(installed, query);
|
|
86
|
+
if (!preset) {
|
|
87
|
+
throw new Error(`No built-in sky matches "${query}". Installed skies: ${installed.map((p) => p.name).join('; ')}.`);
|
|
88
|
+
}
|
|
89
|
+
return { kind: 'preset', path: preset.file, label: preset.name };
|
|
90
|
+
}
|
|
91
|
+
async function replaceSky(connection, snippetClient, rawArgs, presets) {
|
|
92
|
+
if (rawArgs.sky_file === undefined &&
|
|
93
|
+
typeof rawArgs.sky === 'string' &&
|
|
94
|
+
rawArgs.sky.trim().toLowerCase() === 'list') {
|
|
95
|
+
const names = presets().map((p) => p.name);
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: names.length
|
|
101
|
+
? `Installed skies: ${names.join('; ')}.`
|
|
102
|
+
: "Photoshop's built-in skies were not found on this computer. Pass sky_file with the absolute path to any image to use as the sky.",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
structuredContent: { replaced: false, skies: names },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
let sky;
|
|
70
109
|
return runSnippetTool({
|
|
71
110
|
connection,
|
|
72
111
|
snippetClient,
|
|
@@ -75,27 +114,33 @@ async function replaceSky(connection, snippetClient, rawArgs) {
|
|
|
75
114
|
snippet: 'replaceSky',
|
|
76
115
|
errorPrefix: 'Error running Sky Replacement',
|
|
77
116
|
timeoutMs: SKY_REPLACEMENT_TIMEOUT_MS,
|
|
78
|
-
params: (args) =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
params: (args) => {
|
|
118
|
+
sky = resolveSky(args, presets);
|
|
119
|
+
return {
|
|
120
|
+
skyPath: sky.path,
|
|
121
|
+
skyName: args.sky_name ?? sky.label,
|
|
122
|
+
skyId: PLACEHOLDER_SKY_ID,
|
|
123
|
+
shiftEdge: args.shift_edge ?? 0,
|
|
124
|
+
borderSmoothness: args.border_smoothness ?? 50,
|
|
125
|
+
brightness: args.brightness ?? 0,
|
|
126
|
+
temperature: args.temperature ?? 0,
|
|
127
|
+
harmonizationOpacity: args.harmonization_opacity ?? 35,
|
|
128
|
+
foregroundLightingOpacity: args.foreground_lighting_opacity ?? 78,
|
|
129
|
+
edgeLightingOpacity: args.edge_lighting_opacity ?? 70,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
successText: () => sky?.kind === 'preset'
|
|
133
|
+
? `Sky replaced using Photoshop's built-in sky "${sky.label}"`
|
|
134
|
+
: `Sky replaced using ${sky?.path ?? 'the given file'}`,
|
|
91
135
|
});
|
|
92
136
|
}
|
|
93
|
-
export function createSkyTools(connection, snippetClient) {
|
|
137
|
+
export function createSkyTools(connection, snippetClient, options = {}) {
|
|
138
|
+
const presets = options.presets ?? (() => loadSkyPresets());
|
|
94
139
|
return [
|
|
95
140
|
{
|
|
96
141
|
tool: {
|
|
97
142
|
name: 'ps_replace_sky',
|
|
98
|
-
description: 'Run Photoshop\'s Sky Replacement (Adobe Sensei): detect the sky, composite a replacement, and relight/colour-grade the foreground to match. Non-destructive — everything lands in a "Sky Replacement Group" of editable layers above the original, so the result can be tuned or deleted afterwards. sky_file
|
|
143
|
+
description: 'Run Photoshop\'s Sky Replacement (Adobe Sensei): detect the sky, composite a replacement, and relight/colour-grade the foreground to match. Non-destructive — everything lands in a "Sky Replacement Group" of editable layers above the original, so the result can be tuned or deleted afterwards. Pick one of Photoshop\'s built-in skies by name with sky (no path needed; sky "list" names them), or pass sky_file with an ABSOLUTE path to ANY image on disk; with neither, Photoshop\'s Blue Sky 001 is used (or the first installed sky). Reach for harmonization_opacity and foreground_lighting_opacity when the composite reads as pasted-on; those two carry most of the believability. Fails cleanly when the image has no detectable sky (indoor or closed compositions).',
|
|
99
144
|
inputSchema: replaceSkyInputSchema,
|
|
100
145
|
outputSchema: {
|
|
101
146
|
type: 'object',
|
|
@@ -113,6 +158,11 @@ export function createSkyTools(connection, snippetClient) {
|
|
|
113
158
|
},
|
|
114
159
|
sky_file: { type: 'string' },
|
|
115
160
|
sky_name: { type: 'string' },
|
|
161
|
+
skies: {
|
|
162
|
+
type: 'array',
|
|
163
|
+
items: { type: 'string' },
|
|
164
|
+
description: 'Installed sky names; returned instead of a composite when sky is "list".',
|
|
165
|
+
},
|
|
116
166
|
context: { type: 'object' },
|
|
117
167
|
},
|
|
118
168
|
},
|
|
@@ -121,7 +171,7 @@ export function createSkyTools(connection, snippetClient) {
|
|
|
121
171
|
idempotentHint: false,
|
|
122
172
|
},
|
|
123
173
|
},
|
|
124
|
-
handler: async (args) => replaceSky(connection, snippetClient, args),
|
|
174
|
+
handler: async (args) => replaceSky(connection, snippetClient, args, presets),
|
|
125
175
|
},
|
|
126
176
|
];
|
|
127
177
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
|
+
const NAME_KEY = Buffer.from('skpNTEXT', 'latin1');
|
|
6
|
+
const ID_KEY = Buffer.from('SzidTEXT', 'latin1');
|
|
7
|
+
export function prettySkyName(raw) {
|
|
8
|
+
return raw
|
|
9
|
+
.replace(/([a-z])([A-Z0-9])/g, '$1 $2')
|
|
10
|
+
.replace(/(\d)([A-Za-z])/g, '$1 $2')
|
|
11
|
+
.replace(/\s+/g, ' ')
|
|
12
|
+
.trim();
|
|
13
|
+
}
|
|
14
|
+
export function parseSkyPresets(buf, dir) {
|
|
15
|
+
const readText = (at) => {
|
|
16
|
+
if (at + 4 > buf.length)
|
|
17
|
+
return null;
|
|
18
|
+
const units = buf.readUInt32BE(at);
|
|
19
|
+
const end = at + 4 + units * 2;
|
|
20
|
+
if (units === 0 || units > 512 || end > buf.length)
|
|
21
|
+
return null;
|
|
22
|
+
let text = '';
|
|
23
|
+
for (let i = at + 4; i < end; i += 2) {
|
|
24
|
+
const unit = buf.readUInt16BE(i);
|
|
25
|
+
if (unit === 0)
|
|
26
|
+
break;
|
|
27
|
+
text += String.fromCharCode(unit);
|
|
28
|
+
}
|
|
29
|
+
return { text, end };
|
|
30
|
+
};
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let pos = buf.indexOf(NAME_KEY); pos !== -1; pos = buf.indexOf(NAME_KEY, pos + NAME_KEY.length)) {
|
|
33
|
+
const name = readText(pos + NAME_KEY.length);
|
|
34
|
+
if (!name)
|
|
35
|
+
continue;
|
|
36
|
+
const rel = buf.subarray(name.end, name.end + 16 + ID_KEY.length).indexOf(ID_KEY);
|
|
37
|
+
if (rel === -1)
|
|
38
|
+
continue;
|
|
39
|
+
const id = readText(name.end + rel + ID_KEY.length);
|
|
40
|
+
if (!id || !GUID_RE.test(id.text))
|
|
41
|
+
continue;
|
|
42
|
+
const file = join(dir, `${id.text}.jpg`);
|
|
43
|
+
if (existsSync(file))
|
|
44
|
+
out.push({ name: prettySkyName(name.text), file });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function findSkyPresetsDir(env = process.env, platform = process.platform, home = homedir()) {
|
|
49
|
+
const list = (dir) => {
|
|
50
|
+
try {
|
|
51
|
+
return readdirSync(dir);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
let candidates = [];
|
|
58
|
+
if (platform === 'win32') {
|
|
59
|
+
if (!env.APPDATA)
|
|
60
|
+
return null;
|
|
61
|
+
const base = join(env.APPDATA, 'Adobe');
|
|
62
|
+
candidates = list(base).flatMap((d) => {
|
|
63
|
+
const m = /^Adobe Photoshop (\d{4})$/.exec(d);
|
|
64
|
+
return m ? [{ year: +m[1], dir: join(base, d, `${d} Settings`, 'Sky_Presets') }] : [];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else if (platform === 'darwin') {
|
|
68
|
+
const base = join(home, 'Library', 'Preferences');
|
|
69
|
+
candidates = list(base).flatMap((d) => {
|
|
70
|
+
const m = /^Adobe Photoshop (\d{4}) Settings$/.exec(d);
|
|
71
|
+
return m ? [{ year: +m[1], dir: join(base, d, 'Sky_Presets') }] : [];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
candidates.sort((a, b) => b.year - a.year);
|
|
75
|
+
return candidates.find((c) => existsSync(join(c.dir, 'Skies.psp')))?.dir ?? null;
|
|
76
|
+
}
|
|
77
|
+
export function loadSkyPresets(dir = findSkyPresetsDir()) {
|
|
78
|
+
if (!dir)
|
|
79
|
+
return [];
|
|
80
|
+
try {
|
|
81
|
+
return parseSkyPresets(readFileSync(join(dir, 'Skies.psp')), dir);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const norm = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
|
88
|
+
const familyAndNumber = (s) => {
|
|
89
|
+
const m = /^(\p{L}+?)(\d+)/u.exec(norm(s));
|
|
90
|
+
return m ? { family: m[1], num: Number(m[2]) } : null;
|
|
91
|
+
};
|
|
92
|
+
export function matchSkyPreset(presets, query) {
|
|
93
|
+
const q = norm(query);
|
|
94
|
+
if (!q)
|
|
95
|
+
return undefined;
|
|
96
|
+
const exact = presets.find((p) => norm(p.name) === q);
|
|
97
|
+
if (exact)
|
|
98
|
+
return exact;
|
|
99
|
+
const want = familyAndNumber(query);
|
|
100
|
+
if (want) {
|
|
101
|
+
const hit = presets.find((p) => {
|
|
102
|
+
const have = familyAndNumber(p.name);
|
|
103
|
+
return have !== null && have.family.startsWith(want.family) && have.num === want.num;
|
|
104
|
+
});
|
|
105
|
+
if (hit)
|
|
106
|
+
return hit;
|
|
107
|
+
}
|
|
108
|
+
return presets.find((p) => norm(p.name).startsWith(q));
|
|
109
|
+
}
|
|
110
|
+
export function defaultSkyPreset(presets) {
|
|
111
|
+
return (presets.find((p) => /^blue sky\b/i.test(p.name) && /\bby photoshop$/i.test(p.name)) ??
|
|
112
|
+
presets.find((p) => /^blue sky\b/i.test(p.name)) ??
|
|
113
|
+
presets[0]);
|
|
114
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.9.
|
|
1
|
+
export const VERSION = '1.9.1';
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editmamei",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "Photoshop MCP server: natural-language AI photo editing in desktop Photoshop (Community Edition)",
|
|
5
5
|
"mcpName": "io.github.editmamei/editmamei",
|
|
6
6
|
"editmamei": {
|
|
7
7
|
"fixesByVersion": {
|
|
8
|
+
"1.9.1": [
|
|
9
|
+
"ps_replace_sky"
|
|
10
|
+
],
|
|
8
11
|
"1.9.0": [
|
|
9
12
|
"ps_save_psd"
|
|
10
13
|
],
|
|
@@ -14,10 +17,6 @@
|
|
|
14
17
|
"ps_select",
|
|
15
18
|
"ps_modify_selection"
|
|
16
19
|
],
|
|
17
|
-
"1.2.0": [
|
|
18
|
-
"ps_delete_layer",
|
|
19
|
-
"ps_select_layer"
|
|
20
|
-
],
|
|
21
20
|
"1.2.1": [
|
|
22
21
|
"ps_read_scene"
|
|
23
22
|
]
|