Reubencf commited on
Commit
2526fde
·
1 Parent(s): 1d30c5e

made changes to the code

Browse files
Files changed (4) hide show
  1. .claude/settings.local.json +2 -1
  2. app/api/process/route.ts +379 -47
  3. app/nodes.tsx +186 -27
  4. 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
- } else if (params.customPrompt) {
 
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
- "Gibhli": "Apply Studio Ghibli style with vibrant colors, detailed character design, and fantasy elements typical of Studio Ghibli movies",
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
- cameraSettings.push("Apply 8mm fisheye lens effect with 180-degree circular distortion");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  } else {
353
- cameraSettings.push(`Focal Length: ${params.focalLength}`);
354
  }
355
  }
356
- if (params.aperture) cameraSettings.push(`Aperture: ${params.aperture}`);
357
- if (params.shutterSpeed) cameraSettings.push(`Shutter Speed: ${params.shutterSpeed}`);
358
- if (params.whiteBalance) cameraSettings.push(`White Balance: ${params.whiteBalance}`);
359
- if (params.angle) cameraSettings.push(`Camera Angle: ${params.angle}`);
360
- if (params.iso) cameraSettings.push(`${params.iso}`);
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
- // Lightning effects
375
- if (params.lightingImage && params.selectedLighting) {
376
- prompts.push(`Apply ${params.selectedLighting} lighting effect to the person in the image. Adjust the lighting, shadows, and highlights to match the reference lighting style shown in the second image. Maintain the person's appearance, pose, and background`);
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.poseImage && params.selectedPose) {
390
- prompts.push(`Change the pose of the person in the first image to match the pose shown in the reference image. Keep the person's facial features, clothing, and overall appearance the same, only modify their body position and pose to match the reference`);
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
- const response = await ai.models.generateContent({
439
- model: "gemini-2.5-flash-image-preview",
440
- contents: parts,
441
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
 
 
443
  const outParts = (response as any)?.candidates?.[0]?.content?.parts ?? [];
444
  const images: string[] = [];
 
445
 
446
- for (const p of outParts) {
 
 
 
 
 
 
 
 
 
 
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
- { error: "No image generated. Try adjusting your parameters." },
 
 
 
 
 
 
 
 
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
- // Manual or automatic
1057
- const manualOrAutomatic = ["None", "AF-S", "AF-C", "AF-A"];
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
- {/* Automaticormanual Control - affects field of view and perspective */}
1107
  <div>
1108
- <label className="text-xs text-white/70">Manual or Automatic</label>
1109
  <Select
1110
  className="w-full"
1111
- value={node.manualOrAutomatic || "None"} // Default to "None" if not set
1112
- onChange={(e) => onUpdate(node.id, { manualOrAutomatic: (e.target as HTMLSelectElement).value })}
1113
- title="Select Focus Modes"
1114
  >
1115
- {manualOrAutomatic.map(f => <option key={f} value={f}>{f}</option>)}
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 reference images
1711
- * Each preset demonstrates a different lighting setup and mood
1712
  */
1713
  const presetLightings = [
1714
- { name: "Studio Light", path: "/lighting/light1.png" }, // Professional studio lighting
1715
- { name: "Natural Light", path: "/lighting/light2.png" }, // Soft natural daylight
1716
- { name: "Dramatic Light", path: "/lighting/light3.png" }, // High-contrast dramatic lighting
 
 
 
 
 
 
 
 
 
 
 
 
1717
  ];
1718
 
1719
  /**
1720
  * Handle selection of a lighting preset
1721
- * Updates both the lighting image path and the selected preset name
1722
  */
1723
- const selectLighting = (lightingPath: string, lightingName: string) => {
1724
  onUpdate(node.id, {
1725
- lightingImage: lightingPath, // Path to lighting reference image
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 reference images
1863
- * Each preset shows a different body position or posture
1864
  */
1865
  const presetPoses = [
1866
- { name: "Standing Pose 1", path: "/poses/stand1.png" }, // First standing position variant
1867
- { name: "Standing Pose 2", path: "/poses/stand2.png" }, // Second standing position variant
1868
- { name: "Sitting Pose 1", path: "/poses/sit1.png" }, // First sitting position variant
1869
- { name: "Sitting Pose 2", path: "/poses/sit2.png" }, // Second sitting position variant
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1870
  ];
1871
 
1872
  /**
1873
  * Handle selection of a pose preset
1874
- * Updates both the pose reference image and the selected pose name
1875
  */
1876
- const selectPose = (posePath: string, poseName: string) => {
1877
  onUpdate(node.id, {
1878
- poseImage: posePath, // Path to pose reference image
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
- lightingImage?: string; // Path to lighting image
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
- poseImage?: string; // Path to pose reference image
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
- config.backgroundType = (node as BackgroundNode).backgroundType;
1181
- config.backgroundColor = (node as BackgroundNode).backgroundColor;
1182
- config.backgroundImage = (node as BackgroundNode).backgroundImage;
1183
- config.customBackgroundImage = (node as BackgroundNode).customBackgroundImage;
1184
- config.customPrompt = (node as BackgroundNode).customPrompt;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) throw new Error(data.error || "Processing failed");
 
 
 
 
 
 
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