comrender commited on
Commit
4e3b77d
·
verified ·
1 Parent(s): 857d418

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -329
app.py CHANGED
@@ -6,22 +6,91 @@ import gradio as gr
6
  import numpy as np
7
  import spaces
8
  import torch
9
- from diffusers import FluxImg2ImgPipeline
10
  from gradio_imageslider import ImageSlider
11
  from PIL import Image
12
- from huggingface_hub import snapshot_download
13
  import requests
14
- from transformers import T5TokenizerFast
15
-
16
- # For ESRGAN (requires pip install basicsr gfpgan)
17
- try:
18
- from basicsr.archs.rrdbnet_arch import RRDBNet
19
- from basicsr.utils import img2tensor, tensor2img
20
- USE_ESRGAN = True
21
- except ImportError:
22
- USE_ESRGAN = False
23
- warnings.warn("basicsr not installed; falling back to LANCZOS interpolation.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
 
25
  css = """
26
  #col-container {
27
  margin: 0 auto;
@@ -33,127 +102,37 @@ css = """
33
  }
34
  """
35
 
36
- # Device setup - Default to CPU, let runtime handle GPU
37
  power_device = "ZeroGPU"
38
- device = "cpu"
39
-
40
- # Get HuggingFace token
41
- huggingface_token = os.getenv("HF_TOKEN")
42
-
43
  MAX_SEED = 1000000
44
- MAX_PIXEL_BUDGET = 8192 * 8192 # Increased for tiling support
45
-
46
 
47
  def make_divisible_by_16(size):
48
- """Adjust size to nearest multiple of 16, stretching if necessary"""
49
  return ((size // 16) * 16) if (size % 16) < 8 else ((size // 16 + 1) * 16)
50
 
51
-
52
  def process_input(input_image, upscale_factor):
53
- """Process input image and handle size constraints"""
54
  w, h = input_image.size
55
  w_original, h_original = w, h
56
- aspect_ratio = w / h
57
 
58
  was_resized = False
59
 
60
  if w * h * upscale_factor**2 > MAX_PIXEL_BUDGET:
61
- warnings.warn(
62
- f"Requested output image is too large ({w * upscale_factor}x{h * upscale_factor}). Resizing to fit budget."
63
- )
64
- gr.Info(
65
- f"Requested output image is too large. Resizing input to fit within pixel budget."
66
- )
67
  target_input_pixels = MAX_PIXEL_BUDGET / (upscale_factor ** 2)
68
  scale = (target_input_pixels / (w * h)) ** 0.5
69
- new_w = int(w * scale) // 16 * 16 # Ensure divisible by 16 for Flux compatibility
70
- new_h = int(h * scale) // 16 * 16
71
- if new_w == 0 or new_h == 0:
72
- new_w = max(16, new_w)
73
- new_h = max(16, new_h)
74
  input_image = input_image.resize((new_w, new_h), resample=Image.LANCZOS)
75
  was_resized = True
76
 
77
  return input_image, w_original, h_original, was_resized
78
 
79
-
80
  def load_image_from_url(url):
81
- """Load image from URL"""
82
  try:
83
  response = requests.get(url, stream=True)
84
  response.raise_for_status()
85
  return Image.open(response.raw)
86
  except Exception as e:
87
- raise gr.Error(f"Failed to load image from URL: {e}")
88
-
89
-
90
- def esrgan_upscale(image, scale=4):
91
- if not USE_ESRGAN:
92
- return image.resize((image.width * scale, image.height * scale), resample=Image.LANCZOS)
93
- img = img2tensor(np.array(image) / 255., bgr2rgb=False, float32=True)
94
- with torch.no_grad():
95
- output = esrgan_model(img.unsqueeze(0)).squeeze()
96
- output_img = tensor2img(output, rgb2bgr=False, min_max=(0, 1))
97
- return Image.fromarray(output_img)
98
-
99
-
100
- def tiled_flux_img2img(pipe, prompt, image, strength, steps, guidance, generator, tile_size=1024, overlap=32):
101
- """Tiled Img2Img to mimic Ultimate SD Upscaler tiling"""
102
- w, h = image.size
103
- output = image.copy() # Start with the control image
104
-
105
- for x in range(0, w, tile_size - overlap):
106
- for y in range(0, h, tile_size - overlap):
107
- tile_w = min(tile_size, w - x)
108
- tile_h = min(tile_size, h - y)
109
- if tile_h < 16 or tile_w < 16: # Skip tiny tiles
110
- continue
111
- tile = image.crop((x, y, x + tile_w, y + tile_h))
112
-
113
- # Force tile to div by 16
114
- new_tile_w = make_divisible_by_16(tile_w)
115
- new_tile_h = make_divisible_by_16(tile_h)
116
- tile = tile.resize((new_tile_w, new_tile_h), resample=Image.LANCZOS)
117
-
118
- # Run Flux on tile
119
- gen_tile = pipe(
120
- prompt=prompt,
121
- image=tile,
122
- strength=strength,
123
- num_inference_steps=steps,
124
- guidance_scale=guidance,
125
- height=new_tile_h,
126
- width=new_tile_w,
127
- generator=generator,
128
- ).images[0]
129
-
130
- # Resize gen_tile back to original tile dimensions
131
- gen_tile = gen_tile.resize((tile_w, tile_h), resample=Image.LANCZOS)
132
-
133
- # Paste with blending if overlap
134
- if overlap > 0:
135
- paste_box = (x, y, x + tile_w, y + tile_h)
136
- if x > 0 or y > 0:
137
- # Simple linear blend on overlaps
138
- mask = Image.new('L', (tile_w, tile_h), 255)
139
- effective_overlap_x = min(overlap, tile_w)
140
- effective_overlap_y = min(overlap, tile_h)
141
- if x > 0:
142
- for i in range(effective_overlap_x):
143
- for j in range(tile_h):
144
- mask.putpixel((i, j), int(255 * (i / overlap)))
145
- if y > 0:
146
- for i in range(tile_w):
147
- for j in range(effective_overlap_y):
148
- mask.putpixel((i, j), int(255 * (j / overlap)))
149
- output.paste(gen_tile, paste_box, mask)
150
- else:
151
- output.paste(gen_tile, paste_box)
152
- else:
153
- output.paste(gen_tile, (x, y))
154
-
155
- return output
156
-
157
 
158
  @spaces.GPU(duration=120)
159
  def enhance_image(
@@ -168,139 +147,119 @@ def enhance_image(
168
  tile_size,
169
  progress=gr.Progress(track_tqdm=True),
170
  ):
171
- """Main enhancement function"""
172
- # Lazy loading of models
173
- global pipe, esrgan_model
174
- if 'pipe' not in globals():
175
- try:
176
- device = "cuda" if torch.cuda.is_available() else "cpu"
177
- dtype = torch.bfloat16 if device == "cuda" else torch.float32
178
-
179
- print(f"📥 Loading FLUX Img2Img on {device}...")
180
- tokenizer_2 = T5TokenizerFast.from_pretrained("black-forest-labs/FLUX.1-schnell", subfolder="tokenizer_2", token=huggingface_token)
181
- pipe = FluxImg2ImgPipeline.from_pretrained(
182
- "black-forest-labs/FLUX.1-schnell",
183
- torch_dtype=dtype,
184
- low_cpu_mem_usage=True,
185
- device_map="balanced",
186
- tokenizer_2=tokenizer_2,
187
- token=huggingface_token
188
- )
189
- pipe.enable_vae_tiling()
190
- pipe.enable_vae_slicing()
191
- if device == "cuda":
192
- pipe.reset_device_map()
193
- pipe.enable_model_cpu_offload()
194
-
195
- if USE_ESRGAN:
196
- esrgan_path = "4x-UltraSharp.pth"
197
- if not os.path.exists(esrgan_path):
198
- url = "https://huggingface.co/uwg/upscaler/resolve/main/ESRGAN/4x-UltraSharp.pth"
199
- with open(esrgan_path, "wb") as f:
200
- f.write(requests.get(url).content)
201
- esrgan_model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4)
202
- state_dict = torch.load(esrgan_path)['params_ema']
203
- esrgan_model.load_state_dict(state_dict)
204
- esrgan_model.eval()
205
- esrgan_model.to(device)
206
-
207
- print("✅ Models loaded successfully!")
208
- except Exception as e:
209
- print(f"Model loading error: {e}, falling back to CPU")
210
- device = "cpu"
211
- dtype = torch.float32
212
- # Reload on CPU if needed
213
- tokenizer_2 = T5TokenizerFast.from_pretrained("black-forest-labs/FLUX.1-schnell", subfolder="tokenizer_2", token=huggingface_token)
214
- pipe = FluxImg2ImgPipeline.from_pretrained(
215
- "black-forest-labs/FLUX.1-schnell",
216
- torch_dtype=dtype,
217
- low_cpu_mem_usage=True,
218
- device_map=None,
219
- tokenizer_2=tokenizer_2,
220
- token=huggingface_token
221
- )
222
- pipe.enable_vae_tiling()
223
- pipe.enable_vae_slicing()
224
-
225
- # Handle image input
226
- if image_input is not None:
227
- input_image = image_input
228
- elif image_url:
229
- input_image = load_image_from_url(image_url)
230
- else:
231
- raise gr.Error("Please provide an image (upload or URL)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
- if randomize_seed:
234
- seed = random.randint(0, MAX_SEED)
235
 
236
- true_input_image = input_image
237
-
238
- # Process input image
239
- input_image, w_original, h_original, was_resized = process_input(
240
- input_image, upscale_factor
241
- )
242
 
243
- prompt = custom_prompt if custom_prompt.strip() else ""
244
-
245
- generator = torch.Generator(device=device).manual_seed(seed)
246
-
247
- gr.Info("🚀 Upscaling image...")
248
-
249
- # Initial upscale
250
- if USE_ESRGAN and upscale_factor == 4:
251
- control_image = esrgan_upscale(input_image, upscale_factor)
252
- else:
253
- w, h = input_image.size
254
- control_image = input_image.resize((w * upscale_factor, h * upscale_factor), resample=Image.LANCZOS)
255
-
256
- # Resize control_image to divisible by 16 (stretching)
257
- control_w, control_h = control_image.size
258
- new_control_w = make_divisible_by_16(control_w)
259
- new_control_h = make_divisible_by_16(control_h)
260
- if (new_control_w, new_control_h) != (control_w, control_h):
261
- control_image = control_image.resize((new_control_w, new_control_h), resample=Image.LANCZOS)
262
-
263
- # Tiled Flux Img2Img for refinement
264
- image = tiled_flux_img2img(
265
- pipe,
266
- prompt,
267
- control_image,
268
- denoising_strength,
269
- num_inference_steps,
270
- 3.5, # Updated guidance_scale to match workflow (3.5)
271
- generator,
272
- tile_size=tile_size,
273
- overlap=32
274
- )
275
 
276
- # Resize back to original target size if stretched
277
- target_w, target_h = w_original * upscale_factor, h_original * upscale_factor
278
- if image.size != (target_w, target_h):
279
- image = image.resize((target_w, target_h), resample=Image.LANCZOS)
280
 
281
- if was_resized:
282
- gr.Info(f"📏 Resizing output to target size: {target_w}x{target_h}")
283
- image = image.resize((target_w, target_h), resample=Image.LANCZOS)
284
-
285
- # Resize input image to match output size for slider alignment
286
- resized_input = true_input_image.resize(image.size, resample=Image.LANCZOS)
287
-
288
- # Move back to CPU to release GPU if possible
289
- if device == "cuda":
290
- pipe.to("cpu")
291
- if USE_ESRGAN:
292
- esrgan_model.to("cpu")
293
-
294
- return [resized_input, image]
295
 
 
296
 
297
- # Create Gradio interface
298
- with gr.Blocks(css=css, title="🎨 AI Image Upscaler - FLUX") as demo:
299
  gr.HTML("""
300
  <div class="main-header">
301
- <h1>🎨 AI Image Upscaler</h1>
302
- <p>Upload an image or provide a URL to upscale it using FLUX upscaling</p>
303
- <p>Currently running on <strong>{}</strong></p>
304
  </div>
305
  """.format(power_device))
306
 
@@ -310,11 +269,7 @@ with gr.Blocks(css=css, title="🎨 AI Image Upscaler - FLUX") as demo:
310
 
311
  with gr.Tabs():
312
  with gr.TabItem("📁 Upload Image"):
313
- input_image = gr.Image(
314
- label="Upload Image",
315
- type="pil",
316
- height=200 # Made smaller
317
- )
318
 
319
  with gr.TabItem("🔗 Image URL"):
320
  image_url = gr.Textbox(
@@ -338,17 +293,15 @@ with gr.Blocks(css=css, title="🎨 AI Image Upscaler - FLUX") as demo:
338
  minimum=1,
339
  maximum=4,
340
  step=1,
341
- value=2,
342
- info="How much to upscale the image"
343
  )
344
 
345
  num_inference_steps = gr.Slider(
346
- label="Number of Inference Steps",
347
  minimum=1,
348
  maximum=50,
349
  step=1,
350
- value=4,
351
- info="More steps = better quality but slower (default 4 for schnell)"
352
  )
353
 
354
  denoising_strength = gr.Slider(
@@ -356,8 +309,7 @@ with gr.Blocks(css=css, title="🎨 AI Image Upscaler - FLUX") as demo:
356
  minimum=0.0,
357
  maximum=1.0,
358
  step=0.05,
359
- value=0.3,
360
- info="Controls how much the image is transformed"
361
  )
362
 
363
  tile_size = gr.Slider(
@@ -365,42 +317,20 @@ with gr.Blocks(css=css, title="🎨 AI Image Upscaler - FLUX") as demo:
365
  minimum=256,
366
  maximum=2048,
367
  step=64,
368
- value=1024,
369
- info="Size of tiles for processing (larger = faster but more memory)"
370
  )
371
 
372
  with gr.Row():
373
- randomize_seed = gr.Checkbox(
374
- label="Randomize seed",
375
- value=True
376
- )
377
- seed = gr.Slider(
378
- label="Seed",
379
- minimum=0,
380
- maximum=MAX_SEED,
381
- step=1,
382
- value=42,
383
- interactive=True
384
- )
385
 
386
- enhance_btn = gr.Button(
387
- "🚀 Upscale Image",
388
- variant="primary",
389
- size="lg"
390
- )
391
 
392
- with gr.Column(scale=2): # Larger scale for results
393
  gr.HTML("<h3>📊 Results</h3>")
394
 
395
- result_slider = ImageSlider(
396
- type="pil",
397
- interactive=False, # Disable interactivity to prevent uploads
398
- height=600, # Made larger
399
- elem_id="result_slider",
400
- label=None # Remove default label
401
- )
402
 
403
- # Event handler
404
  enhance_btn.click(
405
  fn=enhance_image,
406
  inputs=[
@@ -419,76 +349,26 @@ with gr.Blocks(css=css, title="🎨 AI Image Upscaler - FLUX") as demo:
419
 
420
  gr.HTML("""
421
  <div style="margin-top: 2rem; padding: 1rem; background: #f0f0f0; border-radius: 8px;">
422
- <p><strong>Note:</strong> This upscaler uses the Flux.1-schnell model. Users are responsible for obtaining commercial rights if used commercially under their license.</p>
423
  </div>
424
  """)
425
 
426
- # Custom CSS for slider
427
  gr.HTML("""
428
  <style>
429
- #result_slider .slider {
430
- width: 100% !important;
431
- max-width: inherit !important;
432
- }
433
- #result_slider img {
434
- object-fit: contain !important;
435
- width: 100% !important;
436
- height: auto !important;
437
- }
438
- #result_slider .gr-button-tool {
439
- display: none !important;
440
- }
441
- #result_slider .gr-button-undo {
442
- display: none !important;
443
- }
444
- #result_slider .gr-button-clear {
445
- display: none !important;
446
- }
447
- #result_slider .badge-container .badge {
448
- display: none !important;
449
- }
450
- #result_slider .badge-container::before {
451
- content: "Before";
452
- position: absolute;
453
- top: 10px;
454
- left: 10px;
455
- background: rgba(0,0,0,0.5);
456
- color: white;
457
- padding: 5px;
458
- border-radius: 5px;
459
- z-index: 10;
460
- }
461
- #result_slider .badge-container::after {
462
- content: "After";
463
- position: absolute;
464
- top: 10px;
465
- right: 10px;
466
- background: rgba(0,0,0,0.5);
467
- color: white;
468
- padding: 5px;
469
- border-radius: 5px;
470
- z-index: 10;
471
- }
472
- #result_slider .fullscreen img {
473
- object-fit: contain !important;
474
- width: 100vw !important;
475
- height: 100vh !important;
476
- position: absolute;
477
- top: 0;
478
- left: 0;
479
- }
480
  </style>
481
  """)
482
 
483
- # JS to set slider default position to middle
484
  gr.HTML("""
485
  <script>
486
  document.addEventListener('DOMContentLoaded', function() {
487
  const sliderInput = document.querySelector('#result_slider input[type="range"]');
488
- if (sliderInput) {
489
- sliderInput.value = 50;
490
- sliderInput.dispatchEvent(new Event('input'));
491
- }
492
  });
493
  </script>
494
  """)
 
6
  import numpy as np
7
  import spaces
8
  import torch
 
9
  from gradio_imageslider import ImageSlider
10
  from PIL import Image
 
11
  import requests
12
+ import sys
13
+ import subprocess
14
+ from huggingface_hub import hf_hub_download
15
+ import tempfile
16
+
17
+ # Setup ComfyUI and custom nodes
18
+ if not os.path.exists("ComfyUI"):
19
+ subprocess.run(["git", "clone", "https://github.com/comfyanonymous/ComfyUI"])
20
+
21
+ custom_nodes_dir = os.path.join("ComfyUI", "custom_nodes")
22
+ os.makedirs(custom_nodes_dir, exist_ok=True)
23
+
24
+ # Clone UltimateSDUpscaler
25
+ usd_dir = os.path.join(custom_nodes_dir, "ComfyUI_UltimateSDUpscaler")
26
+ if not os.path.exists(usd_dir):
27
+ subprocess.run(["git", "clone", "https://github.com/ssitu/ComfyUI_UltimateSDUpscaler", usd_dir])
28
+
29
+ # Clone comfy_mtb
30
+ mtb_dir = os.path.join(custom_nodes_dir, "comfy_mtb")
31
+ if not os.path.exists(mtb_dir):
32
+ subprocess.run(["git", "clone", "https://github.com/melMass/comfy_mtb", mtb_dir])
33
+ # Install requirements
34
+ if os.path.exists(os.path.join(mtb_dir, "requirements.txt")):
35
+ subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=mtb_dir)
36
+
37
+ # Clone KJNodes
38
+ kjn_dir = os.path.join(custom_nodes_dir, "ComfyUI-KJNodes")
39
+ if not os.path.exists(kjn_dir):
40
+ subprocess.run(["git", "clone", "https://github.com/kijai/ComfyUI-KJNodes", kjn_dir])
41
+ # Install requirements
42
+ if os.path.exists(os.path.join(kjn_dir, "requirements.txt")):
43
+ subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=kjn_dir)
44
+
45
+ # Download models if not present
46
+ comfy_models_dir = os.path.join("ComfyUI", "models")
47
+ os.makedirs(comfy_models_dir, exist_ok=True)
48
+
49
+ # UNET (Flux FP8)
50
+ unet_dir = os.path.join(comfy_models_dir, "unet")
51
+ os.makedirs(unet_dir, exist_ok=True)
52
+ if not os.path.exists(os.path.join(unet_dir, "flux1-dev-fp8.safetensors")):
53
+ hf_hub_download(repo_id="Kijai/flux-fp8", filename="flux1-dev-fp8.safetensors", local_dir=unet_dir)
54
+
55
+ # CLIP models
56
+ clip_dir = os.path.join(comfy_models_dir, "clip")
57
+ os.makedirs(clip_dir, exist_ok=True)
58
+ if not os.path.exists(os.path.join(clip_dir, "clip_l.safetensors")):
59
+ hf_hub_download(repo_id="comfyanonymous/flux_text_encoders", filename="clip_l.safetensors", local_dir=clip_dir)
60
+ if not os.path.exists(os.path.join(clip_dir, "t5xxl_fp8_e4m3fn.safetensors")):
61
+ hf_hub_download(repo_id="comfyanonymous/flux_text_encoders", filename="t5xxl_fp8_e4m3fn.safetensors", local_dir=clip_dir)
62
+
63
+ # VAE
64
+ vae_dir = os.path.join(comfy_models_dir, "vae")
65
+ os.makedirs(vae_dir, exist_ok=True)
66
+ if not os.path.exists(os.path.join(vae_dir, "ae.safetensors")):
67
+ hf_hub_download(repo_id="black-forest-labs/FLUX.1-dev", filename="ae.safetensors", subfolder="vae", local_dir=vae_dir)
68
+
69
+ # Upscale models
70
+ upscale_dir = os.path.join(comfy_models_dir, "upscale_models")
71
+ os.makedirs(upscale_dir, exist_ok=True)
72
+ for model_name in ["RealESRGAN_x2.pth", "RealESRGAN_x4.pth"]:
73
+ model_path = os.path.join(upscale_dir, model_name)
74
+ if not os.path.exists(model_path):
75
+ url = f"https://huggingface.co/ai-forever/Real-ESRGAN/resolve/main/{model_name}"
76
+ with open(model_path, "wb") as f:
77
+ f.write(requests.get(url).content)
78
+
79
+ # Add ComfyUI to sys.path
80
+ sys.path.append(os.path.abspath("ComfyUI"))
81
+
82
+ # Import custom nodes
83
+ from nodes import NODE_CLASS_MAPPINGS, init_custom_nodes
84
+ init_custom_nodes()
85
+
86
+ # From the provided script
87
+ def get_value_at_index(obj, index):
88
+ try:
89
+ return obj[index]
90
+ except KeyError:
91
+ return obj["result"][index]
92
 
93
+ # CSS and constants similar to original
94
  css = """
95
  #col-container {
96
  margin: 0 auto;
 
102
  }
103
  """
104
 
 
105
  power_device = "ZeroGPU"
 
 
 
 
 
106
  MAX_SEED = 1000000
107
+ MAX_PIXEL_BUDGET = 8192 * 8192
 
108
 
109
  def make_divisible_by_16(size):
 
110
  return ((size // 16) * 16) if (size % 16) < 8 else ((size // 16 + 1) * 16)
111
 
 
112
  def process_input(input_image, upscale_factor):
 
113
  w, h = input_image.size
114
  w_original, h_original = w, h
 
115
 
116
  was_resized = False
117
 
118
  if w * h * upscale_factor**2 > MAX_PIXEL_BUDGET:
119
+ gr.Info("Requested output too large. Resizing input.")
 
 
 
 
 
120
  target_input_pixels = MAX_PIXEL_BUDGET / (upscale_factor ** 2)
121
  scale = (target_input_pixels / (w * h)) ** 0.5
122
+ new_w = max(16, int(w * scale) // 16 * 16)
123
+ new_h = max(16, int(h * scale) // 16 * 16)
 
 
 
124
  input_image = input_image.resize((new_w, new_h), resample=Image.LANCZOS)
125
  was_resized = True
126
 
127
  return input_image, w_original, h_original, was_resized
128
 
 
129
  def load_image_from_url(url):
 
130
  try:
131
  response = requests.get(url, stream=True)
132
  response.raise_for_status()
133
  return Image.open(response.raw)
134
  except Exception as e:
135
+ raise gr.Error(f"Failed to load image: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  @spaces.GPU(duration=120)
138
  def enhance_image(
 
147
  tile_size,
148
  progress=gr.Progress(track_tqdm=True),
149
  ):
150
+ with torch.inference_mode():
151
+ # Handle input image
152
+ if image_input is not None:
153
+ true_input_image = image_input
154
+ elif image_url:
155
+ true_input_image = load_image_from_url(image_url)
156
+ else:
157
+ raise gr.Error("Provide an image or URL")
158
+
159
+ input_image, w_original, h_original, was_resized = process_input(true_input_image, upscale_factor)
160
+
161
+ if randomize_seed:
162
+ seed = random.randint(0, MAX_SEED)
163
+
164
+ # Prepare ComfyUI input image
165
+ input_dir = os.path.join("ComfyUI", "input")
166
+ os.makedirs(input_dir, exist_ok=True)
167
+ temp_filename = f"input_{random.randint(0, 1000000)}.png"
168
+ input_path = os.path.join(input_dir, temp_filename)
169
+ input_image.save(input_path)
170
+
171
+ # Nodes
172
+ load_image_node = NODE_CLASS_MAPPINGS["LoadImage"]()
173
+ image_loaded = load_image_node.load_image(image=temp_filename)
174
+ image = get_value_at_index(image_loaded, 0)
175
+
176
+ text_multiline = NODE_CLASS_MAPPINGS["Text Multiline"]()
177
+ text_out = text_multiline.text_multiline(text=custom_prompt if custom_prompt.strip() else "")
178
+ prompt_text = get_value_at_index(text_out, 0)
179
+
180
+ dualcliploader = NODE_CLASS_MAPPINGS["DualCLIPLoader"]()
181
+ clip_out = dualcliploader.load_clip(
182
+ clip_name1="clip_l.safetensors",
183
+ clip_name2="t5xxl_fp8_e4m3fn.safetensors",
184
+ type="flux",
185
+ )
186
+ clip = get_value_at_index(clip_out, 0)
187
+
188
+ cliptextencode = NODE_CLASS_MAPPINGS["CLIPTextEncode"]()
189
+ conditioning = get_value_at_index(cliptextencode.encode(text=prompt_text, clip=clip), 0)
190
+
191
+ fluxguidance = NODE_CLASS_MAPPINGS["FluxGuidance"]()
192
+ positive_out = fluxguidance.append(guidance=3.5, conditioning=conditioning) # Using 3.5 as in original app
193
+ positive = get_value_at_index(positive_out, 0)
194
+
195
+ conditioningzeroout = NODE_CLASS_MAPPINGS["ConditioningZeroOut"]()
196
+ negative_out = conditioningzeroout.zero_out(conditioning=conditioning)
197
+ negative = get_value_at_index(negative_out, 0)
198
+
199
+ upscale_name = "RealESRGAN_x2.pth" if upscale_factor == 2 else "RealESRGAN_x4.pth"
200
+ upscalemodelloader = NODE_CLASS_MAPPINGS["UpscaleModelLoader"]()
201
+ upscale_model = get_value_at_index(upscalemodelloader.load_model(model_name=upscale_name), 0)
202
+
203
+ vaeloader = NODE_CLASS_MAPPINGS["VAELoader"]()
204
+ vae = get_value_at_index(vaeloader.load_vae(vae_name="ae.safetensors"), 0)
205
+
206
+ unetloader = NODE_CLASS_MAPPINGS["UNETLoader"]()
207
+ model = get_value_at_index(unetloader.load_unet(unet_name="flux1-dev-fp8.safetensors", weight_dtype="fp8_e4m3fn"), 0)
208
+
209
+ ultimatesdupscale = NODE_CLASS_MAPPINGS["UltimateSDUpscale"]()
210
+ upscale_out = ultimatesdupscale.upscale(
211
+ upscale_by=float(upscale_factor),
212
+ seed=seed,
213
+ steps=num_inference_steps,
214
+ cfg=1.0,
215
+ sampler_name="euler",
216
+ scheduler="normal",
217
+ denoise=denoising_strength,
218
+ mode_type="Linear",
219
+ tile_width=tile_size,
220
+ tile_height=tile_size,
221
+ mask_blur=8,
222
+ tile_padding=32,
223
+ seam_fix_mode="None",
224
+ seam_fix_denoise=1.0,
225
+ seam_fix_width=64,
226
+ seam_fix_mask_blur=8,
227
+ seam_fix_padding=16,
228
+ force_uniform_tiles=True,
229
+ tiled_decode=False,
230
+ image=image,
231
+ model=model,
232
+ positive=positive,
233
+ negative=negative,
234
+ vae=vae,
235
+ upscale_model=upscale_model,
236
+ )
237
+ upscaled_tensor = get_value_at_index(upscale_out, 0)
238
 
239
+ # Convert to PIL
240
+ upscaled_img = Image.fromarray((upscaled_tensor[0].cpu().numpy() * 255).astype(np.uint8))
241
 
242
+ target_w, target_h = w_original * upscale_factor, h_original * upscale_factor
243
+ if upscaled_img.size != (target_w, target_h):
244
+ upscaled_img = upscaled_img.resize((target_w, target_h), resample=Image.LANCZOS)
 
 
 
245
 
246
+ if was_resized:
247
+ upscaled_img = upscaled_img.resize((target_w, target_h), resample=Image.LANCZOS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
+ resized_input = true_input_image.resize(upscaled_img.size, resample=Image.LANCZOS)
 
 
 
250
 
251
+ # Cleanup temp file
252
+ os.remove(input_path)
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ return [resized_input, upscaled_img]
255
 
256
+ # Gradio interface similar to original
257
+ with gr.Blocks(css=css, title="🎨 AI Image Upscaler - Flux FP8") as demo:
258
  gr.HTML("""
259
  <div class="main-header">
260
+ <h1>🎨 AI Image Upscaler - Flux FP8</h1>
261
+ <p>Upscale images using Flux FP8 with ComfyUI workflow</p>
262
+ <p>Running on <strong>{}</strong></p>
263
  </div>
264
  """.format(power_device))
265
 
 
269
 
270
  with gr.Tabs():
271
  with gr.TabItem("📁 Upload Image"):
272
+ input_image = gr.Image(label="Upload Image", type="pil", height=200)
 
 
 
 
273
 
274
  with gr.TabItem("🔗 Image URL"):
275
  image_url = gr.Textbox(
 
293
  minimum=1,
294
  maximum=4,
295
  step=1,
296
+ value=2
 
297
  )
298
 
299
  num_inference_steps = gr.Slider(
300
+ label="Inference Steps",
301
  minimum=1,
302
  maximum=50,
303
  step=1,
304
+ value=25
 
305
  )
306
 
307
  denoising_strength = gr.Slider(
 
309
  minimum=0.0,
310
  maximum=1.0,
311
  step=0.05,
312
+ value=0.3
 
313
  )
314
 
315
  tile_size = gr.Slider(
 
317
  minimum=256,
318
  maximum=2048,
319
  step=64,
320
+ value=1024
 
321
  )
322
 
323
  with gr.Row():
324
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
325
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42)
 
 
 
 
 
 
 
 
 
 
326
 
327
+ enhance_btn = gr.Button("🚀 Upscale Image", variant="primary", size="lg")
 
 
 
 
328
 
329
+ with gr.Column(scale=2):
330
  gr.HTML("<h3>📊 Results</h3>")
331
 
332
+ result_slider = ImageSlider(type="pil", interactive=False, height=600, label=None)
 
 
 
 
 
 
333
 
 
334
  enhance_btn.click(
335
  fn=enhance_image,
336
  inputs=[
 
349
 
350
  gr.HTML("""
351
  <div style="margin-top: 2rem; padding: 1rem; background: #f0f0f0; border-radius: 8px;">
352
+ <p><strong>Note:</strong> Uses Flux FP8 model. Ensure compliance with licenses for commercial use.</p>
353
  </div>
354
  """)
355
 
 
356
  gr.HTML("""
357
  <style>
358
+ #result_slider .slider { width: 100% !important; }
359
+ #result_slider img { object-fit: contain !important; width: 100% !important; height: auto !important; }
360
+ #result_slider .gr-button-tool, #result_slider .gr-button-undo, #result_slider .gr-button-clear { display: none !important; }
361
+ #result_slider .badge-container .badge { display: none !important; }
362
+ #result_slider .badge-container::before { content: "Before"; position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.5); color: white; padding: 5px; border-radius: 5px; z-index: 10; }
363
+ #result_slider .badge-container::after { content: "After"; position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.5); color: white; padding: 5px; border-radius: 5px; z-index: 10; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  </style>
365
  """)
366
 
 
367
  gr.HTML("""
368
  <script>
369
  document.addEventListener('DOMContentLoaded', function() {
370
  const sliderInput = document.querySelector('#result_slider input[type="range"]');
371
+ if (sliderInput) { sliderInput.value = 50; sliderInput.dispatchEvent(new Event('input')); }
 
 
 
372
  });
373
  </script>
374
  """)