mcp-relight-harmonize 1.0.2 → 1.0.3
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/.agents/skills/mcp-relight-harmonize/SKILL.md +17 -0
- package/.agents/skills/mcp-relight-harmonize/scripts/extract_layers.py +397 -0
- package/MASTER.md +162 -89
- package/README.md +3 -2
- package/dist/domain/layer_extractor.d.ts +42 -0
- package/dist/domain/layer_extractor.js +350 -0
- package/dist/index.js +1 -1
- package/dist/resources/presets.js +1 -1
- package/dist/server.js +25 -10
- package/dist/tools/analyze_optical.d.ts +1 -1
- package/dist/tools/analyze_optical.js +25 -6
- package/package.json +1 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildDynamicDiagnostics = buildDynamicDiagnostics;
|
|
7
|
+
exports.extractLayersImpl = extractLayersImpl;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const image_io_1 = require("./image_io");
|
|
11
|
+
const optical_analyzer_1 = require("./optical_analyzer");
|
|
12
|
+
const security_1 = require("../core/security");
|
|
13
|
+
function buildDynamicDiagnostics(cctKelvin, azimuthDeg, elevationDeg, hlPct, shPct, aoPct, meanGradient, meanSaturation, userIntent) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
const tailoredOptions = [];
|
|
16
|
+
// 1. Thermal & Chromatic Diagnostics
|
|
17
|
+
let thermalDesc = "";
|
|
18
|
+
if (cctKelvin < 3800) {
|
|
19
|
+
thermalDesc = `طيف لوني دافئ جداً (${Math.round(cctKelvin)}K - Tungsten/Golden Hour)`;
|
|
20
|
+
findings.push(`🔥 **انحياز طيفي دافئ**: الصورة مسجلة بحرارة \`${Math.round(cctKelvin)}K\` مع غلبة للأطياف الكهرمانية والبرتقالية في القنوات الضوئية.`);
|
|
21
|
+
tailoredOptions.push(`موازنة حرارة الألوان من \`${Math.round(cctKelvin)}K\` إلى 5500K نهارية محايدة لإزالة المسحة الكهرمانية.`);
|
|
22
|
+
}
|
|
23
|
+
else if (cctKelvin > 6500) {
|
|
24
|
+
thermalDesc = `طيف لوني بارد (${Math.round(cctKelvin)}K - Cool Atmospheric Skylight)`;
|
|
25
|
+
findings.push(`❄️ **انحياز طيفي بارد**: الصورة مسجلة بحرارة \`${Math.round(cctKelvin)}K\` مع غلبة للزرقة والارتداد السماوي البارد.`);
|
|
26
|
+
tailoredOptions.push(`تدفئة المشهد الضوئي برفع حرارة الألوان إلى 3200K (Mood Tungsten) أو 4500K سينمائي.`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
thermalDesc = `طيف نهاري متوازن (${Math.round(cctKelvin)}K - Balanced Daylight)`;
|
|
30
|
+
findings.push(`☀️ **حرارة ألوان متوازنة**: حرارة الألوان عند \`${Math.round(cctKelvin)}K\` تماثل الإضاءة الاستوديوية المتوازنة.`);
|
|
31
|
+
}
|
|
32
|
+
// 2. Lighting Direction & Dynamic Contrast
|
|
33
|
+
const lightDir = (azimuthDeg >= 0 && azimuthDeg < 90) || (azimuthDeg >= 270 && azimuthDeg <= 360) ? "يمين" : "يسار";
|
|
34
|
+
const lightVert = elevationDeg > 50 ? "علوية" : "أفقية جانبية";
|
|
35
|
+
findings.push(`💡 **توجيه الإضاءة الرئيسية**: زاوية السمت \`${Math.round(azimuthDeg)}°\` (إضاءة من جهة ال${lightDir}) مع زاوية ارتفاع \`${Math.round(elevationDeg)}°\` (${lightVert}).`);
|
|
36
|
+
if (shPct > 35.0 && hlPct > 15.0) {
|
|
37
|
+
findings.push(`⚡ **تباين درامي عالي (Chiaroscuro)**: كثافة الظلال \`${shPct.toFixed(1)}%\` مع مساحة ساطعة \`${hlPct.toFixed(1)}%\` تدل على تباين قوي بين المفتاح الضوئي والملء.`);
|
|
38
|
+
tailoredOptions.push("تنعيم التباين ورفع تفاصيل الظلال المغلقة عبر وضع الإضاءة المحيطية (Ambient Fill +0.8 EV).");
|
|
39
|
+
}
|
|
40
|
+
else if (shPct < 12.0 && hlPct < 12.0) {
|
|
41
|
+
findings.push("🌫️ **إضاءة منبسطة ناعمة (Flat Diffuse)**: تباين منخفض وغياب للمناطق الساطعة الحادة، ما يعطي مظهراً هادئاً ومنبسطاً.");
|
|
42
|
+
tailoredOptions.push("إضافة عمق درامي وزيادة التباين بنمط Chiaroscuro عالي التحديد مع إضاءة اتجاهية بارزة.");
|
|
43
|
+
}
|
|
44
|
+
// 3. Grounding & Ambient Occlusion
|
|
45
|
+
if (aoPct < 2.0) {
|
|
46
|
+
findings.push(`⚠️ **فقدان الارتكاز الأرضي (Lack of Grounding)**: نسبة الظلال التلامسية العميقة \`${aoPct.toFixed(2)}%\` تكاد تنعدم، مما قد يظهر العنصر كأنه طافٍ في الفراغ.`);
|
|
47
|
+
tailoredOptions.push("بناء وتثبيت ظل تلامسي أرضي فيزيائي (Contact Shadow Footprint) أسفل أدنى نقطة ارتكاز لمنع الظهور الطافي.");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
findings.push(`⚓ **ارتكاز أرضي متماسك**: نسبة ظلال التلامس والانغلاق الموضعي \`${aoPct.toFixed(1)}%\` تؤمن التصاقاً بصرياً طبيعياً بالأرضية.`);
|
|
51
|
+
}
|
|
52
|
+
// 4. Micro-texture & Edges
|
|
53
|
+
if (meanGradient > 0.03) {
|
|
54
|
+
findings.push(`🔍 **تفاصيل سطحية دقيقة وحواف حادة**: متوسط تباين سوبل \`${meanGradient.toFixed(4)}\` يشير إلى وفرة في التفاصيل المجهرية والأنسجة السطحية الواضحة.`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
findings.push(`🎨 **حواف ناعمة وسطح انسيابي**: متوسط تدرج سوبل \`${meanGradient.toFixed(4)}\` يشير إلى مساحات ناعمة متصلة ومناسبة للتنعيم وإعادة التوزيع.`);
|
|
58
|
+
}
|
|
59
|
+
// 5. Chroma / Saturation
|
|
60
|
+
if (meanSaturation > 0.4) {
|
|
61
|
+
findings.push(`🌈 **تشبع لوني مكثف**: متوسط التشبع \`${meanSaturation.toFixed(3)}\` يظهر كثافة لونية حيوية في البكسلات.`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
findings.push(`🔘 **لوحة لونية هادئة أو معتدلة**: متوسط التشبع \`${meanSaturation.toFixed(3)}\` يعكس تدرجات رصينة غير مبالغ بها.`);
|
|
65
|
+
}
|
|
66
|
+
// User intent or default options
|
|
67
|
+
if (userIntent) {
|
|
68
|
+
tailoredOptions.unshift(`تطبيق طلبك المخصص فوراً: "${userIntent}" بتطابق فيزيائي دقيق مع بيانات الطبقات الست.`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
tailoredOptions.push("إعادة إضاءة بنمط هالة الحواف (Rim Light Halo +1.2 EV) لإبراز حدود المجسم وعزله عن الخلفية.");
|
|
72
|
+
tailoredOptions.push("دمج العنصر في خلفية جديدة مع مطابقة إحصائيات الألوان ودرجة الحرارة وظلال الارتكاز.");
|
|
73
|
+
}
|
|
74
|
+
const intentClause = userIntent ? `, ${userIntent}` : "";
|
|
75
|
+
const gptPrompt = `A master-quality studio photograph${intentClause}, calibrated optical lighting at ${Math.round(azimuthDeg)}° azimuth ` +
|
|
76
|
+
`and ${Math.round(elevationDeg)}° elevation, authentic ${Math.round(cctKelvin)}K color temperature balance, ` +
|
|
77
|
+
`physically-grounded ambient occlusion contact shadows firmly anchoring the base plane, ` +
|
|
78
|
+
`crisp micro-surface geometry (Sobel roughness index ${meanGradient.toFixed(3)}), ` +
|
|
79
|
+
`smooth luminance falloff and authentic subsurface scattering, 85mm prime lens f/2.0.`;
|
|
80
|
+
const nanoPrompt = `optics relight, cct ${Math.round(cctKelvin)}K, light vector azimuth ${Math.round(azimuthDeg)} deg elevation ${Math.round(elevationDeg)} deg, ` +
|
|
81
|
+
`surface roughness ${meanGradient.toFixed(4)}, saturation index ${meanSaturation.toFixed(3)}, ` +
|
|
82
|
+
`ground contact occlusion caster, volumetric photon bounce, high-key rim highlight accents, ` +
|
|
83
|
+
`denoising strength 0.38${intentClause}`;
|
|
84
|
+
return {
|
|
85
|
+
thermalDescription: thermalDesc,
|
|
86
|
+
findings,
|
|
87
|
+
tailoredOptions,
|
|
88
|
+
gptImagePrompt: gptPrompt,
|
|
89
|
+
nanoBananaPrompt: nanoPrompt,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function extractLayersImpl(imagePath, outputDir = "Layers", userIntent) {
|
|
93
|
+
const resolvedImagePath = (0, security_1.validateImagePath)(imagePath);
|
|
94
|
+
const resolvedOutputDir = path_1.default.resolve(outputDir);
|
|
95
|
+
if (!fs_1.default.existsSync(resolvedOutputDir)) {
|
|
96
|
+
fs_1.default.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
const raw = (0, image_io_1.readImage)(resolvedImagePath);
|
|
99
|
+
const { width, height, data } = raw;
|
|
100
|
+
const numPixels = width * height;
|
|
101
|
+
const lumArray = new Float32Array(numPixels);
|
|
102
|
+
let sumR = 0;
|
|
103
|
+
let sumG = 0;
|
|
104
|
+
let sumB = 0;
|
|
105
|
+
let sumSat = 0;
|
|
106
|
+
for (let i = 0; i < numPixels; i++) {
|
|
107
|
+
const idx = i * 4;
|
|
108
|
+
const r = data[idx];
|
|
109
|
+
const g = data[idx + 1];
|
|
110
|
+
const b = data[idx + 2];
|
|
111
|
+
sumR += r;
|
|
112
|
+
sumG += g;
|
|
113
|
+
sumB += b;
|
|
114
|
+
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
115
|
+
lumArray[i] = lum / 255.0;
|
|
116
|
+
const maxC = Math.max(r, g, b);
|
|
117
|
+
const minC = Math.min(r, g, b);
|
|
118
|
+
const chroma = maxC - minC;
|
|
119
|
+
const sat = maxC > 0 ? chroma / maxC : 0;
|
|
120
|
+
sumSat += sat;
|
|
121
|
+
}
|
|
122
|
+
const meanR = sumR / numPixels;
|
|
123
|
+
const meanG = sumG / numPixels;
|
|
124
|
+
const meanB = sumB / numPixels;
|
|
125
|
+
const meanLum = 0.2126 * meanR + 0.7152 * meanG + 0.0722 * meanB;
|
|
126
|
+
const meanSaturation = sumSat / numPixels;
|
|
127
|
+
const cctKelvin = (0, optical_analyzer_1.calculateCctFromRgb)(meanR, meanG, meanB);
|
|
128
|
+
const { roughness, lightVector, angles } = (0, optical_analyzer_1.computeSurfaceNormalsAndLightVector)(lumArray, width, height);
|
|
129
|
+
const bufHighlights = Buffer.alloc(numPixels * 4);
|
|
130
|
+
const bufShadows = Buffer.alloc(numPixels * 4);
|
|
131
|
+
const bufAO = Buffer.alloc(numPixels * 4);
|
|
132
|
+
const bufEdges = Buffer.alloc(numPixels * 4);
|
|
133
|
+
const bufNormals = Buffer.alloc(numPixels * 4);
|
|
134
|
+
const bufChroma = Buffer.alloc(numPixels * 4);
|
|
135
|
+
let highlightsCount = 0;
|
|
136
|
+
let shadowsCount = 0;
|
|
137
|
+
let aoCount = 0;
|
|
138
|
+
let edgeSum = 0;
|
|
139
|
+
const scale = 6.0;
|
|
140
|
+
for (let y = 0; y < height; y++) {
|
|
141
|
+
for (let x = 0; x < width; x++) {
|
|
142
|
+
const idx = y * width + x;
|
|
143
|
+
const pIdx = idx * 4;
|
|
144
|
+
const r = data[pIdx];
|
|
145
|
+
const g = data[pIdx + 1];
|
|
146
|
+
const b = data[pIdx + 2];
|
|
147
|
+
const lum = lumArray[idx] * 255.0;
|
|
148
|
+
// 1. Highlights
|
|
149
|
+
if (lum > 170)
|
|
150
|
+
highlightsCount++;
|
|
151
|
+
const hlMask = Math.min(Math.max((lum - 170.0) / 75.0, 0.0), 1.0);
|
|
152
|
+
bufHighlights[pIdx] = Math.round(r * hlMask);
|
|
153
|
+
bufHighlights[pIdx + 1] = Math.round(g * hlMask);
|
|
154
|
+
bufHighlights[pIdx + 2] = Math.round(b * hlMask);
|
|
155
|
+
bufHighlights[pIdx + 3] = 255;
|
|
156
|
+
// 2. Shadows
|
|
157
|
+
if (lum < 85)
|
|
158
|
+
shadowsCount++;
|
|
159
|
+
const shMask = Math.min(Math.max((95.0 - lum) / 80.0, 0.0), 1.0);
|
|
160
|
+
bufShadows[pIdx] = Math.round(r * shMask);
|
|
161
|
+
bufShadows[pIdx + 1] = Math.round(g * shMask);
|
|
162
|
+
bufShadows[pIdx + 2] = Math.round(b * shMask);
|
|
163
|
+
bufShadows[pIdx + 3] = 255;
|
|
164
|
+
// 3. Deep Ambient Occlusion
|
|
165
|
+
if (lum < 35)
|
|
166
|
+
aoCount++;
|
|
167
|
+
const aoMask = Math.min(Math.max((40.0 - lum) / 40.0, 0.0), 1.0);
|
|
168
|
+
bufAO[pIdx] = Math.round((1.0 - aoMask) * 230.0);
|
|
169
|
+
bufAO[pIdx + 1] = Math.round((1.0 - aoMask) * 240.0 + aoMask * 15.0);
|
|
170
|
+
bufAO[pIdx + 2] = Math.round((1.0 - aoMask) * 255.0 + aoMask * 30.0);
|
|
171
|
+
bufAO[pIdx + 3] = 255;
|
|
172
|
+
// 4. Edges via Sobel
|
|
173
|
+
let gx = 0;
|
|
174
|
+
let gy = 0;
|
|
175
|
+
if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
|
|
176
|
+
gx =
|
|
177
|
+
-lumArray[(y - 1) * width + (x - 1)] +
|
|
178
|
+
lumArray[(y - 1) * width + (x + 1)] -
|
|
179
|
+
2 * lumArray[y * width + (x - 1)] +
|
|
180
|
+
2 * lumArray[y * width + (x + 1)] -
|
|
181
|
+
lumArray[(y + 1) * width + (x - 1)] +
|
|
182
|
+
lumArray[(y + 1) * width + (x + 1)];
|
|
183
|
+
gy =
|
|
184
|
+
-lumArray[(y - 1) * width + (x - 1)] -
|
|
185
|
+
2 * lumArray[(y - 1) * width + x] -
|
|
186
|
+
lumArray[(y - 1) * width + (x + 1)] +
|
|
187
|
+
lumArray[(y + 1) * width + (x - 1)] +
|
|
188
|
+
2 * lumArray[(y + 1) * width + x] +
|
|
189
|
+
lumArray[(y + 1) * width + (x + 1)];
|
|
190
|
+
}
|
|
191
|
+
const edgeMag = Math.sqrt(gx * gx + gy * gy);
|
|
192
|
+
edgeSum += edgeMag;
|
|
193
|
+
const edgeVal = Math.min(Math.round(edgeMag * 4.0 * 255.0), 255);
|
|
194
|
+
bufEdges[pIdx] = edgeVal;
|
|
195
|
+
bufEdges[pIdx + 1] = edgeVal;
|
|
196
|
+
bufEdges[pIdx + 2] = edgeVal;
|
|
197
|
+
bufEdges[pIdx + 3] = 255;
|
|
198
|
+
// 5. Depth Normals
|
|
199
|
+
const nx = -gx * scale;
|
|
200
|
+
const ny = -gy * scale;
|
|
201
|
+
const nz = 1.0;
|
|
202
|
+
const mag = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1e-6;
|
|
203
|
+
const normX = nx / mag;
|
|
204
|
+
const normY = ny / mag;
|
|
205
|
+
const normZ = nz / mag;
|
|
206
|
+
bufNormals[pIdx] = Math.round((normX * 0.5 + 0.5) * 255);
|
|
207
|
+
bufNormals[pIdx + 1] = Math.round((normY * 0.5 + 0.5) * 255);
|
|
208
|
+
bufNormals[pIdx + 2] = Math.round((normZ * 0.5 + 0.5) * 255);
|
|
209
|
+
bufNormals[pIdx + 3] = 255;
|
|
210
|
+
// 6. Chroma / Saturation
|
|
211
|
+
const maxC = Math.max(r, g, b);
|
|
212
|
+
const minC = Math.min(r, g, b);
|
|
213
|
+
const chroma = maxC - minC;
|
|
214
|
+
const sat = maxC > 0 ? chroma / maxC : 0;
|
|
215
|
+
bufChroma[pIdx] = Math.min(Math.round(sat * 255.0 * 1.5), 255);
|
|
216
|
+
bufChroma[pIdx + 1] = Math.min(Math.round((1.0 - Math.abs(sat - 0.5) * 2.0) * 220.0), 255);
|
|
217
|
+
bufChroma[pIdx + 2] = Math.min(Math.round((1.0 - sat) * 200.0), 255);
|
|
218
|
+
bufChroma[pIdx + 3] = 255;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const hlPath = path_1.default.join(resolvedOutputDir, "01_highlights.png");
|
|
222
|
+
const shPath = path_1.default.join(resolvedOutputDir, "02_shadows.png");
|
|
223
|
+
const aoPath = path_1.default.join(resolvedOutputDir, "03_ambient_occlusion.png");
|
|
224
|
+
const edPath = path_1.default.join(resolvedOutputDir, "04_edges.png");
|
|
225
|
+
const normPath = path_1.default.join(resolvedOutputDir, "05_depth_normals.png");
|
|
226
|
+
const chrPath = path_1.default.join(resolvedOutputDir, "06_chroma_saturation.png");
|
|
227
|
+
(0, image_io_1.writeImage)(hlPath, { width, height, data: bufHighlights });
|
|
228
|
+
(0, image_io_1.writeImage)(shPath, { width, height, data: bufShadows });
|
|
229
|
+
(0, image_io_1.writeImage)(aoPath, { width, height, data: bufAO });
|
|
230
|
+
(0, image_io_1.writeImage)(edPath, { width, height, data: bufEdges });
|
|
231
|
+
(0, image_io_1.writeImage)(normPath, { width, height, data: bufNormals });
|
|
232
|
+
(0, image_io_1.writeImage)(chrPath, { width, height, data: bufChroma });
|
|
233
|
+
const hlPct = (highlightsCount / numPixels) * 100.0;
|
|
234
|
+
const shPct = (shadowsCount / numPixels) * 100.0;
|
|
235
|
+
const aoPct = (aoCount / numPixels) * 100.0;
|
|
236
|
+
const meanEdge = edgeSum / numPixels;
|
|
237
|
+
// Build Dynamic Diagnostics
|
|
238
|
+
const diagnostics = buildDynamicDiagnostics(cctKelvin, angles.azimuthDeg, angles.elevationDeg, hlPct, shPct, aoPct, meanEdge, meanSaturation, userIntent);
|
|
239
|
+
const layerMdPath = path_1.default.join(resolvedOutputDir, "Layer.md");
|
|
240
|
+
const toForwardSlash = (p) => p.replace(/\\/g, "/");
|
|
241
|
+
const findingsRendered = diagnostics.findings.join("\n\n");
|
|
242
|
+
const optionsRendered = diagnostics.tailoredOptions.map((opt, idx) => `- **خيار ${idx + 1}**: ${opt}`).join("\n");
|
|
243
|
+
const userIntentHeader = userIntent ? `\n**الهدف المخصص المطلوب**: \`${userIntent}\`\n` : "";
|
|
244
|
+
const mdContent = `# تقرير الطبقات التحليلية الست (Layer.md)
|
|
245
|
+
**مصدر الصورة**: \`${resolvedImagePath}\`
|
|
246
|
+
**أبعاد الصورة**: \`${width}x${height}\` بكسل
|
|
247
|
+
**التصنيف الضوئي المكتشف**: \`${diagnostics.thermalDescription}\`
|
|
248
|
+
**زاوية الإضاءة المسجلة**: سمت \`${angles.azimuthDeg}°\` | ارتفاع \`${angles.elevationDeg}°\`${userIntentHeader}
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 1. فهرس الطبقات التحليلية الست (The 6 Extracted Layers)
|
|
253
|
+
|
|
254
|
+
| # | اسم الطبقة | الملف المولد | القراءة الفيزيائية المكتشفة |
|
|
255
|
+
|---|---|---|---|
|
|
256
|
+
| **1** | **طبقة الألوان الفاتحة (Highlights)** | [\`01_highlights.png\`](file:///${toForwardSlash(hlPath)}) | نسبة تغطية الأضواء الساطعة: \`${hlPct.toFixed(2)}%\` |
|
|
257
|
+
| **2** | **طبقة الألوان الغامقة (Shadows)** | [\`02_shadows.png\`](file:///${toForwardSlash(shPath)}) | مساحة الظلال والمناطق الداكنة: \`${shPct.toFixed(2)}%\` |
|
|
258
|
+
| **3** | **طبقة الظل العالي (Ambient Occlusion)** | [\`03_ambient_occlusion.png\`](file:///${toForwardSlash(aoPath)}) | كثافة الارتكاز الأرضي وظلال التلامس: \`${aoPct.toFixed(2)}%\` |
|
|
259
|
+
| **4** | **طبقة الحواف (Edges & Contours)** | [\`04_edges.png\`](file:///${toForwardSlash(edPath)}) | تباين الحواف المجهرية لسوبل: \`${meanEdge.toFixed(4)}\` |
|
|
260
|
+
| **5** | **طبقة العمق (3D Depth Normals)** | [\`05_depth_normals.png\`](file:///${toForwardSlash(normPath)}) | توجيه التنسور المتجهي ثلاثي الأبعاد: سمت \`${angles.azimuthDeg}°\` |
|
|
261
|
+
| **6** | **طبقة الألوان والتشبع (Chroma / Saturation)** | [\`06_chroma_saturation.png\`](file:///${toForwardSlash(chrPath)}) | متوسط تشبع وتوزيع نقاء الألوان: \`${meanSaturation.toFixed(3)}\` |
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 2. النتائج البصرية الديناميكية المستخلصة من الصورة (Dynamic Findings)
|
|
266
|
+
|
|
267
|
+
${findingsRendered}
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 3. التوجيه التوليدي المخصص (Calibrated Prompts for Generative Engines)
|
|
272
|
+
|
|
273
|
+
### أ. موجه GPT Image (DALL-E 3 / GPT-4o Vision):
|
|
274
|
+
> "${diagnostics.gptImagePrompt}"
|
|
275
|
+
|
|
276
|
+
### ب. موجه Nano Banana (High-Density Optical Tokenizer):
|
|
277
|
+
> "${diagnostics.nanoBananaPrompt}"
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 4. الخطوة التفاعلية الموجهة بالبيانات (Dynamic Interactive Decision)
|
|
282
|
+
|
|
283
|
+
**ماذا تريد من تعديل؟**
|
|
284
|
+
بناءً على المعطيات الضوئية المكتشفة في هذه الصورة تحديداً، إليك خيارات التعديل المقترحة:
|
|
285
|
+
|
|
286
|
+
${optionsRendered}
|
|
287
|
+
|
|
288
|
+
💬 *يمكنك أيضاً طلب أي تعديل مخصص بحرية تامة، وسيتم فوراً توليد برومبت دقيق للغاية أو إنشاء الصورة المعدلة بالكامل!*
|
|
289
|
+
`;
|
|
290
|
+
fs_1.default.writeFileSync(layerMdPath, mdContent, "utf-8");
|
|
291
|
+
return {
|
|
292
|
+
sourceImage: resolvedImagePath,
|
|
293
|
+
layersDirectory: resolvedOutputDir,
|
|
294
|
+
dimensions: [width, height],
|
|
295
|
+
opticalMetrics: {
|
|
296
|
+
cctKelvin: Math.round(cctKelvin * 10) / 10,
|
|
297
|
+
dominantLightVector: lightVector,
|
|
298
|
+
lightingAngles: angles,
|
|
299
|
+
meanLuminance: Math.round(meanLum * 100) / 100,
|
|
300
|
+
meanSaturation: Math.round(meanSaturation * 1000) / 1000,
|
|
301
|
+
edgeRoughness: Math.round(roughness * 10000) / 10000,
|
|
302
|
+
},
|
|
303
|
+
diagnostics,
|
|
304
|
+
layers: {
|
|
305
|
+
highlights: {
|
|
306
|
+
file: "01_highlights.png",
|
|
307
|
+
path: hlPath,
|
|
308
|
+
description: "طبقة الألوان الفاتحة - High-key Luminance & Specular Zones",
|
|
309
|
+
metricLabel: "coveragePct",
|
|
310
|
+
metricValue: Math.round(hlPct * 100) / 100,
|
|
311
|
+
},
|
|
312
|
+
shadows: {
|
|
313
|
+
file: "02_shadows.png",
|
|
314
|
+
path: shPath,
|
|
315
|
+
description: "طبقة الألوان الغامقة - Low-key & Shadow Zones",
|
|
316
|
+
metricLabel: "coveragePct",
|
|
317
|
+
metricValue: Math.round(shPct * 100) / 100,
|
|
318
|
+
},
|
|
319
|
+
ambientOcclusion: {
|
|
320
|
+
file: "03_ambient_occlusion.png",
|
|
321
|
+
path: aoPath,
|
|
322
|
+
description: "طبقة الظل العالي - High Contact Shadows & Deep Occlusion Crevices",
|
|
323
|
+
metricLabel: "coveragePct",
|
|
324
|
+
metricValue: Math.round(aoPct * 100) / 100,
|
|
325
|
+
},
|
|
326
|
+
edges: {
|
|
327
|
+
file: "04_edges.png",
|
|
328
|
+
path: edPath,
|
|
329
|
+
description: "طبقة الحواف - Sobel High-Frequency Gradient Contours",
|
|
330
|
+
metricLabel: "meanGradient",
|
|
331
|
+
metricValue: Math.round(meanEdge * 10000) / 10000,
|
|
332
|
+
},
|
|
333
|
+
depthNormals: {
|
|
334
|
+
file: "05_depth_normals.png",
|
|
335
|
+
path: normPath,
|
|
336
|
+
description: "طبقة العمق - 3D Surface Normal Gradient Field (RGB Tangent Map)",
|
|
337
|
+
metricLabel: "dominantAzimuth",
|
|
338
|
+
metricValue: angles.azimuthDeg,
|
|
339
|
+
},
|
|
340
|
+
chromaSaturation: {
|
|
341
|
+
file: "06_chroma_saturation.png",
|
|
342
|
+
path: chrPath,
|
|
343
|
+
description: "طبقة الألوان والتشبع والبكسلات - Chrominance & Saturation Distribution",
|
|
344
|
+
metricLabel: "meanSaturation",
|
|
345
|
+
metricValue: Math.round(meanSaturation * 1000) / 1000,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
layerMarkdownPath: layerMdPath,
|
|
349
|
+
};
|
|
350
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ async function main() {
|
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
if (args.includes("--verify")) {
|
|
10
10
|
console.log("[MarwanDevSpace] Verifying TypeScript mcp-relight-harmonize server...");
|
|
11
|
-
console.log("Server 'mcp-relight-harmonize' v1.0.
|
|
11
|
+
console.log("Server 'mcp-relight-harmonize' v1.0.3 initialized.");
|
|
12
12
|
console.log("Registered tools: ['analyze_optical_profile', 'generate_relight_variations', 'harmonize_composite', 'synthesize_diffusion_prompt', 'list_cached_variations']");
|
|
13
13
|
console.log("Registered resources: ['optical://presets']");
|
|
14
14
|
console.log("Output cache directory:", (0, config_1.ensureOutputDirectory)());
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LIGHTING_PRESETS_REFERENCE = void 0;
|
|
4
4
|
exports.getPresetsJson = getPresetsJson;
|
|
5
5
|
exports.LIGHTING_PRESETS_REFERENCE = {
|
|
6
|
-
version: "1.0.
|
|
6
|
+
version: "1.0.3",
|
|
7
7
|
target_generators: ["GPT Image", "Nano Banana"],
|
|
8
8
|
presets: [
|
|
9
9
|
{
|
package/dist/server.js
CHANGED
|
@@ -46,7 +46,7 @@ const StandardEnvelopeSchema = {
|
|
|
46
46
|
function createServer() {
|
|
47
47
|
const server = new index_js_1.Server({
|
|
48
48
|
name: "mcp-relight-harmonize",
|
|
49
|
-
version: "1.0.
|
|
49
|
+
version: "1.0.3",
|
|
50
50
|
}, {
|
|
51
51
|
capabilities: {
|
|
52
52
|
tools: {},
|
|
@@ -61,14 +61,14 @@ function createServer() {
|
|
|
61
61
|
name: "analyze_optical_profile",
|
|
62
62
|
description: "Extract physical optical metrics from an image, including Correlated Color Temperature (CCT in Kelvin), " +
|
|
63
63
|
"dominant 3D lighting vector (azimuth and elevation angles), photometric luminance dynamic range, contrast zones, " +
|
|
64
|
-
"
|
|
65
|
-
"• Purpose: Diagnostic optical extraction
|
|
66
|
-
"
|
|
67
|
-
"• Behavior:
|
|
68
|
-
"• When to use: Use as the prerequisite first step
|
|
69
|
-
"• When NOT to use: Do NOT use if you need
|
|
70
|
-
"(use 'harmonize_composite'), or if you need generative AI
|
|
71
|
-
"• Alternatives: Use 'generate_relight_variations' for
|
|
64
|
+
"surface normal roughness index, and optionally decompose into 6 analytical image layers in Layers/ directory.\n\n" +
|
|
65
|
+
"• Purpose: Diagnostic optical extraction and 6-layer decomposition (Highlights, Shadows, Ambient Occlusion, Edges, Depth Normals, Chroma/Saturation). " +
|
|
66
|
+
"Unlike 'generate_relight_variations' which creates artistic relit renders, this tool extracts physical diagnostic metrics and visual analytical decomposition layers.\n" +
|
|
67
|
+
"• Behavior: Read-only by default; writes 6 analytical layer PNGs and Layer.md to the specified directory when 'extract_layers' is true. Unmetered local execution, zero network egress, zero external auth.\n" +
|
|
68
|
+
"• When to use: Use as the prerequisite first step to inspect baseline lighting conditions or generate the 6 diagnostic layers into Layers/ before relighting or prompting.\n" +
|
|
69
|
+
"• When NOT to use: Do NOT use if you need creative relighted styles (use 'generate_relight_variations'), if merging a cutout into a scene " +
|
|
70
|
+
"(use 'harmonize_composite'), or if you only need generative AI prompt synthesis (use 'synthesize_diffusion_prompt').\n" +
|
|
71
|
+
"• Alternatives: Use 'generate_relight_variations' for creative lighting styles, or 'synthesize_diffusion_prompt' for model prompts.",
|
|
72
72
|
inputSchema: {
|
|
73
73
|
type: "object",
|
|
74
74
|
properties: {
|
|
@@ -77,6 +77,18 @@ function createServer() {
|
|
|
77
77
|
description: "Absolute or workspace-relative path to a local image file (.png, .jpg, or .jpeg). " +
|
|
78
78
|
"Must be an existing image under 50 MB.",
|
|
79
79
|
},
|
|
80
|
+
extract_layers: {
|
|
81
|
+
type: "boolean",
|
|
82
|
+
description: "If true, decomposes image into 6 analytical layers (Highlights, Shadows, Ambient Occlusion, Edges, Depth Normals, Chroma/Saturation) saved to Layers/ directory and generates Layer.md.",
|
|
83
|
+
},
|
|
84
|
+
layers_dir: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Optional target directory to store the 6 analytical layer images and Layer.md (defaults to 'Layers').",
|
|
87
|
+
},
|
|
88
|
+
user_intent: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Optional creative or corrective intent to dynamically tailor layer diagnostics, recommendations, and diffusion prompts.",
|
|
91
|
+
},
|
|
80
92
|
},
|
|
81
93
|
required: ["image_path"],
|
|
82
94
|
},
|
|
@@ -384,7 +396,10 @@ function createServer() {
|
|
|
384
396
|
switch (name) {
|
|
385
397
|
case "analyze_optical_profile": {
|
|
386
398
|
const imagePath = String(args.image_path || "");
|
|
387
|
-
|
|
399
|
+
const extractLayers = Boolean(args.extract_layers);
|
|
400
|
+
const layersDir = args.layers_dir ? String(args.layers_dir) : undefined;
|
|
401
|
+
const userIntent = args.user_intent ? String(args.user_intent) : undefined;
|
|
402
|
+
envelope = (0, analyze_optical_1.analyzeOpticalProfileTool)(imagePath, extractLayers, layersDir, userIntent);
|
|
388
403
|
break;
|
|
389
404
|
}
|
|
390
405
|
case "generate_relight_variations": {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ResultEnvelope } from "../core/envelope";
|
|
2
|
-
export declare function analyzeOpticalProfileTool(imagePath: string): ResultEnvelope;
|
|
2
|
+
export declare function analyzeOpticalProfileTool(imagePath: string, extractLayers?: boolean, layersDir?: string, userIntent?: string): ResultEnvelope;
|
|
@@ -8,19 +8,38 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
8
8
|
const envelope_1 = require("../core/envelope");
|
|
9
9
|
const errors_1 = require("../core/errors");
|
|
10
10
|
const optical_analyzer_1 = require("../domain/optical_analyzer");
|
|
11
|
-
|
|
11
|
+
const layer_extractor_1 = require("../domain/layer_extractor");
|
|
12
|
+
function analyzeOpticalProfileTool(imagePath, extractLayers = false, layersDir, userIntent) {
|
|
12
13
|
try {
|
|
13
14
|
const report = (0, optical_analyzer_1.analyzeOpticalProfileImpl)(imagePath);
|
|
14
15
|
const hash = crypto_1.default.createHash("sha256").update(imagePath).digest("hex").slice(0, 12);
|
|
15
|
-
|
|
16
|
+
let layerReport;
|
|
17
|
+
const artifacts = [];
|
|
18
|
+
if (extractLayers || layersDir) {
|
|
19
|
+
const targetDir = layersDir || "Layers";
|
|
20
|
+
layerReport = (0, layer_extractor_1.extractLayersImpl)(imagePath, targetDir, userIntent);
|
|
21
|
+
artifacts.push({ label: "Layer 1 - Highlights", uri: `file://${layerReport.layers.highlights.path}` }, { label: "Layer 2 - Shadows", uri: `file://${layerReport.layers.shadows.path}` }, { label: "Layer 3 - Ambient Occlusion", uri: `file://${layerReport.layers.ambientOcclusion.path}` }, { label: "Layer 4 - Edges", uri: `file://${layerReport.layers.edges.path}` }, { label: "Layer 5 - Depth Normals", uri: `file://${layerReport.layers.depthNormals.path}` }, { label: "Layer 6 - Chroma Saturation", uri: `file://${layerReport.layers.chromaSaturation.path}` }, { label: "Layer Markdown Report", uri: `file://${layerReport.layerMarkdownPath}` });
|
|
22
|
+
}
|
|
23
|
+
const payload = { ...report };
|
|
24
|
+
if (layerReport) {
|
|
25
|
+
payload.layersDirectory = layerReport.layersDirectory;
|
|
26
|
+
payload.layers = layerReport.layers;
|
|
27
|
+
payload.layerMarkdownPath = layerReport.layerMarkdownPath;
|
|
28
|
+
}
|
|
29
|
+
const nextActions = [
|
|
30
|
+
`Call 'generate_relight_variations' on '${imagePath}' to explore alternative lighting schemes.`,
|
|
31
|
+
`Call 'synthesize_diffusion_prompt' targeting GPT Image or Nano Banana for high-res generative inpainting.`,
|
|
32
|
+
];
|
|
33
|
+
if (layerReport) {
|
|
34
|
+
nextActions.unshift(`Inspect 6 analytical layers and Layer.md, then present to user with prompt: 'ماذا تريد من تعديل؟'`);
|
|
35
|
+
}
|
|
36
|
+
return (0, envelope_1.createEnvelope)("success", `Optical profile extracted successfully: ${report.opticalProfileSummary}${layerReport ? ` (6 Analytical layers generated in '${layerReport.layersDirectory}')` : ""}`, payload, {
|
|
16
37
|
evidence: {
|
|
17
38
|
inputsDigest: `sha256:${hash}`,
|
|
18
39
|
sources: [{ label: "Source Image", uri: `file://${report.imagePath}` }],
|
|
40
|
+
artifacts,
|
|
19
41
|
},
|
|
20
|
-
nextActions
|
|
21
|
-
`Call 'generate_relight_variations' on '${imagePath}' to explore alternative lighting schemes.`,
|
|
22
|
-
`Call 'synthesize_diffusion_prompt' targeting GPT Image or Nano Banana for high-res generative inpainting.`,
|
|
23
|
-
],
|
|
42
|
+
nextActions,
|
|
24
43
|
});
|
|
25
44
|
}
|
|
26
45
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-relight-harmonize",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Professional TypeScript Model Context Protocol (MCP) server & agent skill for optical profiling, physical relighting, composite harmonization, and diffusion prompt synthesis (GPT Image & Nano Banana).",
|
|
5
5
|
"author": "MarwanDevSpace <dev@marwandevspace.io>",
|
|
6
6
|
"license": "MIT",
|