mcp-relight-harmonize 1.0.2 → 1.0.4

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.
@@ -0,0 +1,352 @@
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 universalPrompt = `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
+ universalImagePrompt: universalPrompt,
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 Image Generators)
272
+
273
+ ### أ. النمط الوصفي الاستوديوي (Any Image Generator Model):
274
+ > "${diagnostics.universalImagePrompt}"
275
+
276
+ ### ب. نمط الشيدر الضوئي المكثف (GEMINI Nano Banana - Antigravity):
277
+ > "${diagnostics.nanoBananaPrompt}"
278
+
279
+ > 💡 **ملاحظة**: هذا التوجيه متوافق مع **أي Image Generator Model**، ويُفضل ويُوصى بشدة باستخدامه داخل **Google Antigravity** لتطبيق التعديل المباشر عبر أداة \`generate_image\` ومحرك **GEMINI Nano Banana**!
280
+
281
+ ---
282
+
283
+ ## 4. الخطوة التفاعلية الموجهة بالبيانات (Dynamic Interactive Decision)
284
+
285
+ **ماذا تريد من تعديل؟**
286
+ بناءً على المعطيات الضوئية المكتشفة في هذه الصورة تحديداً، إليك خيارات التعديل المقترحة:
287
+
288
+ ${optionsRendered}
289
+
290
+ 💬 *يمكنك أيضاً طلب أي تعديل مخصص بحرية تامة، وسيتم فوراً توليد برومبت دقيق للغاية أو إنشاء الصورة المعدلة بالكامل!*
291
+ `;
292
+ fs_1.default.writeFileSync(layerMdPath, mdContent, "utf-8");
293
+ return {
294
+ sourceImage: resolvedImagePath,
295
+ layersDirectory: resolvedOutputDir,
296
+ dimensions: [width, height],
297
+ opticalMetrics: {
298
+ cctKelvin: Math.round(cctKelvin * 10) / 10,
299
+ dominantLightVector: lightVector,
300
+ lightingAngles: angles,
301
+ meanLuminance: Math.round(meanLum * 100) / 100,
302
+ meanSaturation: Math.round(meanSaturation * 1000) / 1000,
303
+ edgeRoughness: Math.round(roughness * 10000) / 10000,
304
+ },
305
+ diagnostics,
306
+ layers: {
307
+ highlights: {
308
+ file: "01_highlights.png",
309
+ path: hlPath,
310
+ description: "طبقة الألوان الفاتحة - High-key Luminance & Specular Zones",
311
+ metricLabel: "coveragePct",
312
+ metricValue: Math.round(hlPct * 100) / 100,
313
+ },
314
+ shadows: {
315
+ file: "02_shadows.png",
316
+ path: shPath,
317
+ description: "طبقة الألوان الغامقة - Low-key & Shadow Zones",
318
+ metricLabel: "coveragePct",
319
+ metricValue: Math.round(shPct * 100) / 100,
320
+ },
321
+ ambientOcclusion: {
322
+ file: "03_ambient_occlusion.png",
323
+ path: aoPath,
324
+ description: "طبقة الظل العالي - High Contact Shadows & Deep Occlusion Crevices",
325
+ metricLabel: "coveragePct",
326
+ metricValue: Math.round(aoPct * 100) / 100,
327
+ },
328
+ edges: {
329
+ file: "04_edges.png",
330
+ path: edPath,
331
+ description: "طبقة الحواف - Sobel High-Frequency Gradient Contours",
332
+ metricLabel: "meanGradient",
333
+ metricValue: Math.round(meanEdge * 10000) / 10000,
334
+ },
335
+ depthNormals: {
336
+ file: "05_depth_normals.png",
337
+ path: normPath,
338
+ description: "طبقة العمق - 3D Surface Normal Gradient Field (RGB Tangent Map)",
339
+ metricLabel: "dominantAzimuth",
340
+ metricValue: angles.azimuthDeg,
341
+ },
342
+ chromaSaturation: {
343
+ file: "06_chroma_saturation.png",
344
+ path: chrPath,
345
+ description: "طبقة الألوان والتشبع والبكسلات - Chrominance & Saturation Distribution",
346
+ metricLabel: "meanSaturation",
347
+ metricValue: Math.round(meanSaturation * 1000) / 1000,
348
+ },
349
+ },
350
+ layerMarkdownPath: layerMdPath,
351
+ };
352
+ }
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.synthesizeDiffusionPromptImpl = synthesizeDiffusionPromptImpl;
4
4
  const optical_analyzer_1 = require("./optical_analyzer");
5
- function synthesizeDiffusionPromptImpl(imagePath, userIntent = "", targetModel = "gpt_image") {
5
+ function synthesizeDiffusionPromptImpl(imagePath, userIntent = "", targetModel = "universal") {
6
6
  const profile = (0, optical_analyzer_1.analyzeOpticalProfileImpl)(imagePath);
7
- const normalizedModel = targetModel.toLowerCase().includes("banana") ? "Nano Banana" : "GPT Image";
7
+ const normalizedModel = targetModel.toLowerCase().includes("banana")
8
+ ? "GEMINI Nano Banana"
9
+ : "Universal Image Generator";
8
10
  const cct = Math.round(profile.colorTemperatureKelvin);
9
11
  const azimuth = profile.lightingAngles.azimuthDeg;
10
12
  const elevation = profile.lightingAngles.elevationDeg;
@@ -50,7 +52,7 @@ function synthesizeDiffusionPromptImpl(imagePath, userIntent = "", targetModel =
50
52
  let enhancementPrompt = "";
51
53
  let relightingPrompt = "";
52
54
  let recommendedParameters = {};
53
- if (normalizedModel === "GPT Image") {
55
+ if (normalizedModel === "Universal Image Generator") {
54
56
  enhancementPrompt =
55
57
  `A master-quality studio photograph, exquisite micro-surface textures, pores and fine material grain, ` +
56
58
  `subsurface scattering, 85mm prime lens at f/2.0, razor-sharp optical boundary and crystal-clear geometry${cleanIntent}.`;
@@ -59,16 +61,17 @@ function synthesizeDiffusionPromptImpl(imagePath, userIntent = "", targetModel =
59
61
  `${cctTerm}, ${contrastTerm}, subtle rim lighting tracing the outer silhouette, volumetric dust rays visible in the air, ` +
60
62
  `physically-based contact shadows naturally anchoring the base to the ground plane, authentic photometric falloff${cleanIntent}.`;
61
63
  recommendedParameters = {
62
- model: "gpt-image-dalle3",
64
+ model: "universal-image-generator",
63
65
  style: "natural",
64
66
  quality: "hd",
65
67
  camera_lens: "85mm prime f/2.0",
66
68
  denoising_strength: 0.38,
67
69
  recommended_dimensions: `${profile.dimensions[0]}x${profile.dimensions[1]}`,
70
+ compatibility: "Works with any Image Generator Model; optimized for Antigravity",
68
71
  };
69
72
  }
70
73
  else {
71
- // Nano Banana Target
74
+ // GEMINI Nano Banana Target
72
75
  enhancementPrompt =
73
76
  `ultra-detailed optical capture, raw sensor clarity, 8k uhd, micro-pores, surface specular roughness index ${profile.surfaceNormalVariation.toFixed(3)}, ` +
74
77
  `zero chromatic aberration, pristine alpha edge delineation${cleanIntent}`;
@@ -77,7 +80,8 @@ function synthesizeDiffusionPromptImpl(imagePath, userIntent = "", targetModel =
77
80
  `volumetric raytraced bounce, physically-grounded ground contact shadow, ambient occlusion caster, ` +
78
81
  `denoising 0.38, light_azimuth_${Math.round(azimuth)}deg${cleanIntent}`;
79
82
  recommendedParameters = {
80
- model: "nano-banana-optical-v1",
83
+ model: "gemini-nano-banana",
84
+ target_environment: "Google Antigravity",
81
85
  denoising_strength: 0.38,
82
86
  guidance_scale: 4.5,
83
87
  steps: 32,
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.2 initialized.");
11
+ console.log("Server 'mcp-relight-harmonize' v1.0.4 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,8 +3,8 @@ 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.2",
7
- target_generators: ["GPT Image", "Nano Banana"],
6
+ version: "1.0.4",
7
+ target_generators: ["Universal Image Generator", "GEMINI Nano Banana"],
8
8
  presets: [
9
9
  {
10
10
  name: "Ambient",
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.2",
49
+ version: "1.0.4",
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
- "and surface normal roughness index.\n\n" +
65
- "• Purpose: Diagnostic optical extraction. Unlike 'generate_relight_variations', this tool produces no image files, " +
66
- "and unlike 'synthesize_diffusion_prompt', it returns pure numerical color-science data rather than text prompts.\n" +
67
- "• Behavior: Completely read-only, deterministic, zero filesystem modifications, no network egress, and no authentication required.\n" +
68
- "• When to use: Use as the prerequisite first step before relighting, inpainting, or compositing an image to inspect baseline lighting conditions.\n" +
69
- "• When NOT to use: Do NOT use if you need modified image files on disk (use 'generate_relight_variations'), if merging a cutout into a scene " +
70
- "(use 'harmonize_composite'), or if you need generative AI prompts (use 'synthesize_diffusion_prompt').\n" +
71
- "• Alternatives: Use 'generate_relight_variations' for visual lighting files, or 'synthesize_diffusion_prompt' for model prompts.",
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
  },
@@ -149,7 +161,7 @@ function createServer() {
149
161
  "• When to use: Use when you need tangible image alternatives of a photo or product render with alternative lighting schemes.\n" +
150
162
  "• When NOT to use: Do NOT use if you only need optical metrics (use 'analyze_optical_profile'), if blending a cutout into a background " +
151
163
  "(use 'harmonize_composite'), or if you need diffusion AI text prompts (use 'synthesize_diffusion_prompt').\n" +
152
- "• Alternatives: Use 'synthesize_diffusion_prompt' for text prompts targeting GPT Image or Nano Banana, or 'list_cached_variations' to browse existing outputs.",
164
+ "• Alternatives: Use 'synthesize_diffusion_prompt' for your Image Generator (GEMINI Nano Banana / Universal), or 'list_cached_variations' to browse existing outputs.",
153
165
  inputSchema: {
154
166
  type: "object",
155
167
  properties: {
@@ -268,12 +280,12 @@ function createServer() {
268
280
  {
269
281
  name: "synthesize_diffusion_prompt",
270
282
  description: "Synthesize precision enhancement and relighting diffusion prompts based on physical optical analysis of an image, " +
271
- "tailored specifically for GPT Image (DALL-E 3 / GPT-4o) and Nano Banana. Outputs photorealistic prompts with physical keywords " +
283
+ "compatible with Any Image Generator Model (optimized for GEMINI Nano Banana). Outputs photorealistic prompts with physical keywords " +
272
284
  "(exact Kelvin CCT, 3D light angles, volumetric dust rays, contact shadows) and calibrated denoising parameters (0.35 - 0.45).\n\n" +
273
285
  "• Purpose: Generative AI prompt synthesis. Unlike 'generate_relight_variations' which creates image files locally, this tool translates " +
274
- "optical geometry into targeted text prompts and hyperparameter sets for external diffusion generators.\n" +
286
+ "optical geometry into targeted text prompts and hyperparameter sets for Any Image Generator (optimized for Antigravity).\n" +
275
287
  "• Behavior: Completely read-only, deterministic, zero filesystem modifications, no network calls, and no authentication required.\n" +
276
- "• When to use: Use when you want to feed photorealistic lighting directives or inpainting prompts into GPT Image or Nano Banana.\n" +
288
+ "• When to use: Use when you want to feed photorealistic lighting directives or inpainting prompts into your Image Generator or Antigravity's generate_image.\n" +
277
289
  "• When NOT to use: Do NOT use if you need local image rendering without an external AI model (use 'generate_relight_variations'), " +
278
290
  "or if merging cutouts locally (use 'harmonize_composite').\n" +
279
291
  "• Alternatives: Use 'generate_relight_variations' for instant offline image files, or 'analyze_optical_profile' for raw numerical statistics.",
@@ -291,10 +303,10 @@ function createServer() {
291
303
  },
292
304
  target_model: {
293
305
  type: "string",
294
- enum: ["gpt_image", "nano_banana"],
295
- default: "gpt_image",
296
- description: "Target generative engine: 'gpt_image' (outputs natural descriptive studio directives with 85mm prime lens and physical illumination) " +
297
- "or 'nano_banana' (outputs dense tokenized optical shaders, roughness index, raytraced bounce, and ground contact shadow). Defaults to 'gpt_image'.",
306
+ enum: ["universal", "nano_banana"],
307
+ default: "universal",
308
+ description: "Target generative engine format: 'universal' (outputs natural descriptive studio directives with 85mm prime lens and physical illumination compatible with Any Image Generator) " +
309
+ "or 'nano_banana' (outputs dense tokenized optical shaders for GEMINI Nano Banana). Defaults to 'universal'.",
298
310
  },
299
311
  },
300
312
  required: ["image_path"],
@@ -306,7 +318,7 @@ function createServer() {
306
318
  data: {
307
319
  type: "object",
308
320
  properties: {
309
- targetModel: { type: "string", enum: ["GPT Image", "Nano Banana"] },
321
+ targetModel: { type: "string" },
310
322
  userIntent: { type: "string" },
311
323
  enhancementPrompt: { type: "string", description: "Prompt for micro-surface detail and lens clarity upgrade." },
312
324
  relightingPrompt: { type: "string", description: "Prompt for physical relighting with angles, CCT, and contact shadows." },
@@ -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
- envelope = (0, analyze_optical_1.analyzeOpticalProfileTool)(imagePath);
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": {
@@ -404,7 +419,7 @@ function createServer() {
404
419
  case "synthesize_diffusion_prompt": {
405
420
  const imagePath = String(args.image_path || "");
406
421
  const userIntent = String(args.user_intent || "");
407
- const targetModel = String(args.target_model || "gpt_image");
422
+ const targetModel = String(args.target_model || "universal");
408
423
  envelope = (0, synthesize_prompt_1.synthesizeDiffusionPromptTool)(imagePath, userIntent, targetModel);
409
424
  break;
410
425
  }
@@ -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
- function analyzeOpticalProfileTool(imagePath) {
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
- return (0, envelope_1.createEnvelope)("success", `Optical profile extracted successfully: ${report.opticalProfileSummary}`, report, {
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' for your Image Generator (GEMINI Nano Banana / Universal) or trigger 'generate_image'.`,
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) {
@@ -25,7 +25,7 @@ function generateRelightVariationsTool(imagePath, targetLighting = "All", output
25
25
  },
26
26
  nextActions: [
27
27
  "Review generated variation files to inspect aesthetic illumination.",
28
- "Use 'synthesize_diffusion_prompt' for GPT Image or Nano Banana generative enhancement.",
28
+ "Use 'synthesize_diffusion_prompt' for your Image Generator (GEMINI Nano Banana / Universal) or trigger 'generate_image'.",
29
29
  ],
30
30
  });
31
31
  }
@@ -32,7 +32,7 @@ function harmonizeCompositeTool(foregroundPath, backgroundPath, blendMode = "sea
32
32
  },
33
33
  nextActions: [
34
34
  `Inspect the composite output at '${result.compositeImagePath}'.`,
35
- "Call 'synthesize_diffusion_prompt' targeting GPT Image or Nano Banana for seamless boundary inpainting.",
35
+ "Call 'synthesize_diffusion_prompt' for your Image Generator (GEMINI Nano Banana / Universal) or trigger 'generate_image'.",
36
36
  ],
37
37
  });
38
38
  }
@@ -8,7 +8,7 @@ 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 prompt_synthesizer_1 = require("../domain/prompt_synthesizer");
11
- function synthesizeDiffusionPromptTool(imagePath, userIntent = "", targetModel = "gpt_image") {
11
+ function synthesizeDiffusionPromptTool(imagePath, userIntent = "", targetModel = "universal") {
12
12
  try {
13
13
  const result = (0, prompt_synthesizer_1.synthesizeDiffusionPromptImpl)(imagePath, userIntent, targetModel);
14
14
  const hash = crypto_1.default