Spaces:
Running
Running
made changes to the code
Browse files- .claude/settings.local.json +2 -1
- app/api/process/route.ts +379 -47
- app/nodes.tsx +186 -27
- app/page.tsx +68 -9
.claude/settings.local.json
CHANGED
|
@@ -15,7 +15,8 @@
|
|
| 15 |
"Read(//Users/reubenfernandes/Desktop/**)",
|
| 16 |
"mcp__puppeteer__puppeteer_click",
|
| 17 |
"mcp__browser-tools__getConsoleErrors",
|
| 18 |
-
"mcp__sequential-thinking__sequentialthinking"
|
|
|
|
| 19 |
],
|
| 20 |
"deny": [],
|
| 21 |
"ask": []
|
|
|
|
| 15 |
"Read(//Users/reubenfernandes/Desktop/**)",
|
| 16 |
"mcp__puppeteer__puppeteer_click",
|
| 17 |
"mcp__browser-tools__getConsoleErrors",
|
| 18 |
+
"mcp__sequential-thinking__sequentialthinking",
|
| 19 |
+
"WebFetch(domain:developers.googleblog.com)"
|
| 20 |
],
|
| 21 |
"deny": [],
|
| 22 |
"ask": []
|
app/api/process/route.ts
CHANGED
|
@@ -267,21 +267,169 @@ The result should look like all subjects were photographed together in the same
|
|
| 267 |
const prompts: string[] = [];
|
| 268 |
const params = body.params || {};
|
| 269 |
|
|
|
|
|
|
|
|
|
|
| 270 |
// We'll collect additional inline image parts (references)
|
| 271 |
const referenceParts: { inlineData: { mimeType: string; data: string } }[] = [];
|
|
|
|
| 272 |
|
| 273 |
// Background modifications
|
| 274 |
if (params.backgroundType) {
|
| 275 |
const bgType = params.backgroundType;
|
|
|
|
|
|
|
| 276 |
if (bgType === "color") {
|
| 277 |
-
prompts.push(`Change the background to a solid ${params.backgroundColor || "white"} background.`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
} else if (bgType === "image") {
|
| 279 |
prompts.push(`Change the background to ${params.backgroundImage || "a beautiful beach scene"}.`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
} else if (bgType === "upload" && params.customBackgroundImage) {
|
| 281 |
prompts.push(`Replace the background using the provided custom background reference image (attached below). Ensure perspective and lighting match.`);
|
| 282 |
const bgRef = await toInlineDataFromAny(params.customBackgroundImage);
|
| 283 |
if (bgRef) referenceParts.push({ inlineData: bgRef });
|
| 284 |
-
|
|
|
|
| 285 |
prompts.push(params.customPrompt);
|
| 286 |
}
|
| 287 |
}
|
|
@@ -313,10 +461,11 @@ The result should look like all subjects were photographed together in the same
|
|
| 313 |
|
| 314 |
// Style application
|
| 315 |
if (params.stylePreset) {
|
|
|
|
| 316 |
const strength = params.styleStrength || 50;
|
| 317 |
const styleMap: { [key: string]: string } = {
|
| 318 |
"90s-anime": "Convert the image to 90's anime art style with classic anime features",
|
| 319 |
-
"
|
| 320 |
"mha": "Transform the image into My Hero Academia anime style with modern crisp lines, vibrant colors, dynamic character design, and heroic aesthetics typical of the series",
|
| 321 |
"dbz": "Apply Dragon Ball Z anime style with sharp angular features, spiky hair, intense expressions, bold outlines, high contrast shading, and dramatic action-oriented aesthetics",
|
| 322 |
"ukiyo-e": "Render in traditional Japanese Ukiyo-e woodblock print style with flat colors, bold outlines, stylized waves and clouds, traditional Japanese artistic elements",
|
|
@@ -333,7 +482,11 @@ The result should look like all subjects were photographed together in the same
|
|
| 333 |
|
| 334 |
const styleDescription = styleMap[params.stylePreset];
|
| 335 |
if (styleDescription) {
|
|
|
|
| 336 |
prompts.push(`${styleDescription}. Apply this style transformation at ${strength}% intensity while preserving the core subject matter.`);
|
|
|
|
|
|
|
|
|
|
| 337 |
}
|
| 338 |
}
|
| 339 |
|
|
@@ -342,28 +495,135 @@ The result should look like all subjects were photographed together in the same
|
|
| 342 |
prompts.push(params.editPrompt);
|
| 343 |
}
|
| 344 |
|
| 345 |
-
// Camera settings
|
| 346 |
if (params.focalLength || params.aperture || params.shutterSpeed || params.whiteBalance || params.angle ||
|
| 347 |
-
params.iso || params.filmStyle || params.lighting || params.bokeh || params.composition) {
|
| 348 |
const cameraSettings: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
if (params.focalLength) {
|
| 350 |
if (params.focalLength === "8mm") {
|
| 351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
} else {
|
| 353 |
-
|
| 354 |
}
|
| 355 |
}
|
| 356 |
-
|
| 357 |
-
if (params.
|
| 358 |
-
|
| 359 |
-
if (params.
|
| 360 |
-
|
| 361 |
-
if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
|
| 362 |
-
if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
|
| 363 |
-
if (params.filmStyle == "RAW") cameraSettings.push(`Convert the image to RAW image`); else cameraSettings.push(`Film Style: ${params.filmStyle}`);
|
| 364 |
-
if (cameraSettings.length > 0) {
|
| 365 |
-
prompts.push(`Apply professional photography settings: ${cameraSettings.join(", ")}`);
|
| 366 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
}
|
| 368 |
|
| 369 |
// Age transformation
|
|
@@ -371,32 +631,16 @@ The result should look like all subjects were photographed together in the same
|
|
| 371 |
prompts.push(`Transform the person to look exactly ${params.targetAge} years old with age-appropriate features.`);
|
| 372 |
}
|
| 373 |
|
| 374 |
-
//
|
| 375 |
-
if (params.
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
try {
|
| 379 |
-
const lightingRef = await toInlineDataFromAny(params.lightingImage);
|
| 380 |
-
if (lightingRef) {
|
| 381 |
-
referenceParts.push({ inlineData: lightingRef });
|
| 382 |
-
}
|
| 383 |
-
} catch (error) {
|
| 384 |
-
console.error('[API] Error processing lighting image:', error);
|
| 385 |
-
}
|
| 386 |
}
|
| 387 |
|
| 388 |
// Pose modifications
|
| 389 |
-
if (params.
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
try {
|
| 393 |
-
const poseRef = await toInlineDataFromAny(params.poseImage);
|
| 394 |
-
if (poseRef) {
|
| 395 |
-
referenceParts.push({ inlineData: poseRef });
|
| 396 |
-
}
|
| 397 |
-
} catch (error) {
|
| 398 |
-
console.error('[API] Error processing pose image:', error);
|
| 399 |
-
}
|
| 400 |
}
|
| 401 |
|
| 402 |
// Face modifications
|
|
@@ -426,6 +670,13 @@ The result should look like all subjects were photographed together in the same
|
|
| 426 |
prompt = body.prompt + "\n\n" + prompt;
|
| 427 |
}
|
| 428 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
// Generate with Gemini
|
| 430 |
const parts = [
|
| 431 |
{ text: prompt },
|
|
@@ -435,23 +686,83 @@ The result should look like all subjects were photographed together in the same
|
|
| 435 |
...referenceParts,
|
| 436 |
];
|
| 437 |
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
|
|
|
|
|
|
|
| 443 |
const outParts = (response as any)?.candidates?.[0]?.content?.parts ?? [];
|
| 444 |
const images: string[] = [];
|
|
|
|
| 445 |
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
if (p?.inlineData?.data) {
|
| 448 |
images.push(`data:image/png;base64,${p.inlineData.data}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
}
|
| 450 |
}
|
| 451 |
|
| 452 |
if (!images.length) {
|
|
|
|
| 453 |
return NextResponse.json(
|
| 454 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
{ status: 500 }
|
| 456 |
);
|
| 457 |
}
|
|
@@ -460,15 +771,36 @@ The result should look like all subjects were photographed together in the same
|
|
| 460 |
} catch (err: any) {
|
| 461 |
console.error("/api/process error:", err);
|
| 462 |
console.error("Error stack:", err?.stack);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
// Provide more specific error messages
|
| 465 |
-
if (err?.message?.includes('payload size')) {
|
| 466 |
return NextResponse.json(
|
| 467 |
{ error: "Image data too large. Please use smaller images or reduce image quality." },
|
| 468 |
{ status: 413 }
|
| 469 |
);
|
| 470 |
}
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
if (err?.message?.includes('JSON')) {
|
| 473 |
return NextResponse.json(
|
| 474 |
{ error: "Invalid data format. Please ensure images are properly encoded." },
|
|
|
|
| 267 |
const prompts: string[] = [];
|
| 268 |
const params = body.params || {};
|
| 269 |
|
| 270 |
+
// Debug: Log all received parameters
|
| 271 |
+
console.log(`[API] All received parameters:`, JSON.stringify(params, null, 2));
|
| 272 |
+
|
| 273 |
// We'll collect additional inline image parts (references)
|
| 274 |
const referenceParts: { inlineData: { mimeType: string; data: string } }[] = [];
|
| 275 |
+
let imageCounter = 2; // Start at 2 since image 1 is the primary input
|
| 276 |
|
| 277 |
// Background modifications
|
| 278 |
if (params.backgroundType) {
|
| 279 |
const bgType = params.backgroundType;
|
| 280 |
+
console.log(`[API] Processing background: type=${bgType}`);
|
| 281 |
+
|
| 282 |
if (bgType === "color") {
|
| 283 |
+
prompts.push(`Change the background to a solid ${params.backgroundColor || "white"} background with smooth, even color coverage.`);
|
| 284 |
+
|
| 285 |
+
} else if (bgType === "gradient") {
|
| 286 |
+
const direction = params.gradientDirection || "to right";
|
| 287 |
+
const startColor = params.gradientStartColor || "#ff6b6b";
|
| 288 |
+
const endColor = params.gradientEndColor || "#4ecdc4";
|
| 289 |
+
|
| 290 |
+
if (direction === "radial") {
|
| 291 |
+
prompts.push(`Replace the background with a radial gradient that starts with ${startColor} in the center and transitions smoothly to ${endColor} at the edges, creating a circular gradient effect.`);
|
| 292 |
+
} else {
|
| 293 |
+
prompts.push(`Replace the background with a linear gradient flowing ${direction}, starting with ${startColor} and smoothly transitioning to ${endColor}.`);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
} else if (bgType === "image") {
|
| 297 |
prompts.push(`Change the background to ${params.backgroundImage || "a beautiful beach scene"}.`);
|
| 298 |
+
|
| 299 |
+
} else if (bgType === "city") {
|
| 300 |
+
const sceneType = params.citySceneType || "busy_street";
|
| 301 |
+
const timeOfDay = params.cityTimeOfDay || "daytime";
|
| 302 |
+
|
| 303 |
+
let cityDescription = "";
|
| 304 |
+
|
| 305 |
+
switch (sceneType) {
|
| 306 |
+
case "busy_street":
|
| 307 |
+
cityDescription = "a realistic busy city street with people walking at various distances around the main character. Include pedestrians in business attire, casual clothing, carrying bags and phones - some walking close by (appearing similar size to main character), others further in the background (appearing smaller due to distance). Show urban storefronts, traffic lights, street signs, and parked cars with authentic city atmosphere and proper depth perception";
|
| 308 |
+
break;
|
| 309 |
+
case "tokyo_shibuya":
|
| 310 |
+
cityDescription = "the iconic Tokyo Shibuya Crossing with people walking at various distances around the main character. Include people close by (similar scale to main character) and others further away (smaller due to distance), Japanese signage, neon advertisements, the famous scramble crossing zebra stripes, people in typical Tokyo fashion, some wearing masks, carrying colorful umbrellas. Show the massive LED screens, buildings towering above, and create proper depth with people at different distances creating natural perspective";
|
| 311 |
+
break;
|
| 312 |
+
case "tokyo_subway":
|
| 313 |
+
cityDescription = "a realistic Tokyo subway environment with commuters at various distances from the main character. Include people nearby (similar scale) and others further down corridors (smaller due to perspective), authentic Japanese subway tile walls, directional signage in Japanese, the distinctive Tokyo Metro design aesthetic, and proper depth showing the underground transit system's scale and architecture";
|
| 314 |
+
break;
|
| 315 |
+
case "times_square":
|
| 316 |
+
cityDescription = "Times Square NYC with bright LED billboards, street performers, tourists, and New Yorkers walking closely around the main character. Include authentic yellow taxi cabs, hot dog vendors, people taking selfies, Broadway theater marquees, the famous red steps, TKTS booth, and the overwhelming sensory experience of NYC's most famous intersection";
|
| 317 |
+
break;
|
| 318 |
+
case "downtown_skyline":
|
| 319 |
+
cityDescription = "a downtown city skyline with tall buildings, glass towers, and urban architecture in the background while people in business attire walk nearby on the sidewalk";
|
| 320 |
+
break;
|
| 321 |
+
case "urban_crosswalk":
|
| 322 |
+
cityDescription = "an urban crosswalk intersection with pedestrians of diverse backgrounds crossing around the main character, traffic lights, crosswalk signals, city buses, and the natural flow of city foot traffic";
|
| 323 |
+
break;
|
| 324 |
+
case "shopping_district":
|
| 325 |
+
cityDescription = "a bustling shopping district with people carrying shopping bags walking near the main character, storefront window displays, outdoor cafes, street vendors, and the lively atmosphere of commercial city life";
|
| 326 |
+
break;
|
| 327 |
+
case "city_park":
|
| 328 |
+
cityDescription = "a city park with people jogging, walking dogs, and families enjoying activities around the main character, with urban skyscrapers visible in the background through the trees";
|
| 329 |
+
break;
|
| 330 |
+
case "rooftop_view":
|
| 331 |
+
cityDescription = "a rooftop terrace with people socializing around the main character, overlooking a sprawling city skyline with twinkling lights and urban architecture stretching to the horizon";
|
| 332 |
+
break;
|
| 333 |
+
case "blade_runner_street":
|
| 334 |
+
cityDescription = "a cinematic Blade Runner-inspired street scene with neon-soaked alleyways, people in futuristic clothing walking through steam and rain around the main character. Include holographic advertisements, flying vehicles in the distance, Asian-influenced signage, dark atmospheric lighting with cyan and magenta neon reflections on wet pavement, and the dystopian cyberpunk aesthetic of the iconic film";
|
| 335 |
+
break;
|
| 336 |
+
case "matrix_alley":
|
| 337 |
+
cityDescription = "a Matrix-inspired urban alley with people in dark clothing and sunglasses walking purposefully around the main character. Include the distinctive green-tinted lighting, concrete brutalist architecture, fire escapes, urban decay, shadowy doorways, and the cold, digital atmosphere of the Matrix films with realistic but slightly stylized cinematography";
|
| 338 |
+
break;
|
| 339 |
+
default:
|
| 340 |
+
cityDescription = "a dynamic city environment with people walking naturally around the main character in an authentic urban setting";
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
let timeDescription = "";
|
| 344 |
+
switch (timeOfDay) {
|
| 345 |
+
case "golden_hour":
|
| 346 |
+
timeDescription = " during golden hour with warm, glowing sunlight";
|
| 347 |
+
break;
|
| 348 |
+
case "daytime":
|
| 349 |
+
timeDescription = " during bright daytime with clear lighting";
|
| 350 |
+
break;
|
| 351 |
+
case "blue_hour":
|
| 352 |
+
timeDescription = " during blue hour with twilight atmosphere";
|
| 353 |
+
break;
|
| 354 |
+
case "night":
|
| 355 |
+
timeDescription = " at night with city lights, illuminated windows, and neon glow";
|
| 356 |
+
break;
|
| 357 |
+
case "dawn":
|
| 358 |
+
timeDescription = " at dawn with soft morning light";
|
| 359 |
+
break;
|
| 360 |
+
case "overcast":
|
| 361 |
+
timeDescription = " on an overcast day with diffused lighting";
|
| 362 |
+
break;
|
| 363 |
+
default:
|
| 364 |
+
timeDescription = "";
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
prompts.push(`Replace the background with ${cityDescription}${timeDescription}. CRITICAL SCALE REQUIREMENTS: Keep the main character at their EXACT original size and position - do NOT make them smaller or change their scale. The background people should be appropriately sized relative to their distance from the camera, with people closer to the camera appearing larger and people further away appearing smaller, but the main character must maintain their original proportions. Ensure the main character appears naturally integrated into the scene with proper lighting, shadows, and perspective that matches the environment.`);
|
| 368 |
+
|
| 369 |
+
} else if (bgType === "photostudio") {
|
| 370 |
+
const setup = params.studioSetup || "white_seamless";
|
| 371 |
+
const lighting = params.studioLighting || "key_fill";
|
| 372 |
+
const faceCamera = params.faceCamera || false;
|
| 373 |
+
|
| 374 |
+
let setupDescription = "";
|
| 375 |
+
switch (setup) {
|
| 376 |
+
case "white_seamless":
|
| 377 |
+
setupDescription = "a professional white seamless paper backdrop";
|
| 378 |
+
break;
|
| 379 |
+
case "black_seamless":
|
| 380 |
+
setupDescription = "a professional black seamless paper backdrop";
|
| 381 |
+
break;
|
| 382 |
+
case "grey_seamless":
|
| 383 |
+
setupDescription = "a professional grey seamless paper backdrop";
|
| 384 |
+
break;
|
| 385 |
+
case "colored_seamless":
|
| 386 |
+
const bgColor = params.studioBackgroundColor || "#ffffff";
|
| 387 |
+
setupDescription = `a professional seamless paper backdrop in ${bgColor}`;
|
| 388 |
+
break;
|
| 389 |
+
case "textured_backdrop":
|
| 390 |
+
setupDescription = "a professional textured photography backdrop";
|
| 391 |
+
break;
|
| 392 |
+
case "infinity_cove":
|
| 393 |
+
setupDescription = "a professional infinity cove studio setup with curved backdrop";
|
| 394 |
+
break;
|
| 395 |
+
default:
|
| 396 |
+
setupDescription = "a professional studio backdrop";
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
let lightingDescription = "";
|
| 400 |
+
switch (lighting) {
|
| 401 |
+
case "key_fill":
|
| 402 |
+
lightingDescription = "key and fill lighting for balanced illumination";
|
| 403 |
+
break;
|
| 404 |
+
case "three_point":
|
| 405 |
+
lightingDescription = "three-point lighting with key, fill, and rim lights";
|
| 406 |
+
break;
|
| 407 |
+
case "beauty_lighting":
|
| 408 |
+
lightingDescription = "beauty lighting setup with soft, flattering illumination";
|
| 409 |
+
break;
|
| 410 |
+
case "dramatic_lighting":
|
| 411 |
+
lightingDescription = "dramatic single-light setup with strong shadows";
|
| 412 |
+
break;
|
| 413 |
+
case "soft_lighting":
|
| 414 |
+
lightingDescription = "soft, diffused lighting for gentle illumination";
|
| 415 |
+
break;
|
| 416 |
+
case "hard_lighting":
|
| 417 |
+
lightingDescription = "hard, directional lighting for sharp shadows and contrast";
|
| 418 |
+
break;
|
| 419 |
+
default:
|
| 420 |
+
lightingDescription = "professional studio lighting";
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
let positioningInstruction = faceCamera ? " Position the person to face directly toward the camera with confident posture." : "";
|
| 424 |
+
|
| 425 |
+
prompts.push(`Place the person in a professional photo studio with ${setupDescription} and ${lightingDescription}. Create a clean, professional portrait setup with proper studio atmosphere.${positioningInstruction}`);
|
| 426 |
+
|
| 427 |
} else if (bgType === "upload" && params.customBackgroundImage) {
|
| 428 |
prompts.push(`Replace the background using the provided custom background reference image (attached below). Ensure perspective and lighting match.`);
|
| 429 |
const bgRef = await toInlineDataFromAny(params.customBackgroundImage);
|
| 430 |
if (bgRef) referenceParts.push({ inlineData: bgRef });
|
| 431 |
+
|
| 432 |
+
} else if (bgType === "custom" && params.customPrompt) {
|
| 433 |
prompts.push(params.customPrompt);
|
| 434 |
}
|
| 435 |
}
|
|
|
|
| 461 |
|
| 462 |
// Style application
|
| 463 |
if (params.stylePreset) {
|
| 464 |
+
console.log(`[API] Processing style node: stylePreset=${params.stylePreset}, styleStrength=${params.styleStrength}`);
|
| 465 |
const strength = params.styleStrength || 50;
|
| 466 |
const styleMap: { [key: string]: string } = {
|
| 467 |
"90s-anime": "Convert the image to 90's anime art style with classic anime features",
|
| 468 |
+
"Ghibli": "Apply Studio Ghibli style with vibrant colors, detailed character design, and fantasy elements typical of Studio Ghibli movies",
|
| 469 |
"mha": "Transform the image into My Hero Academia anime style with modern crisp lines, vibrant colors, dynamic character design, and heroic aesthetics typical of the series",
|
| 470 |
"dbz": "Apply Dragon Ball Z anime style with sharp angular features, spiky hair, intense expressions, bold outlines, high contrast shading, and dramatic action-oriented aesthetics",
|
| 471 |
"ukiyo-e": "Render in traditional Japanese Ukiyo-e woodblock print style with flat colors, bold outlines, stylized waves and clouds, traditional Japanese artistic elements",
|
|
|
|
| 482 |
|
| 483 |
const styleDescription = styleMap[params.stylePreset];
|
| 484 |
if (styleDescription) {
|
| 485 |
+
console.log(`[API] Style found: ${params.stylePreset} -> ${styleDescription}`);
|
| 486 |
prompts.push(`${styleDescription}. Apply this style transformation at ${strength}% intensity while preserving the core subject matter.`);
|
| 487 |
+
} else {
|
| 488 |
+
console.error(`[API] Style not found in styleMap: ${params.stylePreset}`);
|
| 489 |
+
console.log(`[API] Available styles:`, Object.keys(styleMap));
|
| 490 |
}
|
| 491 |
}
|
| 492 |
|
|
|
|
| 495 |
prompts.push(params.editPrompt);
|
| 496 |
}
|
| 497 |
|
| 498 |
+
// Camera settings - Enhanced for Gemini 2.5 Flash Image
|
| 499 |
if (params.focalLength || params.aperture || params.shutterSpeed || params.whiteBalance || params.angle ||
|
| 500 |
+
params.iso || params.filmStyle || params.lighting || params.bokeh || params.composition || params.motionBlur) {
|
| 501 |
const cameraSettings: string[] = [];
|
| 502 |
+
|
| 503 |
+
// Build cinematic camera prompt for professional, movie-like results
|
| 504 |
+
let cameraPrompt = "CINEMATIC CAMERA TRANSFORMATION: Transform this image into a professional, cinematic photograph with movie-quality production values";
|
| 505 |
+
|
| 506 |
if (params.focalLength) {
|
| 507 |
if (params.focalLength === "8mm") {
|
| 508 |
+
cameraPrompt += " shot with an ultra-wide 8mm fisheye lens creating dramatic barrel distortion, immersive perspective, and cinematic edge curvature typical of action sequences";
|
| 509 |
+
} else if (params.focalLength === "14mm") {
|
| 510 |
+
cameraPrompt += " captured with a 14mm ultra-wide angle lens for sweeping cinematic vistas and dramatic environmental context";
|
| 511 |
+
} else if (params.focalLength === "24mm") {
|
| 512 |
+
cameraPrompt += " shot with a 24mm wide-angle cinema lens for establishing shots with expansive field of view and slight perspective enhancement";
|
| 513 |
+
} else if (params.focalLength === "35mm") {
|
| 514 |
+
cameraPrompt += " filmed with a 35mm lens providing natural cinematic perspective, the gold standard for narrative storytelling";
|
| 515 |
+
} else if (params.focalLength === "50mm") {
|
| 516 |
+
cameraPrompt += " captured with a 50mm cinema lens for authentic human vision perspective and natural depth rendering";
|
| 517 |
+
} else if (params.focalLength === "85mm") {
|
| 518 |
+
cameraPrompt += " shot with an 85mm portrait cinema lens for intimate character close-ups with beautiful subject isolation and compressed backgrounds";
|
| 519 |
+
} else if (params.focalLength === "100mm") {
|
| 520 |
+
cameraPrompt += " filmed with a 100mm telephoto lens for dramatic compression and cinematic subject isolation";
|
| 521 |
+
} else if (params.focalLength === "135mm") {
|
| 522 |
+
cameraPrompt += " captured with a 135mm telephoto cinema lens for extreme compression and dreamlike background separation";
|
| 523 |
+
} else {
|
| 524 |
+
cameraPrompt += ` shot with professional ${params.focalLength} cinema glass`;
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
if (params.aperture) {
|
| 529 |
+
if (params.aperture === "f/1.2") {
|
| 530 |
+
cameraPrompt += `, shot wide open at f/1.2 for extreme shallow depth of field, ethereal bokeh, and cinematic subject isolation with dreamy background blur`;
|
| 531 |
+
} else if (params.aperture === "f/1.4") {
|
| 532 |
+
cameraPrompt += `, captured at f/1.4 for beautiful shallow depth of field, creating that signature cinematic look with smooth background separation`;
|
| 533 |
+
} else if (params.aperture === "f/2.8") {
|
| 534 |
+
cameraPrompt += `, shot at f/2.8 for controlled depth of field, maintaining subject sharpness while creating pleasing background blur`;
|
| 535 |
+
} else if (params.aperture === "f/4") {
|
| 536 |
+
cameraPrompt += `, filmed at f/4 for balanced depth of field, keeping key subjects sharp while maintaining some background separation`;
|
| 537 |
+
} else if (params.aperture === "f/5.6") {
|
| 538 |
+
cameraPrompt += `, captured at f/5.6 for extended depth of field while maintaining cinematic quality and professional sharpness`;
|
| 539 |
+
} else if (params.aperture === "f/8" || params.aperture === "f/11") {
|
| 540 |
+
cameraPrompt += `, shot at ${params.aperture} for deep focus cinematography with tack-sharp details throughout the entire frame`;
|
| 541 |
+
} else {
|
| 542 |
+
cameraPrompt += `, professionally exposed at ${params.aperture}`;
|
| 543 |
+
}
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
if (params.iso) {
|
| 547 |
+
if (params.iso === "ISO 100") {
|
| 548 |
+
cameraPrompt += ", shot at ISO 100 for pristine image quality, zero noise, and maximum dynamic range typical of high-end cinema cameras";
|
| 549 |
+
} else if (params.iso === "ISO 200") {
|
| 550 |
+
cameraPrompt += ", captured at ISO 200 for clean shadows and optimal color reproduction with professional cinema camera characteristics";
|
| 551 |
+
} else if (params.iso === "ISO 400") {
|
| 552 |
+
cameraPrompt += ", filmed at ISO 400 for balanced exposure with minimal noise, the sweet spot for most cinematic scenarios";
|
| 553 |
+
} else if (params.iso === "ISO 800") {
|
| 554 |
+
cameraPrompt += ", shot at ISO 800 creating subtle film grain texture that adds cinematic character and organic feel";
|
| 555 |
+
} else if (params.iso === "ISO 1600") {
|
| 556 |
+
cameraPrompt += ", captured at ISO 1600 with controlled grain for dramatic low-light cinematography and moody atmosphere";
|
| 557 |
+
} else if (params.iso === "ISO 3200") {
|
| 558 |
+
cameraPrompt += ", filmed at ISO 3200 with artistic grain structure for gritty, realistic cinema aesthetics";
|
| 559 |
+
} else {
|
| 560 |
+
cameraPrompt += `, shot at ${params.iso} with appropriate noise characteristics`;
|
| 561 |
+
}
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
if (params.lighting) {
|
| 565 |
+
if (params.lighting === "Golden Hour") {
|
| 566 |
+
cameraPrompt += ", cinematically lit during golden hour with warm, directional sunlight creating magical rim lighting, long shadows, and that coveted cinematic glow";
|
| 567 |
+
} else if (params.lighting === "Blue Hour") {
|
| 568 |
+
cameraPrompt += ", captured during blue hour with soft, even twilight illumination and cool color temperature for moody cinematic atmosphere";
|
| 569 |
+
} else if (params.lighting === "Studio") {
|
| 570 |
+
cameraPrompt += ", professionally lit with multi-point studio lighting setup featuring key light, fill light, and rim light for commercial cinema quality";
|
| 571 |
+
} else if (params.lighting === "Natural") {
|
| 572 |
+
cameraPrompt += ", naturally lit with soft, diffused daylight providing even illumination and organic shadow patterns";
|
| 573 |
+
} else if (params.lighting === "Dramatic") {
|
| 574 |
+
cameraPrompt += ", dramatically lit with high-contrast lighting creating strong shadows and highlights for cinematic tension";
|
| 575 |
+
} else {
|
| 576 |
+
cameraPrompt += `, professionally lit with ${params.lighting} lighting setup`;
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
if (params.bokeh) {
|
| 581 |
+
if (params.bokeh === "Smooth Bokeh") {
|
| 582 |
+
cameraPrompt += ", featuring silky smooth bokeh with perfectly circular out-of-focus highlights and creamy background transitions";
|
| 583 |
+
} else if (params.bokeh === "Swirly Bokeh") {
|
| 584 |
+
cameraPrompt += ", featuring artistic swirly bokeh with spiral-like background blur patterns for unique visual character";
|
| 585 |
+
} else if (params.bokeh === "Hexagonal Bokeh") {
|
| 586 |
+
cameraPrompt += ", featuring hexagonal bokeh with geometric six-sided highlight shapes typical of cinema lenses";
|
| 587 |
+
} else {
|
| 588 |
+
cameraPrompt += `, featuring ${params.bokeh} quality bokeh rendering in out-of-focus areas`;
|
| 589 |
+
}
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
if (params.motionBlur) {
|
| 593 |
+
if (params.motionBlur === "Light Motion Blur") {
|
| 594 |
+
cameraPrompt += ", with subtle motion blur suggesting gentle movement and adding cinematic flow to the image";
|
| 595 |
+
} else if (params.motionBlur === "Medium Motion Blur") {
|
| 596 |
+
cameraPrompt += ", with moderate motion blur creating dynamic energy and sense of movement typical of action cinematography";
|
| 597 |
+
} else if (params.motionBlur === "Heavy Motion Blur") {
|
| 598 |
+
cameraPrompt += ", with pronounced motion blur creating dramatic movement streaks and high-energy cinematic action";
|
| 599 |
+
} else if (params.motionBlur === "Radial Blur") {
|
| 600 |
+
cameraPrompt += ", with radial motion blur emanating from the center, creating explosive zoom-like movement and dramatic focus pull";
|
| 601 |
+
} else if (params.motionBlur === "Zoom Blur") {
|
| 602 |
+
cameraPrompt += ", with zoom blur effect creating dramatic speed lines and kinetic energy radiating outward from the subject";
|
| 603 |
+
} else {
|
| 604 |
+
cameraPrompt += `, with ${params.motionBlur} motion effect`;
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
if (params.angle) {
|
| 609 |
+
if (params.angle === "Low Angle") {
|
| 610 |
+
cameraPrompt += ", shot from a low-angle perspective looking upward for dramatic impact";
|
| 611 |
+
} else if (params.angle === "Bird's Eye") {
|
| 612 |
+
cameraPrompt += ", captured from a bird's eye view directly overhead";
|
| 613 |
} else {
|
| 614 |
+
cameraPrompt += `, ${params.angle} camera angle`;
|
| 615 |
}
|
| 616 |
}
|
| 617 |
+
|
| 618 |
+
if (params.filmStyle && params.filmStyle !== "RAW") {
|
| 619 |
+
cameraPrompt += `, processed with ${params.filmStyle} film aesthetic and color grading`;
|
| 620 |
+
} else if (params.filmStyle === "RAW") {
|
| 621 |
+
cameraPrompt += ", with natural RAW processing maintaining realistic colors and contrast";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
}
|
| 623 |
+
|
| 624 |
+
cameraPrompt += ". Maintain photorealistic quality with authentic camera characteristics, natural lighting, and professional composition.";
|
| 625 |
+
|
| 626 |
+
prompts.push(cameraPrompt);
|
| 627 |
}
|
| 628 |
|
| 629 |
// Age transformation
|
|
|
|
| 631 |
prompts.push(`Transform the person to look exactly ${params.targetAge} years old with age-appropriate features.`);
|
| 632 |
}
|
| 633 |
|
| 634 |
+
// Lighting effects
|
| 635 |
+
if (params.lightingPrompt && params.selectedLighting) {
|
| 636 |
+
console.log(`[API] Processing lighting node: selectedLighting=${params.selectedLighting}, lightingPrompt exists=${!!params.lightingPrompt}`);
|
| 637 |
+
prompts.push(`IMPORTANT: Completely transform the lighting on this person to match this exact description: ${params.lightingPrompt}. The lighting change should be dramatic and clearly visible. Keep their face, clothes, pose, and background exactly the same, but make the lighting transformation very obvious.`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
}
|
| 639 |
|
| 640 |
// Pose modifications
|
| 641 |
+
if (params.posePrompt && params.selectedPose) {
|
| 642 |
+
console.log(`[API] Processing pose node: selectedPose=${params.selectedPose}, posePrompt exists=${!!params.posePrompt}`);
|
| 643 |
+
prompts.push(`IMPORTANT: Completely change the person's body pose to match this exact description: ${params.posePrompt}. The pose change should be dramatic and clearly visible. Keep their face, clothes, and background exactly the same, but make the pose transformation very obvious.`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
}
|
| 645 |
|
| 646 |
// Face modifications
|
|
|
|
| 670 |
prompt = body.prompt + "\n\n" + prompt;
|
| 671 |
}
|
| 672 |
|
| 673 |
+
// Debug: Log the final combined prompt and parts structure
|
| 674 |
+
console.log(`[API] Final combined prompt: ${prompt}`);
|
| 675 |
+
console.log(`[API] Total prompts collected: ${prompts.length}`);
|
| 676 |
+
console.log(`[API] Individual prompts:`, prompts);
|
| 677 |
+
console.log(`[API] Reference images count: ${referenceParts.length}`);
|
| 678 |
+
console.log(`[API] Total parts being sent to AI: ${1 + 1 + referenceParts.length} (prompt + input image + ${referenceParts.length} reference images)`);
|
| 679 |
+
|
| 680 |
// Generate with Gemini
|
| 681 |
const parts = [
|
| 682 |
{ text: prompt },
|
|
|
|
| 686 |
...referenceParts,
|
| 687 |
];
|
| 688 |
|
| 689 |
+
console.log(`[API] Sending request to Gemini with ${parts.length} parts (prompt + ${referenceParts.length + 1} images)`);
|
| 690 |
+
|
| 691 |
+
let response;
|
| 692 |
+
try {
|
| 693 |
+
response = await ai.models.generateContent({
|
| 694 |
+
model: "gemini-2.5-flash-image-preview",
|
| 695 |
+
contents: parts,
|
| 696 |
+
});
|
| 697 |
+
console.log('[API] Gemini response received successfully');
|
| 698 |
+
} catch (geminiError: any) {
|
| 699 |
+
console.error('[API] Gemini API error:', geminiError);
|
| 700 |
+
console.error('[API] Gemini error details:', {
|
| 701 |
+
message: geminiError.message,
|
| 702 |
+
status: geminiError.status,
|
| 703 |
+
code: geminiError.code
|
| 704 |
+
});
|
| 705 |
+
|
| 706 |
+
if (geminiError.message?.includes('safety')) {
|
| 707 |
+
return NextResponse.json(
|
| 708 |
+
{ error: "Content was blocked by safety filters. Try using different images or prompts." },
|
| 709 |
+
{ status: 400 }
|
| 710 |
+
);
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
if (geminiError.message?.includes('quota') || geminiError.message?.includes('limit')) {
|
| 714 |
+
return NextResponse.json(
|
| 715 |
+
{ error: "API quota exceeded. Please check your Gemini API usage limits." },
|
| 716 |
+
{ status: 429 }
|
| 717 |
+
);
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
return NextResponse.json(
|
| 721 |
+
{ error: `Gemini API error: ${geminiError.message || 'Unknown error'}` },
|
| 722 |
+
{ status: 500 }
|
| 723 |
+
);
|
| 724 |
+
}
|
| 725 |
|
| 726 |
+
console.log('[API] Raw Gemini response structure:', JSON.stringify(response, null, 2));
|
| 727 |
+
|
| 728 |
const outParts = (response as any)?.candidates?.[0]?.content?.parts ?? [];
|
| 729 |
const images: string[] = [];
|
| 730 |
+
const texts: string[] = [];
|
| 731 |
|
| 732 |
+
console.log(`[API] Response parts found: ${outParts.length}`);
|
| 733 |
+
|
| 734 |
+
for (let i = 0; i < outParts.length; i++) {
|
| 735 |
+
const p = outParts[i];
|
| 736 |
+
console.log(`[API] Part ${i}:`, {
|
| 737 |
+
hasInlineData: !!p?.inlineData,
|
| 738 |
+
hasText: !!p?.text,
|
| 739 |
+
inlineDataType: p?.inlineData?.mimeType,
|
| 740 |
+
textLength: p?.text?.length
|
| 741 |
+
});
|
| 742 |
+
|
| 743 |
if (p?.inlineData?.data) {
|
| 744 |
images.push(`data:image/png;base64,${p.inlineData.data}`);
|
| 745 |
+
console.log(`[API] Found image data, length: ${p.inlineData.data.length}`);
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
if (p?.text) {
|
| 749 |
+
texts.push(p.text);
|
| 750 |
+
console.log(`[API] Found text response: ${p.text.substring(0, 200)}...`);
|
| 751 |
}
|
| 752 |
}
|
| 753 |
|
| 754 |
if (!images.length) {
|
| 755 |
+
console.error('[API] No images generated by Gemini. Text responses:', texts);
|
| 756 |
return NextResponse.json(
|
| 757 |
+
{
|
| 758 |
+
error: "No image generated. Try adjusting your parameters.",
|
| 759 |
+
textResponse: texts.join('\n'),
|
| 760 |
+
debugInfo: {
|
| 761 |
+
partsCount: outParts.length,
|
| 762 |
+
candidatesCount: (response as any)?.candidates?.length || 0,
|
| 763 |
+
hasResponse: !!response
|
| 764 |
+
}
|
| 765 |
+
},
|
| 766 |
{ status: 500 }
|
| 767 |
);
|
| 768 |
}
|
|
|
|
| 771 |
} catch (err: any) {
|
| 772 |
console.error("/api/process error:", err);
|
| 773 |
console.error("Error stack:", err?.stack);
|
| 774 |
+
console.error("Error details:", {
|
| 775 |
+
name: err?.name,
|
| 776 |
+
message: err?.message,
|
| 777 |
+
code: err?.code,
|
| 778 |
+
status: err?.status,
|
| 779 |
+
details: err?.details
|
| 780 |
+
});
|
| 781 |
|
| 782 |
// Provide more specific error messages
|
| 783 |
+
if (err?.message?.includes('payload size') || err?.code === 413) {
|
| 784 |
return NextResponse.json(
|
| 785 |
{ error: "Image data too large. Please use smaller images or reduce image quality." },
|
| 786 |
{ status: 413 }
|
| 787 |
);
|
| 788 |
}
|
| 789 |
|
| 790 |
+
if (err?.message?.includes('API key') || err?.message?.includes('authentication')) {
|
| 791 |
+
return NextResponse.json(
|
| 792 |
+
{ error: "Invalid API key. Please check your Google Gemini API token." },
|
| 793 |
+
{ status: 401 }
|
| 794 |
+
);
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
if (err?.message?.includes('quota') || err?.message?.includes('limit')) {
|
| 798 |
+
return NextResponse.json(
|
| 799 |
+
{ error: "API quota exceeded. Please check your Google Gemini API usage limits." },
|
| 800 |
+
{ status: 429 }
|
| 801 |
+
);
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
if (err?.message?.includes('JSON')) {
|
| 805 |
return NextResponse.json(
|
| 806 |
{ error: "Invalid data format. Please ensure images are properly encoded." },
|
app/nodes.tsx
CHANGED
|
@@ -545,7 +545,10 @@ export function BackgroundNodeView({
|
|
| 545 |
onChange={(e) => onUpdate(node.id, { backgroundType: (e.target as HTMLSelectElement).value })}
|
| 546 |
>
|
| 547 |
<option value="color">Solid Color</option>
|
|
|
|
| 548 |
<option value="image">Preset Background</option>
|
|
|
|
|
|
|
| 549 |
<option value="upload">Upload Image</option>
|
| 550 |
<option value="custom">Custom Prompt</option>
|
| 551 |
</Select>
|
|
@@ -558,6 +561,48 @@ export function BackgroundNodeView({
|
|
| 558 |
/>
|
| 559 |
)}
|
| 560 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
{node.backgroundType === "image" && (
|
| 562 |
<Select
|
| 563 |
className="w-full"
|
|
@@ -573,6 +618,92 @@ export function BackgroundNodeView({
|
|
| 573 |
</Select>
|
| 574 |
)}
|
| 575 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
{node.backgroundType === "upload" && (
|
| 577 |
<div className="space-y-2">
|
| 578 |
{node.customBackgroundImage ? (
|
|
@@ -1053,8 +1184,8 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 1053 |
// Bokeh (background blur) styles for different lens characteristics
|
| 1054 |
const bokehStyles = ["None", "Smooth Bokeh", "Swirly Bokeh", "Hexagonal Bokeh", "Cat Eye Bokeh", "Bubble Bokeh"];
|
| 1055 |
|
| 1056 |
-
//
|
| 1057 |
-
const
|
| 1058 |
|
| 1059 |
return (
|
| 1060 |
<div className="nb-node absolute text-white w-[360px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
@@ -1103,16 +1234,16 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 1103 |
{/* Basic Camera Settings Section */}
|
| 1104 |
<div className="text-xs text-white/50 font-semibold mb-1">Basic Settings</div>
|
| 1105 |
<div className="grid grid-cols-2 gap-2"> {/* 2-column grid for compact layout */}
|
| 1106 |
-
{/*
|
| 1107 |
<div>
|
| 1108 |
-
<label className="text-xs text-white/70">
|
| 1109 |
<Select
|
| 1110 |
className="w-full"
|
| 1111 |
-
value={node.
|
| 1112 |
-
onChange={(e) => onUpdate(node.id, {
|
| 1113 |
-
title="Select
|
| 1114 |
>
|
| 1115 |
-
{
|
| 1116 |
</Select>
|
| 1117 |
</div>
|
| 1118 |
{/* Focal Length Control - affects field of view and perspective */}
|
|
@@ -1707,22 +1838,34 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
|
|
| 1707 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1708 |
|
| 1709 |
/**
|
| 1710 |
-
* Available lighting preset options with
|
| 1711 |
-
* Each preset
|
| 1712 |
*/
|
| 1713 |
const presetLightings = [
|
| 1714 |
-
{
|
| 1715 |
-
|
| 1716 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1717 |
];
|
| 1718 |
|
| 1719 |
/**
|
| 1720 |
* Handle selection of a lighting preset
|
| 1721 |
-
* Updates
|
| 1722 |
*/
|
| 1723 |
-
const selectLighting = (lightingPath: string, lightingName: string) => {
|
| 1724 |
onUpdate(node.id, {
|
| 1725 |
-
|
| 1726 |
selectedLighting: lightingName // Name of selected lighting preset
|
| 1727 |
});
|
| 1728 |
};
|
|
@@ -1783,7 +1926,7 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
|
|
| 1783 |
? "border-indigo-400 bg-indigo-500/20"
|
| 1784 |
: "border-white/20 hover:border-white/40"
|
| 1785 |
}`}
|
| 1786 |
-
onClick={() => selectLighting(preset.path, preset.name)}
|
| 1787 |
>
|
| 1788 |
<img
|
| 1789 |
src={preset.path}
|
|
@@ -1859,23 +2002,39 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1859 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1860 |
|
| 1861 |
/**
|
| 1862 |
-
* Available pose preset options with
|
| 1863 |
-
* Each preset
|
| 1864 |
*/
|
| 1865 |
const presetPoses = [
|
| 1866 |
-
{
|
| 1867 |
-
|
| 1868 |
-
|
| 1869 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1870 |
];
|
| 1871 |
|
| 1872 |
/**
|
| 1873 |
* Handle selection of a pose preset
|
| 1874 |
-
* Updates
|
| 1875 |
*/
|
| 1876 |
-
const selectPose = (posePath: string, poseName: string) => {
|
| 1877 |
onUpdate(node.id, {
|
| 1878 |
-
|
| 1879 |
selectedPose: poseName // Name of selected pose preset
|
| 1880 |
});
|
| 1881 |
};
|
|
@@ -1936,7 +2095,7 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1936 |
? "border-indigo-400 bg-indigo-500/20"
|
| 1937 |
: "border-white/20 hover:border-white/40"
|
| 1938 |
}`}
|
| 1939 |
-
onClick={() => selectPose(preset.path, preset.name)}
|
| 1940 |
>
|
| 1941 |
<img
|
| 1942 |
src={preset.path}
|
|
|
|
| 545 |
onChange={(e) => onUpdate(node.id, { backgroundType: (e.target as HTMLSelectElement).value })}
|
| 546 |
>
|
| 547 |
<option value="color">Solid Color</option>
|
| 548 |
+
<option value="gradient">Gradient Color</option>
|
| 549 |
<option value="image">Preset Background</option>
|
| 550 |
+
<option value="city">City Scene</option>
|
| 551 |
+
<option value="photostudio">Photo Studio</option>
|
| 552 |
<option value="upload">Upload Image</option>
|
| 553 |
<option value="custom">Custom Prompt</option>
|
| 554 |
</Select>
|
|
|
|
| 561 |
/>
|
| 562 |
)}
|
| 563 |
|
| 564 |
+
{node.backgroundType === "gradient" && (
|
| 565 |
+
<div className="space-y-3">
|
| 566 |
+
<label className="text-xs text-white/70">Gradient Direction</label>
|
| 567 |
+
<Select
|
| 568 |
+
className="w-full"
|
| 569 |
+
value={node.gradientDirection || "to right"}
|
| 570 |
+
onChange={(e) => onUpdate(node.id, { gradientDirection: (e.target as HTMLSelectElement).value })}
|
| 571 |
+
>
|
| 572 |
+
<option value="to right">Left to Right</option>
|
| 573 |
+
<option value="to left">Right to Left</option>
|
| 574 |
+
<option value="to bottom">Top to Bottom</option>
|
| 575 |
+
<option value="to top">Bottom to Top</option>
|
| 576 |
+
<option value="to bottom right">Diagonal Top-Left to Bottom-Right</option>
|
| 577 |
+
<option value="to bottom left">Diagonal Top-Right to Bottom-Left</option>
|
| 578 |
+
<option value="to top right">Diagonal Bottom-Left to Top-Right</option>
|
| 579 |
+
<option value="to top left">Diagonal Bottom-Right to Top-Left</option>
|
| 580 |
+
<option value="radial">Radial (Center to Edge)</option>
|
| 581 |
+
</Select>
|
| 582 |
+
<label className="text-xs text-white/70">Start Color</label>
|
| 583 |
+
<ColorPicker
|
| 584 |
+
className="w-full"
|
| 585 |
+
value={node.gradientStartColor || "#ff6b6b"}
|
| 586 |
+
onChange={(e) => onUpdate(node.id, { gradientStartColor: (e.target as HTMLInputElement).value })}
|
| 587 |
+
/>
|
| 588 |
+
<label className="text-xs text-white/70">End Color</label>
|
| 589 |
+
<ColorPicker
|
| 590 |
+
className="w-full"
|
| 591 |
+
value={node.gradientEndColor || "#4ecdc4"}
|
| 592 |
+
onChange={(e) => onUpdate(node.id, { gradientEndColor: (e.target as HTMLInputElement).value })}
|
| 593 |
+
/>
|
| 594 |
+
<div
|
| 595 |
+
className="w-full h-8 rounded-md border border-white/20"
|
| 596 |
+
style={{
|
| 597 |
+
background: node.gradientDirection === "radial"
|
| 598 |
+
? `radial-gradient(circle, ${node.gradientStartColor || "#ff6b6b"} 0%, ${node.gradientEndColor || "#4ecdc4"} 100%)`
|
| 599 |
+
: `linear-gradient(${node.gradientDirection || "to right"}, ${node.gradientStartColor || "#ff6b6b"} 0%, ${node.gradientEndColor || "#4ecdc4"} 100%)`
|
| 600 |
+
}}
|
| 601 |
+
title="Gradient Preview"
|
| 602 |
+
/>
|
| 603 |
+
</div>
|
| 604 |
+
)}
|
| 605 |
+
|
| 606 |
{node.backgroundType === "image" && (
|
| 607 |
<Select
|
| 608 |
className="w-full"
|
|
|
|
| 618 |
</Select>
|
| 619 |
)}
|
| 620 |
|
| 621 |
+
{node.backgroundType === "city" && (
|
| 622 |
+
<div className="space-y-3">
|
| 623 |
+
<label className="text-xs text-white/70">City Scene Type</label>
|
| 624 |
+
<Select
|
| 625 |
+
className="w-full"
|
| 626 |
+
value={node.citySceneType || "busy_street"}
|
| 627 |
+
onChange={(e) => onUpdate(node.id, { citySceneType: (e.target as HTMLSelectElement).value })}
|
| 628 |
+
>
|
| 629 |
+
<option value="busy_street">Busy Street with Close Pedestrians</option>
|
| 630 |
+
<option value="tokyo_shibuya">Tokyo Shibuya Crossing</option>
|
| 631 |
+
<option value="tokyo_subway">Tokyo Subway</option>
|
| 632 |
+
<option value="times_square">Times Square NYC</option>
|
| 633 |
+
<option value="downtown_skyline">Downtown Skyline</option>
|
| 634 |
+
<option value="urban_crosswalk">Urban Crosswalk Scene</option>
|
| 635 |
+
<option value="shopping_district">Shopping District</option>
|
| 636 |
+
<option value="city_park">City Park</option>
|
| 637 |
+
<option value="rooftop_view">Rooftop City View</option>
|
| 638 |
+
<option value="blade_runner_street">Blade Runner Style Street</option>
|
| 639 |
+
<option value="matrix_alley">Matrix Style Urban Alley</option>
|
| 640 |
+
</Select>
|
| 641 |
+
<label className="text-xs text-white/70">Time of Day</label>
|
| 642 |
+
<Select
|
| 643 |
+
className="w-full"
|
| 644 |
+
value={node.cityTimeOfDay || "daytime"}
|
| 645 |
+
onChange={(e) => onUpdate(node.id, { cityTimeOfDay: (e.target as HTMLSelectElement).value })}
|
| 646 |
+
>
|
| 647 |
+
<option value="golden_hour">Golden Hour</option>
|
| 648 |
+
<option value="daytime">Daytime</option>
|
| 649 |
+
<option value="blue_hour">Blue Hour</option>
|
| 650 |
+
<option value="night">Night with City Lights</option>
|
| 651 |
+
<option value="dawn">Dawn</option>
|
| 652 |
+
<option value="overcast">Overcast Day</option>
|
| 653 |
+
</Select>
|
| 654 |
+
</div>
|
| 655 |
+
)}
|
| 656 |
+
|
| 657 |
+
{node.backgroundType === "photostudio" && (
|
| 658 |
+
<div className="space-y-3">
|
| 659 |
+
<label className="text-xs text-white/70">Studio Setup</label>
|
| 660 |
+
<Select
|
| 661 |
+
className="w-full"
|
| 662 |
+
value={node.studioSetup || "white_seamless"}
|
| 663 |
+
onChange={(e) => onUpdate(node.id, { studioSetup: (e.target as HTMLSelectElement).value })}
|
| 664 |
+
>
|
| 665 |
+
<option value="white_seamless">White Seamless Background</option>
|
| 666 |
+
<option value="black_seamless">Black Seamless Background</option>
|
| 667 |
+
<option value="grey_seamless">Grey Seamless Background</option>
|
| 668 |
+
<option value="colored_seamless">Colored Seamless Background</option>
|
| 669 |
+
<option value="textured_backdrop">Textured Backdrop</option>
|
| 670 |
+
<option value="infinity_cove">Infinity Cove</option>
|
| 671 |
+
</Select>
|
| 672 |
+
{node.studioSetup === "colored_seamless" && (
|
| 673 |
+
<>
|
| 674 |
+
<label className="text-xs text-white/70">Background Color</label>
|
| 675 |
+
<ColorPicker
|
| 676 |
+
className="w-full"
|
| 677 |
+
value={node.studioBackgroundColor || "#ffffff"}
|
| 678 |
+
onChange={(e) => onUpdate(node.id, { studioBackgroundColor: (e.target as HTMLInputElement).value })}
|
| 679 |
+
/>
|
| 680 |
+
</>
|
| 681 |
+
)}
|
| 682 |
+
<label className="text-xs text-white/70">Lighting Setup</label>
|
| 683 |
+
<Select
|
| 684 |
+
className="w-full"
|
| 685 |
+
value={node.studioLighting || "key_fill"}
|
| 686 |
+
onChange={(e) => onUpdate(node.id, { studioLighting: (e.target as HTMLSelectElement).value })}
|
| 687 |
+
>
|
| 688 |
+
<option value="key_fill">Key + Fill Light</option>
|
| 689 |
+
<option value="three_point">Three-Point Lighting</option>
|
| 690 |
+
<option value="beauty_lighting">Beauty Lighting</option>
|
| 691 |
+
<option value="dramatic_lighting">Dramatic Single Light</option>
|
| 692 |
+
<option value="soft_lighting">Soft Diffused Lighting</option>
|
| 693 |
+
<option value="hard_lighting">Hard Directional Lighting</option>
|
| 694 |
+
</Select>
|
| 695 |
+
<div className="flex items-center gap-2">
|
| 696 |
+
<input
|
| 697 |
+
type="checkbox"
|
| 698 |
+
checked={node.faceCamera || false}
|
| 699 |
+
onChange={(e) => onUpdate(node.id, { faceCamera: (e.target as HTMLInputElement).checked })}
|
| 700 |
+
className="w-4 h-4"
|
| 701 |
+
/>
|
| 702 |
+
<label className="text-xs text-white/70">Position character to face camera</label>
|
| 703 |
+
</div>
|
| 704 |
+
</div>
|
| 705 |
+
)}
|
| 706 |
+
|
| 707 |
{node.backgroundType === "upload" && (
|
| 708 |
<div className="space-y-2">
|
| 709 |
{node.customBackgroundImage ? (
|
|
|
|
| 1184 |
// Bokeh (background blur) styles for different lens characteristics
|
| 1185 |
const bokehStyles = ["None", "Smooth Bokeh", "Swirly Bokeh", "Hexagonal Bokeh", "Cat Eye Bokeh", "Bubble Bokeh"];
|
| 1186 |
|
| 1187 |
+
// Motion blur options
|
| 1188 |
+
const motionBlurOptions = ["None", "Light Motion Blur", "Medium Motion Blur", "Heavy Motion Blur", "Radial Blur", "Zoom Blur"];
|
| 1189 |
|
| 1190 |
return (
|
| 1191 |
<div className="nb-node absolute text-white w-[360px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 1234 |
{/* Basic Camera Settings Section */}
|
| 1235 |
<div className="text-xs text-white/50 font-semibold mb-1">Basic Settings</div>
|
| 1236 |
<div className="grid grid-cols-2 gap-2"> {/* 2-column grid for compact layout */}
|
| 1237 |
+
{/* Motion Blur Control - adds movement effects */}
|
| 1238 |
<div>
|
| 1239 |
+
<label className="text-xs text-white/70">Motion Blur</label>
|
| 1240 |
<Select
|
| 1241 |
className="w-full"
|
| 1242 |
+
value={node.motionBlur || "None"} // Default to "None" if not set
|
| 1243 |
+
onChange={(e) => onUpdate(node.id, { motionBlur: (e.target as HTMLSelectElement).value })}
|
| 1244 |
+
title="Select Motion Blur Effect"
|
| 1245 |
>
|
| 1246 |
+
{motionBlurOptions.map(f => <option key={f} value={f}>{f}</option>)}
|
| 1247 |
</Select>
|
| 1248 |
</div>
|
| 1249 |
{/* Focal Length Control - affects field of view and perspective */}
|
|
|
|
| 1838 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1839 |
|
| 1840 |
/**
|
| 1841 |
+
* Available lighting preset options with text descriptions
|
| 1842 |
+
* Each preset uses detailed lighting prompts instead of reference images
|
| 1843 |
*/
|
| 1844 |
const presetLightings = [
|
| 1845 |
+
{
|
| 1846 |
+
name: "Moody Cinematic",
|
| 1847 |
+
path: "/lighting/light1.png",
|
| 1848 |
+
prompt: "Moody cinematic portrait lighting with a sharp vertical beam of warm orange-red light cutting across the face and neck, contrasted with cool teal ambient fill on the surrounding areas. Strong chiaroscuro effect, deep shadows, high contrast between warm and cool tones, dramatic spotlight strip"
|
| 1849 |
+
},
|
| 1850 |
+
{
|
| 1851 |
+
name: "Dual-Tone Neon",
|
| 1852 |
+
path: "/lighting/light2.png",
|
| 1853 |
+
prompt: "Cinematic portrait lighting with strong dual-tone rim lights: deep blue light illuminating the front-left side of the face, intense red light as a rim light from the back-right, dark black background, high contrast, minimal fill light, dramatic neon glow"
|
| 1854 |
+
},
|
| 1855 |
+
{
|
| 1856 |
+
name: "Natural Shadow Play",
|
| 1857 |
+
path: "/lighting/light3.png",
|
| 1858 |
+
prompt: "DRAMATIC natural shadow play with hard directional sunlight filtering through foliage, creating bold contrasting patterns of light and shadow across the subject. Strong chiaroscuro effect with deep blacks and bright highlights, dappled leaf shadows dancing across face and body, creating an artistic interplay of illumination and darkness. Emphasize the sculptural quality of light carving through shadow, with sharp shadow edges and brilliant sun-kissed highlights for maximum visual impact"
|
| 1859 |
+
},
|
| 1860 |
];
|
| 1861 |
|
| 1862 |
/**
|
| 1863 |
* Handle selection of a lighting preset
|
| 1864 |
+
* Updates with the text prompt instead of reference image
|
| 1865 |
*/
|
| 1866 |
+
const selectLighting = (lightingPath: string, lightingName: string, lightingPrompt: string) => {
|
| 1867 |
onUpdate(node.id, {
|
| 1868 |
+
lightingPrompt: lightingPrompt, // Text prompt for lighting effect
|
| 1869 |
selectedLighting: lightingName // Name of selected lighting preset
|
| 1870 |
});
|
| 1871 |
};
|
|
|
|
| 1926 |
? "border-indigo-400 bg-indigo-500/20"
|
| 1927 |
: "border-white/20 hover:border-white/40"
|
| 1928 |
}`}
|
| 1929 |
+
onClick={() => selectLighting(preset.path, preset.name, preset.prompt)}
|
| 1930 |
>
|
| 1931 |
<img
|
| 1932 |
src={preset.path}
|
|
|
|
| 2002 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 2003 |
|
| 2004 |
/**
|
| 2005 |
+
* Available pose preset options with text descriptions
|
| 2006 |
+
* Each preset uses detailed pose prompts instead of reference images
|
| 2007 |
*/
|
| 2008 |
const presetPoses = [
|
| 2009 |
+
{
|
| 2010 |
+
name: "Dynamic Standing",
|
| 2011 |
+
path: "/poses/stand1.png",
|
| 2012 |
+
prompt: "A dynamic standing pose with the figure's weight shifted to one side. The right arm extends forward in a pointing gesture while the left arm hangs naturally. The figure has a slight hip tilt and appears to be in mid-movement, creating an energetic, directional composition."
|
| 2013 |
+
},
|
| 2014 |
+
{
|
| 2015 |
+
name: "Arms Crossed",
|
| 2016 |
+
path: "/poses/stand2.png",
|
| 2017 |
+
prompt: "A relaxed standing pose with arms crossed over the torso. The weight is distributed fairly evenly, with one leg slightly forward. The figure's posture suggests a casual, confident stance with the head tilted slightly downward in a contemplative manner."
|
| 2018 |
+
},
|
| 2019 |
+
{
|
| 2020 |
+
name: "Seated Composed",
|
| 2021 |
+
path: "/poses/sit1.png",
|
| 2022 |
+
prompt: "A seated pose on what appears to be a stool or high chair. The figure sits with legs crossed at the knee, creating an asymmetrical but balanced composition. The hands rest on the lap, and the overall posture is upright and composed."
|
| 2023 |
+
},
|
| 2024 |
+
{
|
| 2025 |
+
name: "Relaxed Lean",
|
| 2026 |
+
path: "/poses/sit2.png",
|
| 2027 |
+
prompt: "A more relaxed seated pose with the figure leaning to one side. One leg is bent and raised while the other extends downward. The figure appears to be resting or in casual repose, with arms supporting the body and creating a diagonal flow through the composition."
|
| 2028 |
+
},
|
| 2029 |
];
|
| 2030 |
|
| 2031 |
/**
|
| 2032 |
* Handle selection of a pose preset
|
| 2033 |
+
* Updates with the text prompt instead of reference image
|
| 2034 |
*/
|
| 2035 |
+
const selectPose = (posePath: string, poseName: string, posePrompt: string) => {
|
| 2036 |
onUpdate(node.id, {
|
| 2037 |
+
posePrompt: posePrompt, // Text prompt for pose effect
|
| 2038 |
selectedPose: poseName // Name of selected pose preset
|
| 2039 |
});
|
| 2040 |
};
|
|
|
|
| 2095 |
? "border-indigo-400 bg-indigo-500/20"
|
| 2096 |
: "border-white/20 hover:border-white/40"
|
| 2097 |
}`}
|
| 2098 |
+
onClick={() => selectPose(preset.path, preset.name, preset.prompt)}
|
| 2099 |
>
|
| 2100 |
<img
|
| 2101 |
src={preset.path}
|
app/page.tsx
CHANGED
|
@@ -196,9 +196,26 @@ type BackgroundNode = NodeBase & {
|
|
| 196 |
type: "BACKGROUND";
|
| 197 |
input?: string; // ID of the source node (usually CHARACTER)
|
| 198 |
output?: string; // Processed image with new background
|
| 199 |
-
backgroundType: "color" | "image" | "upload" | "custom"; // Type of background to apply
|
| 200 |
backgroundColor?: string; // Hex color code for solid color backgrounds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
backgroundImage?: string; // URL/path for preset background images
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
customBackgroundImage?: string; // User-uploaded background image data
|
| 203 |
customPrompt?: string; // AI prompt for generating custom backgrounds
|
| 204 |
isRunning?: boolean; // Processing state indicator
|
|
@@ -266,6 +283,7 @@ type CameraNode = NodeBase & {
|
|
| 266 |
bokeh?: string; // Background blur style
|
| 267 |
composition?: string; // Composition technique
|
| 268 |
aspectRatio?: string; // Image aspect ratio
|
|
|
|
| 269 |
isRunning?: boolean; // Processing status
|
| 270 |
error?: string | null; // Error message
|
| 271 |
};
|
|
@@ -327,7 +345,7 @@ type LightningNode = NodeBase & {
|
|
| 327 |
input?: string; // Source node ID
|
| 328 |
output?: string; // Image with lighting applied
|
| 329 |
selectedLighting?: string; // Selected lighting preset name
|
| 330 |
-
|
| 331 |
lightingStrength?: number; // Intensity of lighting effect (0-100)
|
| 332 |
isRunning?: boolean; // Processing state
|
| 333 |
error?: string | null; // Error message
|
|
@@ -343,7 +361,7 @@ type PosesNode = NodeBase & {
|
|
| 343 |
input?: string; // Source node ID
|
| 344 |
output?: string; // Image with pose applied
|
| 345 |
selectedPose?: string; // Selected pose preset name
|
| 346 |
-
|
| 347 |
poseStrength?: number; // How strongly to apply the pose (0-100)
|
| 348 |
isRunning?: boolean; // Processing state
|
| 349 |
error?: string | null; // Error message
|
|
@@ -1177,11 +1195,33 @@ export default function EditorPage() {
|
|
| 1177 |
switch (node.type) {
|
| 1178 |
case "BACKGROUND":
|
| 1179 |
if ((node as BackgroundNode).backgroundType) {
|
| 1180 |
-
|
| 1181 |
-
config.
|
| 1182 |
-
config.
|
| 1183 |
-
config.
|
| 1184 |
-
config.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1185 |
}
|
| 1186 |
break;
|
| 1187 |
case "CLOTHES":
|
|
@@ -1214,6 +1254,7 @@ export default function EditorPage() {
|
|
| 1214 |
if (cam.bokeh && cam.bokeh !== "None") config.bokeh = cam.bokeh;
|
| 1215 |
if (cam.composition && cam.composition !== "None") config.composition = cam.composition;
|
| 1216 |
if (cam.aspectRatio && cam.aspectRatio !== "None") config.aspectRatio = cam.aspectRatio;
|
|
|
|
| 1217 |
break;
|
| 1218 |
case "AGE":
|
| 1219 |
if ((node as AgeNode).targetAge) {
|
|
@@ -1241,6 +1282,18 @@ export default function EditorPage() {
|
|
| 1241 |
}
|
| 1242 |
}
|
| 1243 |
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1244 |
}
|
| 1245 |
|
| 1246 |
return config;
|
|
@@ -1509,7 +1562,13 @@ export default function EditorPage() {
|
|
| 1509 |
}
|
| 1510 |
|
| 1511 |
const data = await res.json();
|
| 1512 |
-
if (!res.ok)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1513 |
|
| 1514 |
// Only update the current node with the output
|
| 1515 |
// Don't show output in intermediate nodes - they were just used for configuration
|
|
|
|
| 196 |
type: "BACKGROUND";
|
| 197 |
input?: string; // ID of the source node (usually CHARACTER)
|
| 198 |
output?: string; // Processed image with new background
|
| 199 |
+
backgroundType: "color" | "gradient" | "image" | "city" | "photostudio" | "upload" | "custom"; // Type of background to apply
|
| 200 |
backgroundColor?: string; // Hex color code for solid color backgrounds
|
| 201 |
+
|
| 202 |
+
// Gradient background properties
|
| 203 |
+
gradientDirection?: string; // Direction of gradient (to right, to bottom, radial, etc.)
|
| 204 |
+
gradientStartColor?: string; // Starting color of gradient
|
| 205 |
+
gradientEndColor?: string; // Ending color of gradient
|
| 206 |
+
|
| 207 |
backgroundImage?: string; // URL/path for preset background images
|
| 208 |
+
|
| 209 |
+
// City scene properties
|
| 210 |
+
citySceneType?: string; // Type of city scene (busy_street, times_square, etc.)
|
| 211 |
+
cityTimeOfDay?: string; // Time of day for city scene
|
| 212 |
+
|
| 213 |
+
// Photo studio properties
|
| 214 |
+
studioSetup?: string; // Studio background setup type
|
| 215 |
+
studioBackgroundColor?: string; // Color for colored seamless background
|
| 216 |
+
studioLighting?: string; // Studio lighting setup
|
| 217 |
+
faceCamera?: boolean; // Whether to position character facing camera
|
| 218 |
+
|
| 219 |
customBackgroundImage?: string; // User-uploaded background image data
|
| 220 |
customPrompt?: string; // AI prompt for generating custom backgrounds
|
| 221 |
isRunning?: boolean; // Processing state indicator
|
|
|
|
| 283 |
bokeh?: string; // Background blur style
|
| 284 |
composition?: string; // Composition technique
|
| 285 |
aspectRatio?: string; // Image aspect ratio
|
| 286 |
+
motionBlur?: string; // Motion blur effect
|
| 287 |
isRunning?: boolean; // Processing status
|
| 288 |
error?: string | null; // Error message
|
| 289 |
};
|
|
|
|
| 345 |
input?: string; // Source node ID
|
| 346 |
output?: string; // Image with lighting applied
|
| 347 |
selectedLighting?: string; // Selected lighting preset name
|
| 348 |
+
lightingPrompt?: string; // Text prompt for lighting effect
|
| 349 |
lightingStrength?: number; // Intensity of lighting effect (0-100)
|
| 350 |
isRunning?: boolean; // Processing state
|
| 351 |
error?: string | null; // Error message
|
|
|
|
| 361 |
input?: string; // Source node ID
|
| 362 |
output?: string; // Image with pose applied
|
| 363 |
selectedPose?: string; // Selected pose preset name
|
| 364 |
+
posePrompt?: string; // Text prompt for pose effect
|
| 365 |
poseStrength?: number; // How strongly to apply the pose (0-100)
|
| 366 |
isRunning?: boolean; // Processing state
|
| 367 |
error?: string | null; // Error message
|
|
|
|
| 1195 |
switch (node.type) {
|
| 1196 |
case "BACKGROUND":
|
| 1197 |
if ((node as BackgroundNode).backgroundType) {
|
| 1198 |
+
const bgNode = node as BackgroundNode;
|
| 1199 |
+
config.backgroundType = bgNode.backgroundType;
|
| 1200 |
+
config.backgroundColor = bgNode.backgroundColor;
|
| 1201 |
+
config.backgroundImage = bgNode.backgroundImage;
|
| 1202 |
+
config.customBackgroundImage = bgNode.customBackgroundImage;
|
| 1203 |
+
config.customPrompt = bgNode.customPrompt;
|
| 1204 |
+
|
| 1205 |
+
// Gradient properties
|
| 1206 |
+
if (bgNode.backgroundType === "gradient") {
|
| 1207 |
+
config.gradientDirection = bgNode.gradientDirection;
|
| 1208 |
+
config.gradientStartColor = bgNode.gradientStartColor;
|
| 1209 |
+
config.gradientEndColor = bgNode.gradientEndColor;
|
| 1210 |
+
}
|
| 1211 |
+
|
| 1212 |
+
// City scene properties
|
| 1213 |
+
if (bgNode.backgroundType === "city") {
|
| 1214 |
+
config.citySceneType = bgNode.citySceneType;
|
| 1215 |
+
config.cityTimeOfDay = bgNode.cityTimeOfDay;
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
// Photo studio properties
|
| 1219 |
+
if (bgNode.backgroundType === "photostudio") {
|
| 1220 |
+
config.studioSetup = bgNode.studioSetup;
|
| 1221 |
+
config.studioBackgroundColor = bgNode.studioBackgroundColor;
|
| 1222 |
+
config.studioLighting = bgNode.studioLighting;
|
| 1223 |
+
config.faceCamera = bgNode.faceCamera;
|
| 1224 |
+
}
|
| 1225 |
}
|
| 1226 |
break;
|
| 1227 |
case "CLOTHES":
|
|
|
|
| 1254 |
if (cam.bokeh && cam.bokeh !== "None") config.bokeh = cam.bokeh;
|
| 1255 |
if (cam.composition && cam.composition !== "None") config.composition = cam.composition;
|
| 1256 |
if (cam.aspectRatio && cam.aspectRatio !== "None") config.aspectRatio = cam.aspectRatio;
|
| 1257 |
+
if (cam.motionBlur && cam.motionBlur !== "None") config.motionBlur = cam.motionBlur;
|
| 1258 |
break;
|
| 1259 |
case "AGE":
|
| 1260 |
if ((node as AgeNode).targetAge) {
|
|
|
|
| 1282 |
}
|
| 1283 |
}
|
| 1284 |
break;
|
| 1285 |
+
case "LIGHTNING":
|
| 1286 |
+
if ((node as LightningNode).lightingPrompt && (node as LightningNode).selectedLighting) {
|
| 1287 |
+
config.lightingPrompt = (node as LightningNode).lightingPrompt;
|
| 1288 |
+
config.selectedLighting = (node as LightningNode).selectedLighting;
|
| 1289 |
+
}
|
| 1290 |
+
break;
|
| 1291 |
+
case "POSES":
|
| 1292 |
+
if ((node as PosesNode).posePrompt && (node as PosesNode).selectedPose) {
|
| 1293 |
+
config.posePrompt = (node as PosesNode).posePrompt;
|
| 1294 |
+
config.selectedPose = (node as PosesNode).selectedPose;
|
| 1295 |
+
}
|
| 1296 |
+
break;
|
| 1297 |
}
|
| 1298 |
|
| 1299 |
return config;
|
|
|
|
| 1562 |
}
|
| 1563 |
|
| 1564 |
const data = await res.json();
|
| 1565 |
+
if (!res.ok) {
|
| 1566 |
+
// Handle both string and object error formats
|
| 1567 |
+
const errorMessage = typeof data.error === 'string'
|
| 1568 |
+
? data.error
|
| 1569 |
+
: data.error?.message || JSON.stringify(data.error) || "Processing failed";
|
| 1570 |
+
throw new Error(errorMessage);
|
| 1571 |
+
}
|
| 1572 |
|
| 1573 |
// Only update the current node with the output
|
| 1574 |
// Don't show output in intermediate nodes - they were just used for configuration
|