prithivMLmods commited on
Commit
1f11830
·
verified ·
1 Parent(s): dd04450

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -219
app.py CHANGED
@@ -15,17 +15,17 @@ from gradio.themes.utils import colors, fonts, sizes
15
 
16
  colors.orange_red = colors.Color(
17
  name="orange_red",
18
- c50="rgb(255, 240, 229)",
19
- c100="rgb(255, 224, 204)",
20
- c200="rgb(255, 194, 153)",
21
- c300="rgb(255, 163, 102)",
22
- c400="rgb(255, 133, 51)",
23
- c500="rgb(255, 69, 0)",
24
- c600="rgb(230, 62, 0)",
25
- c700="rgb(204, 55, 0)",
26
- c800="rgb(179, 48, 0)",
27
- c900="rgb(153, 41, 0)",
28
- c950="rgb(128, 34, 0)",
29
  )
30
 
31
  class OrangeRedTheme(Soft):
@@ -127,9 +127,21 @@ ELEVATION_MAP = {
127
  }
128
 
129
  def snap_to_nearest(value, options):
 
130
  return min(options, key=lambda x: abs(x - value))
131
 
132
  def build_lighting_prompt(azimuth: float, elevation: float) -> str:
 
 
 
 
 
 
 
 
 
 
 
133
  azimuth_snapped = snap_to_nearest(azimuth, list(AZIMUTH_MAP.keys()))
134
  elevation_snapped = snap_to_nearest(elevation, list(ELEVATION_MAP.keys()))
135
 
@@ -150,6 +162,9 @@ def infer_lighting_edit(
150
  height: int = 1024,
151
  width: int = 1024,
152
  ):
 
 
 
153
  global loaded
154
  progress = gr.Progress(track_tqdm=True)
155
 
@@ -183,6 +198,7 @@ def infer_lighting_edit(
183
  return result, seed, prompt
184
 
185
  def update_dimensions_on_upload(image):
 
186
  if image is None:
187
  return 1024, 1024
188
  original_width, original_height = image.size
@@ -199,30 +215,40 @@ def update_dimensions_on_upload(image):
199
  return new_width, new_height
200
 
201
  class LightingControl3D(gr.HTML):
 
 
 
 
 
202
  def __init__(self, value=None, imageUrl=None, **kwargs):
203
  if value is None:
204
  value = {"azimuth": 0, "elevation": 0}
205
 
206
  html_template = """
207
- <div id="lighting-control-wrapper" style="width: 100%; height: 450px; position: relative; background: rgb(26, 26, 26); border-radius: 12px; overflow: hidden;">
208
- <div id="prompt-overlay" style="position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); padding: 8px 16px; border-radius: 8px; font-family: monospace; font-size: 12px; color: rgb(0, 255, 136); white-space: nowrap; z-index: 10;"></div>
209
  </div>
210
  """
211
 
212
  js_on_load = """
213
  (() => {
214
- const wrapper = element.querySelector('[id="lighting-control-wrapper"]');
215
- const promptOverlay = element.querySelector('[id="prompt-overlay"]');
216
 
 
217
  const initScene = () => {
218
  if (typeof THREE === 'undefined') {
219
  setTimeout(initScene, 100);
220
  return;
221
  }
222
 
 
223
  const scene = new THREE.Scene();
224
  scene.background = new THREE.Color(0x1a1a1a);
225
 
 
 
 
226
  const camera = new THREE.PerspectiveCamera(50, wrapper.clientWidth / wrapper.clientHeight, 0.1, 1000);
227
  camera.position.set(4.5, 3, 4.5);
228
  camera.lookAt(0, 0.75, 0);
@@ -234,8 +260,10 @@ class LightingControl3D(gr.HTML):
234
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
235
  wrapper.insertBefore(renderer.domElement, promptOverlay);
236
 
 
237
  scene.add(new THREE.AmbientLight(0xffffff, 0.1));
238
 
 
239
  const ground = new THREE.Mesh(
240
  new THREE.PlaneGeometry(10, 10),
241
  new THREE.ShadowMaterial({ opacity: 0.3 })
@@ -245,16 +273,20 @@ class LightingControl3D(gr.HTML):
245
  ground.receiveShadow = true;
246
  scene.add(ground);
247
 
 
248
  scene.add(new THREE.GridHelper(8, 16, 0x333333, 0x222222));
249
 
 
250
  const CENTER = new THREE.Vector3(0, 0.75, 0);
251
  const BASE_DISTANCE = 2.5;
252
  const AZIMUTH_RADIUS = 2.4;
253
  const ELEVATION_RADIUS = 1.8;
254
 
 
255
  let azimuthAngle = props.value?.azimuth || 0;
256
  let elevationAngle = props.value?.elevation || 0;
257
 
 
258
  const azimuthSteps = [0, 45, 90, 135, 180, 225, 270, 315];
259
  const elevationSteps = [-90, 0, 90];
260
  const azimuthNames = {
@@ -268,23 +300,24 @@ class LightingControl3D(gr.HTML):
268
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
269
  }
270
 
 
271
  function createPlaceholderTexture() {
272
  const canvas = document.createElement('canvas');
273
  canvas.width = 256;
274
  canvas.height = 256;
275
  const ctx = canvas.getContext('2d');
276
- ctx.fillStyle = 'rgb(58, 58, 74)';
277
  ctx.fillRect(0, 0, 256, 256);
278
- ctx.fillStyle = 'rgb(255, 204, 153)';
279
  ctx.beginPath();
280
  ctx.arc(128, 128, 80, 0, Math.PI * 2);
281
  ctx.fill();
282
- ctx.fillStyle = 'rgb(51, 51, 51)';
283
  ctx.beginPath();
284
  ctx.arc(100, 110, 10, 0, Math.PI * 2);
285
  ctx.arc(156, 110, 10, 0, Math.PI * 2);
286
  ctx.fill();
287
- ctx.strokeStyle = 'rgb(51, 51, 51)';
288
  ctx.lineWidth = 3;
289
  ctx.beginPath();
290
  ctx.arc(128, 130, 35, 0.2, Math.PI - 0.2);
@@ -292,6 +325,7 @@ class LightingControl3D(gr.HTML):
292
  return new THREE.CanvasTexture(canvas);
293
  }
294
 
 
295
  let currentTexture = createPlaceholderTexture();
296
  const planeMaterial = new THREE.MeshStandardMaterial({ map: currentTexture, side: THREE.DoubleSide, roughness: 0.5, metalness: 0 });
297
  let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
@@ -299,10 +333,13 @@ class LightingControl3D(gr.HTML):
299
  targetPlane.receiveShadow = true;
300
  scene.add(targetPlane);
301
 
 
302
  function updateTextureFromUrl(url) {
303
  if (!url) {
 
304
  planeMaterial.map = createPlaceholderTexture();
305
  planeMaterial.needsUpdate = true;
 
306
  scene.remove(targetPlane);
307
  targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
308
  targetPlane.position.copy(CENTER);
@@ -319,6 +356,7 @@ class LightingControl3D(gr.HTML):
319
  planeMaterial.map = texture;
320
  planeMaterial.needsUpdate = true;
321
 
 
322
  const img = texture.image;
323
  if (img && img.width && img.height) {
324
  const aspect = img.width / img.height;
@@ -345,189 +383,62 @@ class LightingControl3D(gr.HTML):
345
  });
346
  }
347
 
 
348
  if (props.imageUrl) {
349
  updateTextureFromUrl(props.imageUrl);
350
  }
351
 
 
352
  const lightGroup = new THREE.Group();
353
-
354
- const handleGeo = new THREE.CylinderGeometry(0.06, 0.07, 0.45, 16);
355
- const handleMat = new THREE.MeshStandardMaterial({
356
- color: 0x2a2a2a,
357
- roughness: 0.4,
358
- metalness: 0.7
 
359
  });
360
- const handle = new THREE.Mesh(handleGeo, handleMat);
361
  handle.rotation.x = Math.PI / 2;
362
- handle.position.z = -0.35;
363
  lightGroup.add(handle);
364
-
365
- const gripGeo = new THREE.CylinderGeometry(0.065, 0.065, 0.3, 16);
366
- const gripMat = new THREE.MeshStandardMaterial({
367
- color: 0x1a1a1a,
368
- roughness: 0.8,
369
- metalness: 0.2
370
- });
371
- const grip = new THREE.Mesh(gripGeo, gripMat);
372
- grip.rotation.x = Math.PI / 2;
373
- grip.position.z = -0.35;
374
- lightGroup.add(grip);
375
-
376
- for (let i = 0; i < 8; i++) {
377
- const ringGeo = new THREE.TorusGeometry(0.068, 0.008, 8, 16);
378
- const ringMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.6, metalness: 0.3 });
379
- const ring = new THREE.Mesh(ringGeo, ringMat);
380
- ring.position.z = -0.5 + i * 0.035;
381
- lightGroup.add(ring);
382
- }
383
-
384
- const headGeo = new THREE.CylinderGeometry(0.18, 0.08, 0.25, 24);
385
- const headMat = new THREE.MeshStandardMaterial({
386
- color: 0x444444,
387
- roughness: 0.3,
388
- metalness: 0.8
389
- });
390
- const head = new THREE.Mesh(headGeo, headMat);
391
  head.rotation.x = Math.PI / 2;
392
- head.position.z = 0;
393
  lightGroup.add(head);
394
-
395
- const rimGeo = new THREE.TorusGeometry(0.18, 0.02, 16, 32);
396
- const rimMat = new THREE.MeshStandardMaterial({
397
- color: 0x666666,
398
- roughness: 0.2,
399
- metalness: 0.9
400
- });
401
- const rim = new THREE.Mesh(rimGeo, rimMat);
402
- rim.position.z = 0.12;
403
- lightGroup.add(rim);
404
-
405
- const reflectorGeo = new THREE.ConeGeometry(0.15, 0.18, 24, 1, true);
406
- const reflectorMat = new THREE.MeshStandardMaterial({
407
- color: 0xcccccc,
408
- roughness: 0.1,
409
- metalness: 1.0,
410
- side: THREE.BackSide
411
- });
412
- const reflector = new THREE.Mesh(reflectorGeo, reflectorMat);
413
- reflector.rotation.x = -Math.PI / 2;
414
- reflector.position.z = 0.03;
415
- lightGroup.add(reflector);
416
-
417
- const lensGeo = new THREE.CircleGeometry(0.12, 32);
418
  const lensMat = new THREE.MeshStandardMaterial({
419
- color: 0xffffee,
420
- emissive: 0xffffaa,
421
- emissiveIntensity: 4.0,
422
- transparent: true,
423
- opacity: 0.95
424
  });
425
  const lens = new THREE.Mesh(lensGeo, lensMat);
426
- lens.position.z = 0.13;
427
  lightGroup.add(lens);
428
-
429
- const glowGeo = new THREE.CircleGeometry(0.18, 32);
430
- const glowMat = new THREE.MeshBasicMaterial({
431
- color: 0xffffcc,
432
- transparent: true,
433
- opacity: 0.3
434
- });
435
- const glow = new THREE.Mesh(glowGeo, glowMat);
436
- glow.position.z = 0.14;
437
- lightGroup.add(glow);
438
-
439
- const rayGroup = new THREE.Group();
440
-
441
- const coreRayGeo = new THREE.ConeGeometry(0.08, 2.0, 32, 1, true);
442
- const coreRayMat = new THREE.MeshBasicMaterial({
443
  color: 0xffffee,
444
  transparent: true,
445
- opacity: 0.25,
446
- side: THREE.DoubleSide
447
- });
448
- const coreRay = new THREE.Mesh(coreRayGeo, coreRayMat);
449
- coreRay.rotation.x = -Math.PI / 2;
450
- coreRay.position.z = 1.15;
451
- rayGroup.add(coreRay);
452
-
453
- const midRayGeo = new THREE.ConeGeometry(0.25, 2.2, 32, 1, true);
454
- const midRayMat = new THREE.MeshBasicMaterial({
455
- color: 0xffffaa,
456
- transparent: true,
457
  opacity: 0.12,
458
- side: THREE.DoubleSide
459
- });
460
- const midRay = new THREE.Mesh(midRayGeo, midRayMat);
461
- midRay.rotation.x = -Math.PI / 2;
462
- midRay.position.z = 1.25;
463
- rayGroup.add(midRay);
464
-
465
- const outerRayGeo = new THREE.ConeGeometry(0.5, 2.5, 32, 1, true);
466
- const outerRayMat = new THREE.MeshBasicMaterial({
467
- color: 0xffff88,
468
- transparent: true,
469
- opacity: 0.06,
470
- side: THREE.DoubleSide
471
- });
472
- const outerRay = new THREE.Mesh(outerRayGeo, outerRayMat);
473
- outerRay.rotation.x = -Math.PI / 2;
474
- outerRay.position.z = 1.4;
475
- rayGroup.add(outerRay);
476
-
477
- const numBeams = 12;
478
- for (let i = 0; i < numBeams; i++) {
479
- const angle = (i / numBeams) * Math.PI * 2;
480
- const beamLength = 1.8 + Math.random() * 0.4;
481
- const beamGeo = new THREE.CylinderGeometry(0.008, 0.002, beamLength, 8);
482
- const beamMat = new THREE.MeshBasicMaterial({
483
- color: 0xffffcc,
484
- transparent: true,
485
- opacity: 0.15 + Math.random() * 0.1
486
- });
487
- const beam = new THREE.Mesh(beamGeo, beamMat);
488
- beam.rotation.x = -Math.PI / 2;
489
- const spread = 0.15;
490
- beam.position.set(
491
- Math.cos(angle) * spread,
492
- Math.sin(angle) * spread,
493
- 0.15 + beamLength / 2
494
- );
495
- beam.rotation.y = Math.cos(angle) * 0.1;
496
- beam.rotation.x += Math.sin(angle) * 0.1;
497
- rayGroup.add(beam);
498
- }
499
-
500
- const particleCount = 30;
501
- const particlePositions = [];
502
- const particleSizes = [];
503
- for (let i = 0; i < particleCount; i++) {
504
- const t = Math.random();
505
- const angle = Math.random() * Math.PI * 2;
506
- const radius = Math.random() * 0.3 * t;
507
- particlePositions.push(
508
- Math.cos(angle) * radius,
509
- Math.sin(angle) * radius,
510
- 0.3 + t * 1.8
511
- );
512
- particleSizes.push(0.02 + Math.random() * 0.02);
513
- }
514
-
515
- const particleGeo = new THREE.BufferGeometry();
516
- particleGeo.setAttribute('position', new THREE.Float32BufferAttribute(particlePositions, 3));
517
- const particleMat = new THREE.PointsMaterial({
518
- color: 0xffffee,
519
- size: 0.03,
520
- transparent: true,
521
- opacity: 0.4,
522
- sizeAttenuation: true
523
  });
524
- const particles = new THREE.Points(particleGeo, particleMat);
525
- rayGroup.add(particles);
526
-
527
- lightGroup.add(rayGroup);
528
-
529
- const spotLight = new THREE.SpotLight(0xffffee, 12, 10, Math.PI / 4, 0.5, 1);
530
- spotLight.position.set(0, 0, 0.1);
 
531
  spotLight.castShadow = true;
532
  spotLight.shadow.mapSize.width = 1024;
533
  spotLight.shadow.mapSize.height = 1024;
@@ -543,6 +454,9 @@ class LightingControl3D(gr.HTML):
543
 
544
  scene.add(lightGroup);
545
 
 
 
 
546
  const azimuthRing = new THREE.Mesh(
547
  new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.04, 16, 64),
548
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.3 })
@@ -551,6 +465,7 @@ class LightingControl3D(gr.HTML):
551
  azimuthRing.position.y = 0.05;
552
  scene.add(azimuthRing);
553
 
 
554
  const azimuthHandle = new THREE.Mesh(
555
  new THREE.SphereGeometry(0.18, 16, 16),
556
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 })
@@ -558,6 +473,7 @@ class LightingControl3D(gr.HTML):
558
  azimuthHandle.userData.type = 'azimuth';
559
  scene.add(azimuthHandle);
560
 
 
561
  const arcPoints = [];
562
  for (let i = 0; i <= 32; i++) {
563
  const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 32));
@@ -570,6 +486,7 @@ class LightingControl3D(gr.HTML):
570
  );
571
  scene.add(elevationArc);
572
 
 
573
  const elevationHandle = new THREE.Mesh(
574
  new THREE.SphereGeometry(0.18, 16, 16),
575
  new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.5 })
@@ -577,13 +494,14 @@ class LightingControl3D(gr.HTML):
577
  elevationHandle.userData.type = 'elevation';
578
  scene.add(elevationHandle);
579
 
 
580
  const refreshBtn = document.createElement('button');
581
  refreshBtn.innerHTML = 'Reset View';
582
  refreshBtn.style.position = 'absolute';
583
  refreshBtn.style.top = '15px';
584
  refreshBtn.style.right = '15px';
585
- refreshBtn.style.background = 'rgb(230, 62, 0)';
586
- refreshBtn.style.color = 'rgb(255, 255, 255)';
587
  refreshBtn.style.border = 'none';
588
  refreshBtn.style.padding = '8px 16px';
589
  refreshBtn.style.borderRadius = '6px';
@@ -595,8 +513,8 @@ class LightingControl3D(gr.HTML):
595
  refreshBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
596
  refreshBtn.style.transition = 'background 0.2s';
597
 
598
- refreshBtn.onmouseover = () => refreshBtn.style.background = 'rgb(255, 87, 34)';
599
- refreshBtn.onmouseout = () => refreshBtn.style.background = 'rgb(230, 62, 0)';
600
 
601
  wrapper.appendChild(refreshBtn);
602
 
@@ -607,8 +525,6 @@ class LightingControl3D(gr.HTML):
607
  updatePropsAndTrigger();
608
  });
609
 
610
- let time = 0;
611
-
612
  function updatePositions() {
613
  const distance = BASE_DISTANCE;
614
  const azRad = THREE.MathUtils.degToRad(azimuthAngle);
@@ -624,6 +540,7 @@ class LightingControl3D(gr.HTML):
624
  azimuthHandle.position.set(AZIMUTH_RADIUS * Math.sin(azRad), 0.05, AZIMUTH_RADIUS * Math.cos(azRad));
625
  elevationHandle.position.set(-0.8, ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y, ELEVATION_RADIUS * Math.cos(elRad));
626
 
 
627
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
628
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
629
  let prompt = 'Light source from';
@@ -643,6 +560,7 @@ class LightingControl3D(gr.HTML):
643
  trigger('change', props.value);
644
  }
645
 
 
646
  const raycaster = new THREE.Raycaster();
647
  const mouse = new THREE.Vector2();
648
  let isDragging = false;
@@ -715,6 +633,7 @@ class LightingControl3D(gr.HTML):
715
  dragTarget.material.emissiveIntensity = 0.5;
716
  dragTarget.scale.setScalar(1);
717
 
 
718
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
719
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
720
 
@@ -747,7 +666,7 @@ class LightingControl3D(gr.HTML):
747
 
748
  canvas.addEventListener('mouseup', onMouseUp);
749
  canvas.addEventListener('mouseleave', onMouseUp);
750
-
751
  canvas.addEventListener('touchstart', (e) => {
752
  e.preventDefault();
753
  const touch = e.touches[0];
@@ -805,43 +724,24 @@ class LightingControl3D(gr.HTML):
805
  onMouseUp();
806
  }, { passive: false });
807
 
 
808
  updatePositions();
809
 
 
810
  function render() {
811
  requestAnimationFrame(render);
812
-
813
- time += 0.016;
814
-
815
- lensMat.emissiveIntensity = 3.5 + Math.sin(time * 3) * 0.5;
816
- glowMat.opacity = 0.25 + Math.sin(time * 2) * 0.1;
817
-
818
- coreRayMat.opacity = 0.22 + Math.sin(time * 4) * 0.03;
819
- midRayMat.opacity = 0.10 + Math.sin(time * 3 + 1) * 0.02;
820
- outerRayMat.opacity = 0.05 + Math.sin(time * 2 + 2) * 0.015;
821
-
822
- const positions = particles.geometry.attributes.position.array;
823
- for (let i = 0; i < particleCount; i++) {
824
- positions[i * 3 + 2] += 0.01;
825
- if (positions[i * 3 + 2] > 2.2) {
826
- positions[i * 3 + 2] = 0.3;
827
- const angle = Math.random() * Math.PI * 2;
828
- const radius = Math.random() * 0.1;
829
- positions[i * 3] = Math.cos(angle) * radius;
830
- positions[i * 3 + 1] = Math.sin(angle) * radius;
831
- }
832
- }
833
- particles.geometry.attributes.position.needsUpdate = true;
834
-
835
  renderer.render(scene, camera);
836
  }
837
  render();
838
 
 
839
  new ResizeObserver(() => {
840
  camera.aspect = wrapper.clientWidth / wrapper.clientHeight;
841
  camera.updateProjectionMatrix();
842
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
843
  }).observe(wrapper);
844
 
 
845
  wrapper._updateFromProps = (newVal) => {
846
  if (newVal && typeof newVal === 'object') {
847
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
@@ -852,13 +752,16 @@ class LightingControl3D(gr.HTML):
852
 
853
  wrapper._updateTexture = updateTextureFromUrl;
854
 
 
855
  let lastImageUrl = props.imageUrl;
856
  let lastValue = JSON.stringify(props.value);
857
  setInterval(() => {
 
858
  if (props.imageUrl !== lastImageUrl) {
859
  lastImageUrl = props.imageUrl;
860
  updateTextureFromUrl(props.imageUrl);
861
  }
 
862
  const currentValue = JSON.stringify(props.value);
863
  if (currentValue !== lastValue) {
864
  lastValue = currentValue;
@@ -884,14 +787,14 @@ class LightingControl3D(gr.HTML):
884
  )
885
 
886
  css = '''
887
- .col-container { max-width: 1200px; margin: 0 auto; }
888
  .dark .progress-text { color: white !important; }
889
- .lighting-3d-control { min-height: 450px; }
890
  .slider-row { display: flex; gap: 10px; align-items: center; }
891
- .main-title h1 {font-size: 2.4em !important;}
892
  '''
893
  with gr.Blocks(css=css) as demo:
894
- gr.Markdown("**Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_classes=["main-title"])
895
  gr.Markdown("Control lighting directions using the **3D viewport** or **sliders**. Use [Multi-Angle-Lighting](https://huggingface.co/dx8152/Qwen-Edit-2509-Multi-Angle-Lighting) LoRA for precise lighting control, paired with [Rapid-AIO-V19](https://huggingface.co/prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19).")
896
 
897
  with gr.Row():
@@ -903,7 +806,7 @@ with gr.Blocks(css=css) as demo:
903
 
904
  lighting_3d = LightingControl3D(
905
  value={"azimuth": 0, "elevation": 0},
906
- elem_classes=["lighting-3d-control"]
907
  )
908
  run_btn = gr.Button("Generate Image", variant="primary", size="lg")
909
 
@@ -947,10 +850,12 @@ with gr.Blocks(css=css) as demo:
947
  width = gr.Slider(label="Width", minimum=256, maximum=2048, step=8, value=1024)
948
 
949
  def update_prompt_from_sliders(azimuth, elevation):
 
950
  prompt = build_lighting_prompt(azimuth, elevation)
951
  return prompt
952
 
953
  def sync_3d_to_sliders(lighting_value):
 
954
  if lighting_value and isinstance(lighting_value, dict):
955
  az = lighting_value.get('azimuth', 0)
956
  el = lighting_value.get('elevation', 0)
@@ -959,9 +864,11 @@ with gr.Blocks(css=css) as demo:
959
  return gr.update(), gr.update(), gr.update()
960
 
961
  def sync_sliders_to_3d(azimuth, elevation):
 
962
  return {"azimuth": azimuth, "elevation": elevation}
963
 
964
  def update_3d_image(image):
 
965
  if image is None:
966
  return gr.update(imageUrl=None)
967
 
 
15
 
16
  colors.orange_red = colors.Color(
17
  name="orange_red",
18
+ c50="#FFF0E5",
19
+ c100="#FFE0CC",
20
+ c200="#FFC299",
21
+ c300="#FFA366",
22
+ c400="#FF8533",
23
+ c500="#FF4500",
24
+ c600="#E63E00",
25
+ c700="#CC3700",
26
+ c800="#B33000",
27
+ c900="#992900",
28
+ c950="#802200",
29
  )
30
 
31
  class OrangeRedTheme(Soft):
 
127
  }
128
 
129
  def snap_to_nearest(value, options):
130
+ """Snap a value to the nearest option in a list."""
131
  return min(options, key=lambda x: abs(x - value))
132
 
133
  def build_lighting_prompt(azimuth: float, elevation: float) -> str:
134
+ """
135
+ Build a lighting prompt from azimuth and elevation values.
136
+
137
+ Args:
138
+ azimuth: Horizontal rotation in degrees (0-360)
139
+ elevation: Vertical angle in degrees (-90 to 90)
140
+
141
+ Returns:
142
+ Formatted prompt string for the LoRA
143
+ """
144
+ # Snap to nearest valid values
145
  azimuth_snapped = snap_to_nearest(azimuth, list(AZIMUTH_MAP.keys()))
146
  elevation_snapped = snap_to_nearest(elevation, list(ELEVATION_MAP.keys()))
147
 
 
162
  height: int = 1024,
163
  width: int = 1024,
164
  ):
165
+ """
166
+ Edit the lighting of an image using Qwen Image Edit 2511 with multi-angle lighting LoRA.
167
+ """
168
  global loaded
169
  progress = gr.Progress(track_tqdm=True)
170
 
 
198
  return result, seed, prompt
199
 
200
  def update_dimensions_on_upload(image):
201
+ """Compute recommended dimensions preserving aspect ratio."""
202
  if image is None:
203
  return 1024, 1024
204
  original_width, original_height = image.size
 
215
  return new_width, new_height
216
 
217
  class LightingControl3D(gr.HTML):
218
+ """
219
+ A 3D lighting control component using Three.js.
220
+ Outputs: { azimuth: number, elevation: number }
221
+ Accepts imageUrl prop to display user's uploaded image on the plane.
222
+ """
223
  def __init__(self, value=None, imageUrl=None, **kwargs):
224
  if value is None:
225
  value = {"azimuth": 0, "elevation": 0}
226
 
227
  html_template = """
228
+ <div id="lighting-control-wrapper" style="width: 100%; height: 450px; position: relative; background: #1a1a1a; border-radius: 12px; overflow: hidden;">
229
+ <div id="prompt-overlay" style="position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); padding: 8px 16px; border-radius: 8px; font-family: monospace; font-size: 12px; color: #00ff88; white-space: nowrap; z-index: 10;"></div>
230
  </div>
231
  """
232
 
233
  js_on_load = """
234
  (() => {
235
+ const wrapper = element.querySelector('#lighting-control-wrapper');
236
+ const promptOverlay = element.querySelector('#prompt-overlay');
237
 
238
+ // Wait for THREE to load
239
  const initScene = () => {
240
  if (typeof THREE === 'undefined') {
241
  setTimeout(initScene, 100);
242
  return;
243
  }
244
 
245
+ // Scene setup
246
  const scene = new THREE.Scene();
247
  scene.background = new THREE.Color(0x1a1a1a);
248
 
249
+ // Fog for light ray effect
250
+ scene.fog = new THREE.FogExp2(0x1a1a1a, 0.05);
251
+
252
  const camera = new THREE.PerspectiveCamera(50, wrapper.clientWidth / wrapper.clientHeight, 0.1, 1000);
253
  camera.position.set(4.5, 3, 4.5);
254
  camera.lookAt(0, 0.75, 0);
 
260
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
261
  wrapper.insertBefore(renderer.domElement, promptOverlay);
262
 
263
+ // Lighting for the scene
264
  scene.add(new THREE.AmbientLight(0xffffff, 0.1));
265
 
266
+ // Ground plane for shadows
267
  const ground = new THREE.Mesh(
268
  new THREE.PlaneGeometry(10, 10),
269
  new THREE.ShadowMaterial({ opacity: 0.3 })
 
273
  ground.receiveShadow = true;
274
  scene.add(ground);
275
 
276
+ // Grid
277
  scene.add(new THREE.GridHelper(8, 16, 0x333333, 0x222222));
278
 
279
+ // Constants
280
  const CENTER = new THREE.Vector3(0, 0.75, 0);
281
  const BASE_DISTANCE = 2.5;
282
  const AZIMUTH_RADIUS = 2.4;
283
  const ELEVATION_RADIUS = 1.8;
284
 
285
+ // State
286
  let azimuthAngle = props.value?.azimuth || 0;
287
  let elevationAngle = props.value?.elevation || 0;
288
 
289
+ // Mappings
290
  const azimuthSteps = [0, 45, 90, 135, 180, 225, 270, 315];
291
  const elevationSteps = [-90, 0, 90];
292
  const azimuthNames = {
 
300
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
301
  }
302
 
303
+ // Create placeholder texture (smiley face)
304
  function createPlaceholderTexture() {
305
  const canvas = document.createElement('canvas');
306
  canvas.width = 256;
307
  canvas.height = 256;
308
  const ctx = canvas.getContext('2d');
309
+ ctx.fillStyle = '#3a3a4a';
310
  ctx.fillRect(0, 0, 256, 256);
311
+ ctx.fillStyle = '#ffcc99';
312
  ctx.beginPath();
313
  ctx.arc(128, 128, 80, 0, Math.PI * 2);
314
  ctx.fill();
315
+ ctx.fillStyle = '#333';
316
  ctx.beginPath();
317
  ctx.arc(100, 110, 10, 0, Math.PI * 2);
318
  ctx.arc(156, 110, 10, 0, Math.PI * 2);
319
  ctx.fill();
320
+ ctx.strokeStyle = '#333';
321
  ctx.lineWidth = 3;
322
  ctx.beginPath();
323
  ctx.arc(128, 130, 35, 0.2, Math.PI - 0.2);
 
325
  return new THREE.CanvasTexture(canvas);
326
  }
327
 
328
+ // Target image plane
329
  let currentTexture = createPlaceholderTexture();
330
  const planeMaterial = new THREE.MeshStandardMaterial({ map: currentTexture, side: THREE.DoubleSide, roughness: 0.5, metalness: 0 });
331
  let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
 
333
  targetPlane.receiveShadow = true;
334
  scene.add(targetPlane);
335
 
336
+ // Function to update texture from image URL
337
  function updateTextureFromUrl(url) {
338
  if (!url) {
339
+ // Reset to placeholder
340
  planeMaterial.map = createPlaceholderTexture();
341
  planeMaterial.needsUpdate = true;
342
+ // Reset plane to square
343
  scene.remove(targetPlane);
344
  targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
345
  targetPlane.position.copy(CENTER);
 
356
  planeMaterial.map = texture;
357
  planeMaterial.needsUpdate = true;
358
 
359
+ // Adjust plane aspect ratio to match image
360
  const img = texture.image;
361
  if (img && img.width && img.height) {
362
  const aspect = img.width / img.height;
 
383
  });
384
  }
385
 
386
+ // Check for initial imageUrl
387
  if (props.imageUrl) {
388
  updateTextureFromUrl(props.imageUrl);
389
  }
390
 
391
+ // --- NEW LIGHT MODEL: TORCH WITH RAYS ---
392
  const lightGroup = new THREE.Group();
393
+
394
+ // 1. Torch Handle (Dark Metal Cylinder)
395
+ const handleGeo = new THREE.CylinderGeometry(0.06, 0.06, 0.5, 16);
396
+ const torchMat = new THREE.MeshStandardMaterial({
397
+ color: 0x222222,
398
+ roughness: 0.4,
399
+ metalness: 0.8
400
  });
401
+ const handle = new THREE.Mesh(handleGeo, torchMat);
402
  handle.rotation.x = Math.PI / 2;
403
+ handle.position.z = 0.25;
404
  lightGroup.add(handle);
405
+
406
+ // 2. Torch Head (Flared Bezel)
407
+ const headGeo = new THREE.CylinderGeometry(0.12, 0.07, 0.2, 32);
408
+ const head = new THREE.Mesh(headGeo, torchMat);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  head.rotation.x = Math.PI / 2;
410
+ head.position.z = -0.1;
411
  lightGroup.add(head);
412
+
413
+ // 3. Bulb/Lens (Bright Emissive Circle)
414
+ const lensGeo = new THREE.CircleGeometry(0.1, 32);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  const lensMat = new THREE.MeshStandardMaterial({
416
+ color: 0xffffff,
417
+ emissive: 0xffffff,
418
+ emissiveIntensity: 3.0
 
 
419
  });
420
  const lens = new THREE.Mesh(lensGeo, lensMat);
421
+ lens.position.z = -0.201; // Slightly in front of head
422
  lightGroup.add(lens);
423
+
424
+ // 4. Light Rays (Volumetric Cone)
425
+ const beamGeo = new THREE.ConeGeometry(0.5, 3.0, 32, 1, true); // Open ended cone
426
+ const beamMat = new THREE.MeshBasicMaterial({
 
 
 
 
 
 
 
 
 
 
 
427
  color: 0xffffee,
428
  transparent: true,
 
 
 
 
 
 
 
 
 
 
 
 
429
  opacity: 0.12,
430
+ side: THREE.DoubleSide,
431
+ depthWrite: false, // Prevents z-fighting transparency issues
432
+ blending: THREE.AdditiveBlending // Glow effect
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  });
434
+ const beam = new THREE.Mesh(beamGeo, beamMat);
435
+ beam.rotation.x = -Math.PI / 2; // Point forward
436
+ beam.position.z = -1.7; // Offset by half height + lens pos
437
+ lightGroup.add(beam);
438
+
439
+ // Actual Light Source (Invisible logic)
440
+ const spotLight = new THREE.SpotLight(0xffffff, 10, 10, Math.PI / 6, 0.5, 1);
441
+ spotLight.position.set(0, 0, -0.05);
442
  spotLight.castShadow = true;
443
  spotLight.shadow.mapSize.width = 1024;
444
  spotLight.shadow.mapSize.height = 1024;
 
454
 
455
  scene.add(lightGroup);
456
 
457
+ // --- CONTROLS: COLORS UPDATED ---
458
+
459
+ // Azimuth ring (YELLOW)
460
  const azimuthRing = new THREE.Mesh(
461
  new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.04, 16, 64),
462
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.3 })
 
465
  azimuthRing.position.y = 0.05;
466
  scene.add(azimuthRing);
467
 
468
+ // Azimuth Handle (YELLOW)
469
  const azimuthHandle = new THREE.Mesh(
470
  new THREE.SphereGeometry(0.18, 16, 16),
471
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 })
 
473
  azimuthHandle.userData.type = 'azimuth';
474
  scene.add(azimuthHandle);
475
 
476
+ // Elevation arc (BLUE)
477
  const arcPoints = [];
478
  for (let i = 0; i <= 32; i++) {
479
  const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 32));
 
486
  );
487
  scene.add(elevationArc);
488
 
489
+ // Elevation Handle (BLUE)
490
  const elevationHandle = new THREE.Mesh(
491
  new THREE.SphereGeometry(0.18, 16, 16),
492
  new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.5 })
 
494
  elevationHandle.userData.type = 'elevation';
495
  scene.add(elevationHandle);
496
 
497
+ // --- REFRESH BUTTON (Redesigned) ---
498
  const refreshBtn = document.createElement('button');
499
  refreshBtn.innerHTML = 'Reset View';
500
  refreshBtn.style.position = 'absolute';
501
  refreshBtn.style.top = '15px';
502
  refreshBtn.style.right = '15px';
503
+ refreshBtn.style.background = '#e63e00'; // Theme primary color
504
+ refreshBtn.style.color = '#fff';
505
  refreshBtn.style.border = 'none';
506
  refreshBtn.style.padding = '8px 16px';
507
  refreshBtn.style.borderRadius = '6px';
 
513
  refreshBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
514
  refreshBtn.style.transition = 'background 0.2s';
515
 
516
+ refreshBtn.onmouseover = () => refreshBtn.style.background = '#ff5722';
517
+ refreshBtn.onmouseout = () => refreshBtn.style.background = '#e63e00';
518
 
519
  wrapper.appendChild(refreshBtn);
520
 
 
525
  updatePropsAndTrigger();
526
  });
527
 
 
 
528
  function updatePositions() {
529
  const distance = BASE_DISTANCE;
530
  const azRad = THREE.MathUtils.degToRad(azimuthAngle);
 
540
  azimuthHandle.position.set(AZIMUTH_RADIUS * Math.sin(azRad), 0.05, AZIMUTH_RADIUS * Math.cos(azRad));
541
  elevationHandle.position.set(-0.8, ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y, ELEVATION_RADIUS * Math.cos(elRad));
542
 
543
+ // Update prompt
544
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
545
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
546
  let prompt = 'Light source from';
 
560
  trigger('change', props.value);
561
  }
562
 
563
+ // Raycasting
564
  const raycaster = new THREE.Raycaster();
565
  const mouse = new THREE.Vector2();
566
  let isDragging = false;
 
633
  dragTarget.material.emissiveIntensity = 0.5;
634
  dragTarget.scale.setScalar(1);
635
 
636
+ // Snap and animate
637
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
638
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
639
 
 
666
 
667
  canvas.addEventListener('mouseup', onMouseUp);
668
  canvas.addEventListener('mouseleave', onMouseUp);
669
+ // Touch support for mobile
670
  canvas.addEventListener('touchstart', (e) => {
671
  e.preventDefault();
672
  const touch = e.touches[0];
 
724
  onMouseUp();
725
  }, { passive: false });
726
 
727
+ // Initial update
728
  updatePositions();
729
 
730
+ // Render loop
731
  function render() {
732
  requestAnimationFrame(render);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  renderer.render(scene, camera);
734
  }
735
  render();
736
 
737
+ // Handle resize
738
  new ResizeObserver(() => {
739
  camera.aspect = wrapper.clientWidth / wrapper.clientHeight;
740
  camera.updateProjectionMatrix();
741
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
742
  }).observe(wrapper);
743
 
744
+ // Store update functions for external calls
745
  wrapper._updateFromProps = (newVal) => {
746
  if (newVal && typeof newVal === 'object') {
747
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
 
752
 
753
  wrapper._updateTexture = updateTextureFromUrl;
754
 
755
+ // Watch for prop changes (imageUrl and value)
756
  let lastImageUrl = props.imageUrl;
757
  let lastValue = JSON.stringify(props.value);
758
  setInterval(() => {
759
+ // Check imageUrl changes
760
  if (props.imageUrl !== lastImageUrl) {
761
  lastImageUrl = props.imageUrl;
762
  updateTextureFromUrl(props.imageUrl);
763
  }
764
+ // Check value changes (from sliders)
765
  const currentValue = JSON.stringify(props.value);
766
  if (currentValue !== lastValue) {
767
  lastValue = currentValue;
 
787
  )
788
 
789
  css = '''
790
+ #col-container { max-width: 1200px; margin: 0 auto; }
791
  .dark .progress-text { color: white !important; }
792
+ #lighting-3d-control { min-height: 450px; }
793
  .slider-row { display: flex; gap: 10px; align-items: center; }
794
+ #main-title h1 {font-size: 2.4em !important;}
795
  '''
796
  with gr.Blocks(css=css) as demo:
797
+ gr.Markdown("# **Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_id="main-title")
798
  gr.Markdown("Control lighting directions using the **3D viewport** or **sliders**. Use [Multi-Angle-Lighting](https://huggingface.co/dx8152/Qwen-Edit-2509-Multi-Angle-Lighting) LoRA for precise lighting control, paired with [Rapid-AIO-V19](https://huggingface.co/prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19).")
799
 
800
  with gr.Row():
 
806
 
807
  lighting_3d = LightingControl3D(
808
  value={"azimuth": 0, "elevation": 0},
809
+ elem_id="lighting-3d-control"
810
  )
811
  run_btn = gr.Button("Generate Image", variant="primary", size="lg")
812
 
 
850
  width = gr.Slider(label="Width", minimum=256, maximum=2048, step=8, value=1024)
851
 
852
  def update_prompt_from_sliders(azimuth, elevation):
853
+ """Update prompt preview when sliders change."""
854
  prompt = build_lighting_prompt(azimuth, elevation)
855
  return prompt
856
 
857
  def sync_3d_to_sliders(lighting_value):
858
+ """Sync 3D control changes to sliders."""
859
  if lighting_value and isinstance(lighting_value, dict):
860
  az = lighting_value.get('azimuth', 0)
861
  el = lighting_value.get('elevation', 0)
 
864
  return gr.update(), gr.update(), gr.update()
865
 
866
  def sync_sliders_to_3d(azimuth, elevation):
867
+ """Sync slider changes to 3D control."""
868
  return {"azimuth": azimuth, "elevation": elevation}
869
 
870
  def update_3d_image(image):
871
+ """Update the 3D component with the uploaded image."""
872
  if image is None:
873
  return gr.update(imageUrl=None)
874