prithivMLmods commited on
Commit
e759422
·
verified ·
1 Parent(s): 6cd4709

update app

Browse files
Files changed (1) hide show
  1. app.py +365 -550
app.py CHANGED
@@ -1,27 +1,96 @@
 
 
1
  import gradio as gr
2
  import numpy as np
3
- import random
4
- import torch
5
  import spaces
 
 
6
  from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- # --- Imports ---
9
- from diffusers import FlowMatchEulerDiscreteScheduler
10
- try:
11
- from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
12
- from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
13
- from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
14
- except ImportError:
15
- # Fallback/Instruction if custom packages are missing
16
- raise ImportError("Please ensure the 'qwenimage' package is installed.")
17
 
18
- MAX_SEED = np.iinfo(np.int32).max
 
 
 
 
 
 
 
 
 
19
 
20
- # --- Configuration & Model Loading ---
21
  dtype = torch.bfloat16
22
- device = "cuda" if torch.cuda.is_available() else "cpu"
23
 
24
- # 1. Load the Pipeline
25
  pipe = QwenImageEditPlusPipeline.from_pretrained(
26
  "Qwen/Qwen-Image-Edit-2511",
27
  transformer=QwenImageTransformer2DModel.from_pretrained(
@@ -32,576 +101,322 @@ pipe = QwenImageEditPlusPipeline.from_pretrained(
32
  torch_dtype=dtype
33
  ).to(device)
34
 
35
- # 2. Set Flash Attention 3 (if available)
36
  try:
37
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
38
  print("Flash Attention 3 Processor set successfully.")
39
  except Exception as e:
40
  print(f"Warning: Could not set FA3 processor: {e}")
41
 
42
- # 3. Adapter Specs (Lighting LoRA)
 
43
  ADAPTER_SPECS = {
44
- "Multi-Angle-Lighting": {
45
- "repo": "dx8152/Qwen-Edit-2509-Multi-Angle-Lighting",
46
- "weights": "多角度灯光-251116.safetensors",
47
- "adapter_name": "multi-angle-lighting"
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
- # Global state to track currently loaded adapter
52
- CURRENT_LOADED_ADAPTER = None
53
-
54
- # --- Logic: Mappings & Prompt Building ---
55
-
56
- # Lighting mappings for Azimuth (Horizontal)
57
- # 0 = Front, moving clockwise
58
- LIGHTING_AZIMUTH_MAP = {
59
- 0: "Light source from the Front",
60
- 45: "Light source from the Right Front",
61
- 90: "Light source from the Right",
62
- 135: "Light source from the Right Rear",
63
- 180: "Light source from the Rear",
64
- 225: "Light source from the Left Rear",
65
- 270: "Light source from the Left",
66
- 315: "Light source from the Left Front"
67
- }
68
 
69
- def snap_to_nearest_key(value, keys):
70
- """Finds the nearest key in a list of numbers."""
71
- return min(keys, key=lambda x: abs(x - value))
72
-
73
- def build_lighting_prompt(azimuth: float, elevation: float) -> str:
74
- """
75
- Constructs the specific text prompt required by the LoRA.
76
- Logic:
77
- 1. Prioritize Vertical Extremes (>60° or <-60°)
78
- 2. Fallback to Horizontal Azimuth mappings
79
- """
80
- # 1. Vertical Extremes
81
- if elevation >= 60:
82
- return "Light source from Above"
83
- if elevation <= -60:
84
- return "Light source from Below"
85
-
86
- # 2. Horizontal Snap
87
- keys = list(LIGHTING_AZIMUTH_MAP.keys())
88
- # Handle the 360 wrap-around for "Front" (0 vs 360)
89
- # If azimuth is > 337.5, it snaps to 0
90
- if azimuth > 337.5:
91
- azimuth = 0
92
 
93
- azimuth_snapped = snap_to_nearest_key(azimuth, keys)
94
- return LIGHTING_AZIMUTH_MAP[azimuth_snapped]
95
-
96
- # --- Inference Function ---
97
 
98
  @spaces.GPU
99
- def infer_lighting_edit(
100
- image: Image.Image,
101
- azimuth: float = 0.0,
102
- elevation: float = 0.0,
103
- seed: int = 0,
104
- randomize_seed: bool = True,
105
- guidance_scale: float = 5.0,
106
- num_inference_steps: int = 4,
107
- height: int = 1024,
108
- width: int = 1024,
109
  ):
110
- global CURRENT_LOADED_ADAPTER
111
-
112
- # 1. Lazy Load Adapter
113
- spec = ADAPTER_SPECS["Multi-Angle-Lighting"]
114
- if CURRENT_LOADED_ADAPTER != spec["adapter_name"]:
115
- print(f"⚙️ Lazy loading adapter: {spec['adapter_name']}...")
116
- pipe.load_lora_weights(
117
- spec["repo"],
118
- weight_name=spec["weights"],
119
- adapter_name=spec["adapter_name"]
120
- )
121
- pipe.set_adapters([spec["adapter_name"]], adapter_weights=[1.0])
122
- CURRENT_LOADED_ADAPTER = spec["adapter_name"]
123
-
124
- # 2. Build Prompt
125
- prompt = build_lighting_prompt(azimuth, elevation)
126
- print(f"💡 Generated Prompt: {prompt}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
- # 3. Prepare Inputs
129
- if image is None:
130
- raise gr.Error("Please upload an image first.")
131
 
132
  if randomize_seed:
133
  seed = random.randint(0, MAX_SEED)
134
- generator = torch.Generator(device=device).manual_seed(seed)
135
-
136
- pil_image = image.convert("RGB")
137
 
138
- # 4. Run Inference
139
- result = pipe(
140
- image=[pil_image],
141
- prompt=prompt,
142
- height=height,
143
- width=width,
144
- num_inference_steps=num_inference_steps,
145
- generator=generator,
146
- guidance_scale=guidance_scale,
147
- num_images_per_prompt=1,
148
- ).images[0]
 
 
 
 
 
 
 
149
 
150
- return result, seed, prompt
 
 
 
 
151
 
152
- def update_dimensions_on_upload(image):
153
- """Resizes image to nearest multiple of 8, max 1024, preserving aspect ratio."""
154
- if image is None:
155
- return 1024, 1024
156
- w, h = image.size
157
 
158
- # Constraint: Max dimension 1024
159
- if w > h:
160
- new_w = 1024
161
- new_h = int(new_w * (h / w))
162
  else:
163
- new_h = 1024
164
- new_w = int(new_h * (w / h))
165
 
166
- # Constraint: Multiple of 8
167
- new_w = (new_w // 8) * 8
168
- new_h = (new_h // 8) * 8
169
-
170
- return new_w, new_h
 
 
 
 
 
171
 
172
- # --- Enhanced 3D Component ---
 
 
 
 
 
 
173
 
174
- class LightControl3D(gr.HTML):
175
- """
176
- Advanced 3D Light Controller using Three.js.
177
- Features: Hemisphere guide, Beam visualization, Dynamic color feedback.
178
- """
179
- def __init__(self, value=None, imageUrl=None, **kwargs):
180
- if value is None: value = {"azimuth": 0, "elevation": 0}
181
-
182
- # HTML Container
183
- html_template = """
184
- <div id="light-control-wrapper" style="width: 100%; height: 500px; position: relative; background: radial-gradient(circle at center, #1a1a1a 0%, #000000 100%); border-radius: 12px; overflow: hidden; border: 1px solid #333; box-shadow: inset 0 0 20px #000;">
185
- <div id="prompt-badge" style="position: absolute; top: 15px; left: 50%; transform: translateX(-50%);
186
- background: rgba(0,0,0,0.8); border: 1px solid #FFD700; color: #FFD700;
187
- padding: 8px 24px; border-radius: 30px; font-family: monospace; font-weight: bold; font-size: 14px;
188
- z-index: 10; pointer-events: none; transition: all 0.2s ease;">
189
- Light Source: Front
190
- </div>
191
-
192
- <div style="position: absolute; bottom: 15px; right: 15px; color: #555; font-size: 10px; font-family: sans-serif; pointer-events: none;">
193
- Drag to rotate • Scroll to zoom
194
- </div>
195
- </div>
196
- """
197
-
198
- # JavaScript Logic
199
- js_on_load = """
200
- (() => {
201
- const wrapper = element.querySelector('#light-control-wrapper');
202
- const badge = element.querySelector('#prompt-badge');
203
-
204
- const initScene = () => {
205
- if (typeof THREE === 'undefined') { setTimeout(initScene, 100); return; }
206
-
207
- // --- 1. Scene & Camera ---
208
- const scene = new THREE.Scene();
209
- // No background color set here, letting CSS gradient show through
210
-
211
- const camera = new THREE.PerspectiveCamera(45, wrapper.clientWidth / wrapper.clientHeight, 0.1, 1000);
212
- camera.position.set(4, 3, 4); // Isometric-ish view
213
- camera.lookAt(0, 0.5, 0);
214
-
215
- const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
216
- renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
217
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
218
- wrapper.appendChild(renderer.domElement);
219
-
220
- // --- 2. Helpers (Grid & Dome) ---
221
- const CENTER = new THREE.Vector3(0, 0.75, 0);
222
- const RADIUS = 2.5;
223
-
224
- // Floor Grid
225
- const grid = new THREE.GridHelper(6, 12, 0x444444, 0x111111);
226
- scene.add(grid);
227
-
228
- // Hemisphere Guide (Wireframe Dome)
229
- const domeGeo = new THREE.SphereGeometry(RADIUS, 16, 8, 0, Math.PI * 2, 0, Math.PI * 0.5);
230
- const domeMat = new THREE.MeshBasicMaterial({ color: 0x333333, wireframe: true, transparent: true, opacity: 0.15 });
231
- const dome = new THREE.Mesh(domeGeo, domeMat);
232
- dome.position.y = CENTER.y - 0.75; // Ground the dome
233
- scene.add(dome);
234
-
235
- // Elevation Rings (Visual guides for 0, 45, 60 degrees)
236
- const ringMat = new THREE.MeshBasicMaterial({ color: 0x555555, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
237
- const eqRing = new THREE.Mesh(new THREE.TorusGeometry(RADIUS, 0.01, 8, 64), ringMat);
238
- eqRing.rotation.x = Math.PI / 2;
239
- eqRing.position.y = CENTER.y;
240
- scene.add(eqRing);
241
-
242
- // --- 3. The Subject (Image Plane) ---
243
- let planeMesh;
244
- const planeMat = new THREE.MeshBasicMaterial({ color: 0x222222, side: THREE.DoubleSide });
245
-
246
- function createPlane(width=1.2, height=1.2) {
247
- if(planeMesh) scene.remove(planeMesh);
248
- planeMesh = new THREE.Mesh(new THREE.PlaneGeometry(width, height), planeMat);
249
- planeMesh.position.copy(CENTER);
250
- planeMesh.lookAt(camera.position); // Billboarding slightly? No, fixed upright.
251
- planeMesh.rotation.set(0,0,0); // Reset rotation
252
- scene.add(planeMesh);
253
- }
254
- createPlane();
255
-
256
- // Texture Loader
257
- function updateTexture(url) {
258
- if (!url) {
259
- planeMat.map = null;
260
- planeMat.needsUpdate = true;
261
- return;
262
- }
263
- new THREE.TextureLoader().load(url, (tex) => {
264
- planeMat.map = tex;
265
- planeMat.needsUpdate = true;
266
- // Adjust Aspect Ratio
267
- const img = tex.image;
268
- if(img && img.width && img.height) {
269
- const aspect = img.width / img.height;
270
- const size = 1.4; // Max dimension
271
- if (aspect > 1) createPlane(size, size/aspect);
272
- else createPlane(size*aspect, size);
273
- }
274
- });
275
- }
276
- if (props.imageUrl) updateTexture(props.imageUrl);
277
-
278
- // --- 4. The Light Gizmo (Interactive) ---
279
- const lightGroup = new THREE.Group();
280
- scene.add(lightGroup);
281
-
282
- // The Orb
283
- const orb = new THREE.Mesh(
284
- new THREE.SphereGeometry(0.2, 32, 32),
285
- new THREE.MeshBasicMaterial({ color: 0xFFD700 })
286
- );
287
-
288
- // The Glow
289
- const glow = new THREE.Mesh(
290
- new THREE.SphereGeometry(0.35, 32, 32),
291
- new THREE.MeshBasicMaterial({ color: 0xFFD700, transparent: true, opacity: 0.4 })
292
- );
293
- orb.add(glow);
294
- lightGroup.add(orb);
295
-
296
- // The Beam (Cone)
297
- const beamGeo = new THREE.ConeGeometry(0.4, RADIUS, 32, 1, true);
298
- beamGeo.translate(0, -RADIUS/2, 0); // Pivot at base
299
- beamGeo.rotateX(-Math.PI / 2); // Point along Z
300
- const beamMat = new THREE.MeshBasicMaterial({
301
- color: 0xFFD700,
302
- transparent: true,
303
- opacity: 0.15,
304
- side: THREE.DoubleSide,
305
- depthWrite: false,
306
- blending: THREE.AdditiveBlending
307
- });
308
- const beam = new THREE.Mesh(beamGeo, beamMat);
309
- beam.lookAt(CENTER); // This will need dynamic updating
310
- lightGroup.add(beam);
311
-
312
- // --- 5. State & Logic ---
313
- let az = props.value?.azimuth || 0;
314
- let el = props.value?.elevation || 0;
315
-
316
- const AZ_MAP = {
317
- 0: 'Front', 45: 'Right Front', 90: 'Right', 135: 'Right Rear',
318
- 180: 'Rear', 225: 'Left Rear', 270: 'Left', 315: 'Left Front'
319
- };
320
-
321
- function getPrompt(a, e) {
322
- if (e >= 60) return "Light source from Above";
323
- if (e <= -60) return "Light source from Below";
324
- // Snap
325
- const steps = [0,45,90,135,180,225,270,315];
326
- // Handle wrapped 360
327
- let normalized = a % 360;
328
- if(normalized < 0) normalized += 360;
329
- const snapped = steps.reduce((p, c) => Math.abs(c-normalized) < Math.abs(p-normalized) ? c : p);
330
- return `Light source from the ${AZ_MAP[snapped]}`;
331
- }
332
-
333
- function updateGizmo() {
334
- const r_az = THREE.MathUtils.degToRad(az);
335
- const r_el = THREE.MathUtils.degToRad(el);
336
-
337
- // Orbit Calculation
338
- const x = RADIUS * Math.sin(r_az) * Math.cos(r_el);
339
- const y = RADIUS * Math.sin(r_el) + CENTER.y;
340
- const z = RADIUS * Math.cos(r_az) * Math.cos(r_el);
341
-
342
- lightGroup.position.set(x, y, z);
343
- lightGroup.lookAt(CENTER); // Points the Beam at center
344
-
345
- // UI Updates
346
- const text = getPrompt(az, el);
347
- badge.innerText = text;
348
-
349
- // Color Logic (Warning for Above/Below)
350
- let mainColor = 0xFFD700; // Gold
351
- if (el >= 60 || el <= -60) mainColor = 0xFF4500; // OrangeRed
352
-
353
- orb.material.color.setHex(mainColor);
354
- glow.material.color.setHex(mainColor);
355
- beam.material.color.setHex(mainColor);
356
- badge.style.borderColor = '#' + new THREE.Color(mainColor).getHexString();
357
- badge.style.color = '#' + new THREE.Color(mainColor).getHexString();
358
- }
359
-
360
- // --- 6. Interaction (Drag) ---
361
- const raycaster = new THREE.Raycaster();
362
- const mouse = new THREE.Vector2();
363
- let isDragging = false;
364
 
365
- // Invisible Drag Sphere (Larger hit area)
366
- const dragSphere = new THREE.Mesh(
367
- new THREE.SphereGeometry(RADIUS, 32, 16),
368
- new THREE.MeshBasicMaterial({ visible: false, side: THREE.DoubleSide })
369
- );
370
- dragSphere.position.copy(CENTER);
371
- scene.add(dragSphere);
372
-
373
- function getMouse(e) {
374
- const rect = wrapper.getBoundingClientRect();
375
- const clientX = e.clientX || (e.touches ? e.touches[0].clientX : 0);
376
- const clientY = e.clientY || (e.touches ? e.touches[0].clientY : 0);
377
- return {
378
- x: ((clientX - rect.left) / rect.width) * 2 - 1,
379
- y: -((clientY - rect.top) / rect.height) * 2 + 1
380
- };
381
- }
382
-
383
- function onDown(e) {
384
- const m = getMouse(e);
385
- raycaster.setFromCamera(m, camera);
386
- // Check if clicked near the light orb
387
- const intersects = raycaster.intersectObject(dragSphere);
388
- if(intersects.length > 0) {
389
- // Check distance to current light pos to prevent jumping if clicked far away
390
- if (intersects[0].point.distanceTo(lightGroup.position) < 1.0) {
391
- isDragging = true;
392
- wrapper.style.cursor = 'none'; // Hide cursor while dragging for immersion
393
- }
394
- }
395
- }
396
-
397
- function onMove(e) {
398
- if (!isDragging) {
399
- // Hover state
400
- const m = getMouse(e);
401
- raycaster.setFromCamera(m, camera);
402
- const hits = raycaster.intersectObject(dragSphere);
403
- if (hits.length > 0 && hits[0].point.distanceTo(lightGroup.position) < 0.8) {
404
- wrapper.style.cursor = 'pointer';
405
- } else {
406
- wrapper.style.cursor = 'default';
407
- }
408
- return;
409
- }
410
-
411
- const m = getMouse(e);
412
- raycaster.setFromCamera(m, camera);
413
- const intersects = raycaster.intersectObject(dragSphere);
414
-
415
- if (intersects.length > 0) {
416
- const p = intersects[0].point;
417
- const rel = new THREE.Vector3().subVectors(p, CENTER);
418
-
419
- // Convert Cartesian to Spherical (Azimuth/Elevation)
420
- let newAz = Math.atan2(rel.x, rel.z) * (180 / Math.PI);
421
- if (newAz < 0) newAz += 360;
422
-
423
- const distXZ = Math.sqrt(rel.x*rel.x + rel.z*rel.z);
424
- let newEl = Math.atan2(rel.y, distXZ) * (180 / Math.PI);
425
-
426
- // Limits
427
- newEl = Math.max(-89, Math.min(89, newEl));
428
-
429
- az = newAz;
430
- el = newEl;
431
- updateGizmo();
432
- }
433
- }
434
-
435
- function onUp() {
436
- if(isDragging) {
437
- isDragging = false;
438
- wrapper.style.cursor = 'default';
439
- // Propagate value back to Gradio
440
- props.value = { azimuth: az, elevation: el };
441
- trigger('change', props.value);
442
- }
443
- }
444
-
445
- // Event Listeners
446
- wrapper.addEventListener('mousedown', onDown);
447
- window.addEventListener('mousemove', onMove);
448
- window.addEventListener('mouseup', onUp);
449
- wrapper.addEventListener('touchstart', onDown, {passive: false});
450
- window.addEventListener('touchmove', onMove, {passive: false});
451
- window.addEventListener('touchend', onUp);
452
-
453
- // --- 7. Loop & Watchers ---
454
- updateGizmo(); // Init
455
-
456
- function animate() {
457
- requestAnimationFrame(animate);
458
- // Subtle idle animation for the glow
459
- glow.scale.setScalar(1 + Math.sin(Date.now() * 0.003) * 0.1);
460
- renderer.render(scene, camera);
461
- }
462
- animate();
463
-
464
- // Watch for changes from Python/Sliders
465
- setInterval(() => {
466
- // Texture change
467
- if (props.imageUrl && (!planeMat.map || props.imageUrl !== planeMat.map.image.src)) {
468
- // handled by dedicated updater usually, but fail-safe
469
- }
470
- // Value change
471
- if (props.value && !isDragging) {
472
- if (Math.abs(props.value.azimuth - az) > 0.1 || Math.abs(props.value.elevation - el) > 0.1) {
473
- az = props.value.azimuth;
474
- el = props.value.elevation;
475
- updateGizmo();
476
- }
477
- }
478
- }, 100);
479
-
480
- // Expose updater
481
- wrapper._updateTexture = updateTexture;
482
- };
483
- initScene();
484
- })();
485
- """
486
-
487
- super().__init__(
488
- value=value,
489
- html_template=html_template,
490
- js_on_load=js_on_load,
491
- imageUrl=imageUrl,
492
- **kwargs
493
- )
494
 
495
- # --- UI Layout ---
496
 
497
- css = """
498
- #col-container { max-width: 1200px; margin: 0 auto; }
499
- #3d-container { border: 1px solid #333; border-radius: 12px; overflow: hidden; }
500
- .range-slider { accent-color: #FFD700 !important; }
501
- """
502
-
503
- with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="yellow")) as demo:
504
- gr.Markdown("""
505
- # 💡 Qwen Edit 2509 — 3D Lighting Studio
506
-
507
- **Interactive Relighting:** Drag the ☀️ Sun in the 3D Viewport to change the lighting direction.
508
- """)
509
-
510
- with gr.Row():
511
- # --- Left Column: Controls ---
512
- with gr.Column(scale=5):
513
- # Input
514
- image_input = gr.Image(label="Input Image", type="pil", height=320)
515
-
516
- gr.Markdown("### 🎮 3D Controller")
517
- light_controller = LightControl3D(
518
- value={"azimuth": 0, "elevation": 0},
519
- elem_id="3d-container"
520
- )
521
-
522
- # Action
523
- run_btn = gr.Button("✨ Generate Lighting", variant="primary", size="lg")
524
-
525
- # Fine Tuning
526
- with gr.Accordion("🎚️ Fine-Tune & Advanced", open=False):
527
- with gr.Row():
528
- az_slider = gr.Slider(0, 359, value=0, label="Azimuth", step=1)
529
- el_slider = gr.Slider(-90, 90, value=0, label="Elevation", step=1)
530
 
531
  with gr.Row():
532
- seed = gr.Slider(0, MAX_SEED, value=42, label="Seed", step=1)
533
- randomize_seed = gr.Checkbox(True, label="Randomize")
 
 
 
534
 
535
- with gr.Row():
536
- cfg = gr.Slider(1.0, 10.0, value=5.0, label="Guidance (CFG)")
537
- steps = gr.Slider(1, 20, value=4, step=1, label="Steps")
538
-
539
- prompt_display = gr.Textbox(label="Actual Prompt sent to Model", interactive=False)
540
-
541
- # --- Right Column: Output ---
542
- with gr.Column(scale=4):
543
- result_output = gr.Image(label="Result", height=600)
544
-
545
- # --- wiring ---
546
-
547
- # 1. Sync 3D -> Sliders & Text
548
- def on_3d_change(val):
549
- az = val.get('azimuth', 0)
550
- el = val.get('elevation', 0)
551
- prompt = build_lighting_prompt(az, el)
552
- return az, el, prompt
553
-
554
- light_controller.change(
555
- on_3d_change,
556
- inputs=[light_controller],
557
- outputs=[az_slider, el_slider, prompt_display]
558
- )
559
-
560
- # 2. Sync Sliders -> 3D & Text
561
- def on_slider_change(az, el):
562
- prompt = build_lighting_prompt(az, el)
563
- return {"azimuth": az, "elevation": el}, prompt
564
 
565
- az_slider.change(on_slider_change, inputs=[az_slider, el_slider], outputs=[light_controller, prompt_display])
566
- el_slider.change(on_slider_change, inputs=[az_slider, el_slider], outputs=[light_controller, prompt_display])
567
-
568
- # 3. Handle Image Upload (Resize + Update 3D Texture)
569
- def on_upload(img):
570
- w, h = update_dimensions_on_upload(img)
571
- if img is None:
572
- return w, h, gr.update(imageUrl=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
574
- # Convert to Base64 for Three.js
575
- import base64
576
- from io import BytesIO
577
- buffered = BytesIO()
578
- img.save(buffered, format="PNG")
579
- img_str = base64.b64encode(buffered.getvalue()).decode()
580
- data_url = f"data:image/png;base64,{img_str}"
581
- return w, h, gr.update(imageUrl=data_url)
582
-
583
- image_input.upload(
584
- on_upload,
585
- inputs=[image_input],
586
- outputs=[gr.State(), gr.State(), light_controller] # We store W/H in state mostly, or just pass to infer
587
- ).then(
588
- # Pass W/H to hidden sliders or just recalc in infer for simplicity
589
- None, None, None
590
- )
591
-
592
- # 4. Generate
593
- def run_inference_wrapper(img, az, el, seed, rand, cfg, steps):
594
- w, h = update_dimensions_on_upload(img) # Recalc dims here for safety
595
- res, used_seed, p = infer_lighting_edit(img, az, el, seed, rand, cfg, steps, h, w)
596
- return res
597
 
598
- run_btn.click(
599
- run_inference_wrapper,
600
- inputs=[image_input, az_slider, el_slider, seed, randomize_seed, cfg, steps],
601
- outputs=[result_output]
602
  )
603
 
604
  if __name__ == "__main__":
605
- # CDN Load Three.js
606
- head_js = '<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>'
607
- demo.launch(head=head_js)
 
1
+ import os
2
+ import gc
3
  import gradio as gr
4
  import numpy as np
 
 
5
  import spaces
6
+ import torch
7
+ import random
8
  from PIL import Image
9
+ from typing import Iterable
10
+ from gradio.themes import Soft
11
+ from gradio.themes.utils import colors, fonts, sizes
12
+
13
+ colors.orange_red = colors.Color(
14
+ name="orange_red",
15
+ c50="#FFF0E5",
16
+ c100="#FFE0CC",
17
+ c200="#FFC299",
18
+ c300="#FFA366",
19
+ c400="#FF8533",
20
+ c500="#FF4500",
21
+ c600="#E63E00",
22
+ c700="#CC3700",
23
+ c800="#B33000",
24
+ c900="#992900",
25
+ c950="#802200",
26
+ )
27
+
28
+ class OrangeRedTheme(Soft):
29
+ def __init__(
30
+ self,
31
+ *,
32
+ primary_hue: colors.Color | str = colors.gray,
33
+ secondary_hue: colors.Color | str = colors.orange_red,
34
+ neutral_hue: colors.Color | str = colors.slate,
35
+ text_size: sizes.Size | str = sizes.text_lg,
36
+ font: fonts.Font | str | Iterable[fonts.Font | str] = (
37
+ fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
38
+ ),
39
+ font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
40
+ fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
41
+ ),
42
+ ):
43
+ super().__init__(
44
+ primary_hue=primary_hue,
45
+ secondary_hue=secondary_hue,
46
+ neutral_hue=neutral_hue,
47
+ text_size=text_size,
48
+ font=font,
49
+ font_mono=font_mono,
50
+ )
51
+ super().set(
52
+ background_fill_primary="*primary_50",
53
+ background_fill_primary_dark="*primary_900",
54
+ body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
55
+ body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
56
+ button_primary_text_color="white",
57
+ button_primary_text_color_hover="white",
58
+ button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
59
+ button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
60
+ button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
61
+ button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
62
+ button_secondary_text_color="black",
63
+ button_secondary_text_color_hover="white",
64
+ button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
65
+ button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
66
+ button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
67
+ button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
68
+ slider_color="*secondary_500",
69
+ slider_color_dark="*secondary_600",
70
+ block_title_text_weight="600",
71
+ block_border_width="3px",
72
+ block_shadow="*shadow_drop_lg",
73
+ button_primary_shadow="*shadow_drop_lg",
74
+ button_large_padding="11px",
75
+ color_accent_soft="*primary_100",
76
+ block_label_background_fill="*primary_200",
77
+ )
78
 
79
+ orange_red_theme = OrangeRedTheme()
 
 
 
 
 
 
 
 
80
 
81
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
82
+
83
+ print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
84
+ print("torch.__version__ =", torch.__version__)
85
+ print("Using device:", device)
86
+
87
+ from diffusers import FlowMatchEulerDiscreteScheduler
88
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
89
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
90
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
91
 
 
92
  dtype = torch.bfloat16
 
93
 
 
94
  pipe = QwenImageEditPlusPipeline.from_pretrained(
95
  "Qwen/Qwen-Image-Edit-2511",
96
  transformer=QwenImageTransformer2DModel.from_pretrained(
 
101
  torch_dtype=dtype
102
  ).to(device)
103
 
 
104
  try:
105
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
106
  print("Flash Attention 3 Processor set successfully.")
107
  except Exception as e:
108
  print(f"Warning: Could not set FA3 processor: {e}")
109
 
110
+ MAX_SEED = np.iinfo(np.int32).max
111
+
112
  ADAPTER_SPECS = {
113
+ "Multiple-Angles": {
114
+ "repo": "dx8152/Qwen-Edit-2509-Multiple-angles",
115
+ "weights": "镜头转换.safetensors",
116
+ "adapter_name": "multiple-angles"
117
+ },
118
+ "Photo-to-Anime": {
119
+ "repo": "autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime",
120
+ "weights": "Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors",
121
+ "adapter_name": "photo-to-anime"
122
+ },
123
+ "Anime-V2": {
124
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Anime",
125
+ "weights": "Qwen-Image-Edit-2511-Anime-2000.safetensors",
126
+ "adapter_name": "anime-v2"
127
+ },
128
+ "Light-Migration": {
129
+ "repo": "dx8152/Qwen-Edit-2509-Light-Migration",
130
+ "weights": "参考色调.safetensors",
131
+ "adapter_name": "light-migration"
132
+ },
133
+ "Upscaler": {
134
+ "repo": "starsfriday/Qwen-Image-Edit-2511-Upscale2K",
135
+ "weights": "qwen_image_edit_2511_upscale.safetensors",
136
+ "adapter_name": "upscale-2k"
137
+ },
138
+ "Style-Transfer": {
139
+ "repo": "zooeyy/Style-Transfer",
140
+ "weights": "Style Transfer-Alpha-V0.1.safetensors",
141
+ "adapter_name": "style-transfer"
142
+ },
143
+ "Manga-Tone": {
144
+ "repo": "nappa114514/Qwen-Image-Edit-2509-Manga-Tone",
145
+ "weights": "tone001.safetensors",
146
+ "adapter_name": "manga-tone"
147
+ },
148
+ "Anything2Real": {
149
+ "repo": "lrzjason/Anything2Real_2601",
150
+ "weights": "anything2real_2601.safetensors",
151
+ "adapter_name": "anything2real"
152
+ },
153
+ "Fal-Multiple-Angles": {
154
+ "repo": "fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA",
155
+ "weights": "qwen-image-edit-2511-multiple-angles-lora.safetensors",
156
+ "adapter_name": "fal-multiple-angles"
157
+ },
158
+ "Polaroid-Photo": {
159
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Polaroid-Photo",
160
+ "weights": "Qwen-Image-Edit-2511-Polaroid-Photo.safetensors",
161
+ "adapter_name": "polaroid-photo"
162
+ },
163
+ "Unblur-Anything": {
164
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Unblur-Upscale",
165
+ "weights": "Qwen-Image-Edit-Unblur-Upscale_15.safetensors",
166
+ "adapter_name": "unblur-anything"
167
+ },
168
+ "Midnight-Noir-Eyes-Spotlight": {
169
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Midnight-Noir-Eyes-Spotlight",
170
+ "weights": "Qwen-Image-Edit-2511-Midnight-Noir-Eyes-Spotlight.safetensors",
171
+ "adapter_name": "midnight-noir-eyes-spotlight"
172
+ },
173
+ "Hyper-Realistic-Portrait": {
174
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Hyper-Realistic-Portrait",
175
+ "weights": "HRP_20.safetensors",
176
+ "adapter_name": "hyper-realistic-portrait"
177
+ },
178
+ "Ultra-Realistic-Portrait": {
179
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Ultra-Realistic-Portrait",
180
+ "weights": "URP_20.safetensors",
181
+ "adapter_name": "ultra-realistic-portrait"
182
+ },
183
+ "Pixar-Inspired-3D": {
184
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Pixar-Inspired-3D",
185
+ "weights": "PI3_20.safetensors",
186
+ "adapter_name": "pi3"
187
+ },
188
+ "Noir-Comic-Book": {
189
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Noir-Comic-Book-Panel",
190
+ "weights": "Noir-Comic-Book-Panel_20.safetensors",
191
+ "adapter_name": "ncb"
192
+ },
193
+ "Any-light": {
194
+ "repo": "lilylilith/QIE-2511-MP-AnyLight",
195
+ "weights": "QIE-2511-AnyLight_.safetensors",
196
+ "adapter_name": "any-light"
197
+ },
198
+ "Studio-DeLight": {
199
+ "repo": "prithivMLmods/QIE-2511-Studio-DeLight",
200
+ "weights": "QIE-2511-Studio-DeLight-5000.safetensors",
201
+ "adapter_name": "studio-delight"
202
+ },
203
  }
204
 
205
+ LOADED_ADAPTERS = set()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
+ def update_dimensions_on_upload(image):
208
+ if image is None:
209
+ return 1024, 1024
210
+
211
+ original_width, original_height = image.size
212
+
213
+ if original_width > original_height:
214
+ new_width = 1024
215
+ aspect_ratio = original_height / original_width
216
+ new_height = int(new_width * aspect_ratio)
217
+ else:
218
+ new_height = 1024
219
+ aspect_ratio = original_width / original_height
220
+ new_width = int(new_height * aspect_ratio)
 
 
 
 
 
 
 
 
 
221
 
222
+ new_width = (new_width // 8) * 8
223
+ new_height = (new_height // 8) * 8
224
+
225
+ return new_width, new_height
226
 
227
  @spaces.GPU
228
+ def infer(
229
+ images,
230
+ prompt,
231
+ lora_adapter,
232
+ seed,
233
+ randomize_seed,
234
+ guidance_scale,
235
+ steps,
236
+ progress=gr.Progress(track_tqdm=True)
 
237
  ):
238
+ gc.collect()
239
+ torch.cuda.empty_cache()
240
+
241
+ if not images:
242
+ raise gr.Error("Please upload at least one image to edit.")
243
+
244
+ pil_images = []
245
+ if images is not None:
246
+ for item in images:
247
+ try:
248
+ if isinstance(item, tuple) or isinstance(item, list):
249
+ path_or_img = item[0]
250
+ else:
251
+ path_or_img = item
252
+
253
+ if isinstance(path_or_img, str):
254
+ pil_images.append(Image.open(path_or_img).convert("RGB"))
255
+ elif isinstance(path_or_img, Image.Image):
256
+ pil_images.append(path_or_img.convert("RGB"))
257
+ else:
258
+ pil_images.append(Image.open(path_or_img.name).convert("RGB"))
259
+ except Exception as e:
260
+ print(f"Skipping invalid image item: {e}")
261
+ continue
262
+
263
+ if not pil_images:
264
+ raise gr.Error("Could not process uploaded images.")
265
+
266
+ spec = ADAPTER_SPECS.get(lora_adapter)
267
+ if not spec:
268
+ raise gr.Error(f"Configuration not found for: {lora_adapter}")
269
+
270
+ adapter_name = spec["adapter_name"]
271
+
272
+ if adapter_name not in LOADED_ADAPTERS:
273
+ print(f"--- Downloading and Loading Adapter: {lora_adapter} ---")
274
+ try:
275
+ pipe.load_lora_weights(
276
+ spec["repo"],
277
+ weight_name=spec["weights"],
278
+ adapter_name=adapter_name
279
+ )
280
+ LOADED_ADAPTERS.add(adapter_name)
281
+ except Exception as e:
282
+ raise gr.Error(f"Failed to load adapter {lora_adapter}: {e}")
283
+ else:
284
+ print(f"--- Adapter {lora_adapter} is already loaded. ---")
285
 
286
+ pipe.set_adapters([adapter_name], adapter_weights=[1.0])
 
 
287
 
288
  if randomize_seed:
289
  seed = random.randint(0, MAX_SEED)
 
 
 
290
 
291
+ generator = torch.Generator(device=device).manual_seed(seed)
292
+ negative_prompt = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
293
+
294
+ width, height = update_dimensions_on_upload(pil_images[0])
295
+
296
+ try:
297
+ result_image = pipe(
298
+ image=pil_images,
299
+ prompt=prompt,
300
+ negative_prompt=negative_prompt,
301
+ height=height,
302
+ width=width,
303
+ num_inference_steps=steps,
304
+ generator=generator,
305
+ true_cfg_scale=guidance_scale,
306
+ ).images[0]
307
+
308
+ return result_image, seed
309
 
310
+ except Exception as e:
311
+ raise e
312
+ finally:
313
+ gc.collect()
314
+ torch.cuda.empty_cache()
315
 
316
+ @spaces.GPU
317
+ def infer_example(images, prompt, lora_adapter):
318
+ if not images:
319
+ return None, 0
 
320
 
321
+ if isinstance(images, str):
322
+ images_list = [images]
 
 
323
  else:
324
+ images_list = images
 
325
 
326
+ result, seed = infer(
327
+ images=images_list,
328
+ prompt=prompt,
329
+ lora_adapter=lora_adapter,
330
+ seed=0,
331
+ randomize_seed=True,
332
+ guidance_scale=1.0,
333
+ steps=4
334
+ )
335
+ return result, seed
336
 
337
+ css="""
338
+ #col-container {
339
+ margin: 0 auto;
340
+ max-width: 1000px;
341
+ }
342
+ #main-title h1 {font-size: 2.3em !important;}
343
+ """
344
 
345
+ with gr.Blocks() as demo:
346
+ with gr.Column(elem_id="col-container"):
347
+ gr.Markdown("# **Qwen-Image-Edit-2511-LoRAs-Fast**", elem_id="main-title")
348
+ gr.Markdown("Perform diverse image edits using specialized [LoRA](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2511) adapters. Open on [GitHub](https://github.com/PRITHIVSAKTHIUR/Qwen-Image-Edit-2511-LoRAs-Fast-Lazy-Load).")
349
+
350
+ with gr.Row(equal_height=True):
351
+ with gr.Column():
352
+ images = gr.Gallery(
353
+ label="Upload Images",
354
+ type="filepath",
355
+ columns=2,
356
+ rows=1,
357
+ height=300,
358
+ allow_preview=True
359
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
+ prompt = gr.Text(
362
+ label="Edit Prompt",
363
+ show_label=True,
364
+ placeholder="e.g., transform into anime..",
365
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ run_button = gr.Button("Edit Image", variant="primary")
368
 
369
+ with gr.Column():
370
+ output_image = gr.Image(label="Output Image", interactive=False, format="png", height=365)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
  with gr.Row():
373
+ lora_adapter = gr.Dropdown(
374
+ label="Choose Editing Style",
375
+ choices=list(ADAPTER_SPECS.keys()),
376
+ value="Photo-to-Anime"
377
+ )
378
 
379
+ with gr.Accordion("Advanced Settings", open=False, visible=False):
380
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
381
+ randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
382
+ guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
383
+ steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
+ gr.Examples(
386
+ examples=[
387
+ [["examples/B.jpg"], "Transform into anime.", "Photo-to-Anime"],
388
+ [["examples/HRP.jpg"], "Transform into a hyper-realistic face portrait.", "Hyper-Realistic-Portrait"],
389
+ [["examples/A.jpeg"], "Rotate the camera 45 degrees to the right.", "Multiple-Angles"],
390
+ [["examples/U.jpg"], "Upscale this picture to 4K resolution.", "Upscaler"],
391
+ [["examples/L1.jpg", "examples/L2.jpg"], "Apply the lighting from image 2 to image 1.", "Any-light"],
392
+ [["examples/PP1.jpg"], "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed by hf‪‪‬ preserving realistic texture and details", "Polaroid-Photo"],
393
+ [["examples/Z1.jpg"], "Front-right quarter view.", "Fal-Multiple-Angles"],
394
+ [["examples/SL.jpg"], "Neutral uniform lighting Preserve identity and composition.", "Studio-DeLight"],
395
+ [["examples/PI.jpg"], "Transform it into Pixar-inspired 3D.", "Pixar-Inspired-3D"],
396
+ [["examples/MT.jpg"], "Paint with manga tone.", "Manga-Tone"],
397
+ [["examples/NCB.jpg"], "Transform into a noir comic book style.", "Noir-Comic-Book"],
398
+ [["examples/URP.jpg"], "ultra-realistic portrait.", "Ultra-Realistic-Portrait"],
399
+ [["examples/MN.jpg"], "Transform into Midnight Noir Eyes Spotlight.", "Midnight-Noir-Eyes-Spotlight"],
400
+ [["examples/ST1.jpg", "examples/ST2.jpg"], "Convert Image 1 to the style of Image 2.", "Style-Transfer"],
401
+ [["examples/R1.jpg"], "Change the picture to realistic photograph.", "Anything2Real"],
402
+ [["examples/UA.jpeg"], "Unblur and upscale.", "Unblur-Anything"],
403
+ [["examples/L1.jpg", "examples/L2.jpg"], "Refer to the color tone, remove the original lighting from Image 1, and relight Image 1 based on the lighting and color tone of Image 2.", "Light-Migration"],
404
+ [["examples/P1.jpg"], "Transform into anime (while preserving the background and remaining elements maintaining realism and original details.)", "Anime-V2"],
405
+ ],
406
+ inputs=[images, prompt, lora_adapter],
407
+ outputs=[output_image, seed],
408
+ fn=infer_example,
409
+ cache_examples=False,
410
+ label="Examples"
411
+ )
412
 
413
+ gr.Markdown("[*](https://huggingface.co/spaces/prithivMLmods/Qwen-Image-Edit-2511-LoRAs-Fast)This is still an experimental Space for Qwen-Image-Edit-2511.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
+ run_button.click(
416
+ fn=infer,
417
+ inputs=[images, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps],
418
+ outputs=[output_image, seed]
419
  )
420
 
421
  if __name__ == "__main__":
422
+ demo.queue(max_size=30).launch(css=css, theme=orange_red_theme, mcp_server=True, ssr_mode=False, show_error=True)