kulkas2pintu commited on
Commit
e40ee9a
Β·
verified Β·
1 Parent(s): 22e3f8c

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +126 -54
app.py CHANGED
@@ -366,81 +366,64 @@ def update_dimensions_on_upload(image, target_long=1024):
366
  return max(16, (nw // 16) * 16), max(16, (nh // 16) * 16)
367
 
368
 
369
- # duration=120 gives headroom so high-res (1536-2048) runs don't hit the default
370
- # ~60s ZeroGPU wall limit ("GPU task aborted"). Billing is on ACTUAL seconds, so a
371
- # fast 1024 run still costs little; this only raises the admission reservation.
372
- @spaces.GPU(size="xlarge", duration=120)
373
- def infer(
374
- images_b64_json,
375
- prompt,
376
- lora_adapter,
377
- seed,
378
- randomize_seed,
379
- guidance_scale,
380
- steps,
381
- preserve_identity=True,
382
- identity_strength=65,
383
- output_size=1280,
384
- fix_hands=False,
385
- progress=gr.Progress(track_tqdm=True),
386
- ):
387
  gc.collect()
388
  torch.cuda.empty_cache()
389
 
390
- pil_images = b64_to_pil_list(images_b64_json)
391
  if not pil_images:
392
- raise gr.Error("Please upload at least one image to edit.")
393
- if not prompt or prompt.strip() == "":
394
  raise gr.Error("Please enter an edit prompt.")
395
 
396
  # Identity lock (pose-friendly): append a strength-scaled "same person, pose may
397
- # change" instruction so the model re-poses the head while preserving the
398
- # face/identity. No masking/compositing β€” the head is edited so it can follow the
399
- # new pose. `preserve_identity` is the master on/off (uncheck for anime/3D);
400
- # `identity_strength` (0-100) sets how forcefully identity is locked. Applies on
401
- # top of ANY selected LoRA.
402
  eff_strength = int(identity_strength) if bool(preserve_identity) else 0
403
- prompt_used = _identity_prompt(prompt, eff_strength)
404
- # Anatomy lock + realism/HD boost on person edits (positive prompt β€” works even
405
- # at Guidance=1).
406
  if eff_strength > 0:
407
  prompt_used = prompt_used + ANATOMY_POS + REALISM_POS
408
 
409
- spec = ADAPTER_SPECS.get(lora_adapter)
410
- if not spec:
411
- raise gr.Error(f"Configuration not found for: {lora_adapter}")
412
-
413
- adapter_name = spec["adapter_name"]
414
- if adapter_name not in LOADED_ADAPTERS:
415
- print(f"--- Downloading and Loading Adapter: {lora_adapter} ---")
416
- try:
417
- pipe.load_lora_weights(spec["repo"], weight_name=spec["weights"], adapter_name=adapter_name)
418
- LOADED_ADAPTERS.add(adapter_name)
419
- except Exception as e:
420
- raise gr.Error(f"Failed to load adapter {lora_adapter}: {e}")
421
  else:
422
- print(f"--- Adapter {lora_adapter} already loaded. ---")
423
-
424
- pipe.set_adapters([adapter_name], adapter_weights=[1.0])
 
425
 
426
- # "Fix hands": the anti-extra-finger terms live in the NEGATIVE prompt, which is
427
- # ignored at Guidance=1 (CFG off on this distilled model). Raising Guidance turns
428
- # true-CFG on so those negatives actually suppress extra/fused fingers; a couple
429
- # more steps give CFG room to work. Costs ~2x time β€” hence it's opt-in.
430
  if bool(fix_hands):
431
  guidance_scale = max(float(guidance_scale), float(os.getenv("FIXHANDS_CFG", "2.5")))
432
  steps = max(int(steps), int(os.getenv("FIXHANDS_STEPS", "6")))
433
 
434
  if randomize_seed:
435
  seed = random.randint(0, MAX_SEED)
 
436
 
437
  generator = torch.Generator(device=device).manual_seed(seed)
438
  negative_prompt = (
439
  "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, "
440
  "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
441
  )
442
- # Identity + anatomy negatives (active when Guidance > 1). Reinforce face lock
443
- # and suppress extra hands / >5 fingers / duplicated limbs.
444
  if eff_strength > 0:
445
  negative_prompt = negative_prompt + ", " + IDENTITY_NEG + ", " + ANATOMY_NEG
446
  width, height = update_dimensions_on_upload(pil_images[0], output_size)
@@ -452,18 +435,78 @@ def infer(
452
  negative_prompt=negative_prompt,
453
  height=height,
454
  width=width,
455
- num_inference_steps=steps,
456
  generator=generator,
457
- true_cfg_scale=guidance_scale,
458
  ).images[0]
459
  return result_image, seed
460
- except Exception as e:
461
- raise e
462
  finally:
463
  gc.collect()
464
  torch.cuda.empty_cache()
465
 
466
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  css = r"""
468
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
469
  *{box-sizing:border-box;margin:0;padding:0}
@@ -1636,6 +1679,33 @@ with gr.Blocks() as demo:
1636
 
1637
  run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1638
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1639
  demo.load(fn=None, js=gallery_js)
1640
  demo.load(fn=None, js=wire_outputs_js)
1641
 
@@ -1661,6 +1731,7 @@ with gr.Blocks() as demo:
1661
  const fhVal = fhEl ? fhEl.checked : fh;
1662
  return [imgsJson, promptVal, loraVal, s, rs, gs, st, presVal, idstVal, oszVal, fhVal];
1663
  }""",
 
1664
  )
1665
 
1666
  example_load_btn.click(
@@ -1668,6 +1739,7 @@ with gr.Blocks() as demo:
1668
  inputs=[example_idx],
1669
  outputs=[example_result],
1670
  queue=False,
 
1671
  )
1672
 
1673
  if __name__ == "__main__":
 
366
  return max(16, (nw // 16) * 16), max(16, (nh // 16) * 16)
367
 
368
 
369
+ def _run_edit(pil_images, prompt, lora_adapter, seed, randomize_seed,
370
+ guidance_scale, steps, preserve_identity, identity_strength,
371
+ output_size, fix_hands):
372
+ """Core image edit shared by the UI (`infer`) and the API (`api_predict`).
373
+ Runs inside a @spaces.GPU entry. Returns (PIL result image, seed_used)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  gc.collect()
375
  torch.cuda.empty_cache()
376
 
377
+ pil_images = [im for im in (pil_images or []) if im is not None]
378
  if not pil_images:
379
+ raise gr.Error("Please provide at least one image to edit.")
380
+ if not prompt or str(prompt).strip() == "":
381
  raise gr.Error("Please enter an edit prompt.")
382
 
383
  # Identity lock (pose-friendly): append a strength-scaled "same person, pose may
384
+ # change" instruction. `preserve_identity` master on/off; `identity_strength`
385
+ # (0-100). Applies on top of ANY selected LoRA.
 
 
 
386
  eff_strength = int(identity_strength) if bool(preserve_identity) else 0
387
+ prompt_used = _identity_prompt(str(prompt), eff_strength)
388
+ # Anatomy lock + realism/HD boost on person edits (positive prompt, works at CFG=1).
 
389
  if eff_strength > 0:
390
  prompt_used = prompt_used + ANATOMY_POS + REALISM_POS
391
 
392
+ # Optional style LoRA. Empty / unknown adapter -> plain base-model edit.
393
+ spec = ADAPTER_SPECS.get(lora_adapter) if lora_adapter else None
394
+ if spec:
395
+ adapter_name = spec["adapter_name"]
396
+ if adapter_name not in LOADED_ADAPTERS:
397
+ print(f"--- Downloading and Loading Adapter: {lora_adapter} ---")
398
+ try:
399
+ pipe.load_lora_weights(spec["repo"], weight_name=spec["weights"], adapter_name=adapter_name)
400
+ LOADED_ADAPTERS.add(adapter_name)
401
+ except Exception as e:
402
+ raise gr.Error(f"Failed to load adapter {lora_adapter}: {e}")
403
+ pipe.set_adapters([adapter_name], adapter_weights=[1.0])
404
  else:
405
+ try:
406
+ pipe.disable_lora() # no style requested β€” clear any adapter from a prior call
407
+ except Exception:
408
+ pass
409
 
410
+ # "Fix hands": the anti-extra-finger terms live in the NEGATIVE prompt, ignored at
411
+ # Guidance=1 (CFG off on this distilled model). Raising Guidance turns true-CFG on
412
+ # so those negatives suppress extra/fused fingers; +steps give CFG room. ~2x time.
 
413
  if bool(fix_hands):
414
  guidance_scale = max(float(guidance_scale), float(os.getenv("FIXHANDS_CFG", "2.5")))
415
  steps = max(int(steps), int(os.getenv("FIXHANDS_STEPS", "6")))
416
 
417
  if randomize_seed:
418
  seed = random.randint(0, MAX_SEED)
419
+ seed = int(seed)
420
 
421
  generator = torch.Generator(device=device).manual_seed(seed)
422
  negative_prompt = (
423
  "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, "
424
  "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
425
  )
426
+ # Identity + anatomy negatives (active when Guidance > 1).
 
427
  if eff_strength > 0:
428
  negative_prompt = negative_prompt + ", " + IDENTITY_NEG + ", " + ANATOMY_NEG
429
  width, height = update_dimensions_on_upload(pil_images[0], output_size)
 
435
  negative_prompt=negative_prompt,
436
  height=height,
437
  width=width,
438
+ num_inference_steps=int(steps),
439
  generator=generator,
440
+ true_cfg_scale=float(guidance_scale),
441
  ).images[0]
442
  return result_image, seed
 
 
443
  finally:
444
  gc.collect()
445
  torch.cuda.empty_cache()
446
 
447
 
448
+ # duration=120 gives headroom so high-res (1536-2048) runs don't hit the default
449
+ # ~60s ZeroGPU wall limit ("GPU task aborted"). Billing is on ACTUAL seconds, so a
450
+ # fast 1024 run still costs little; this only raises the admission reservation.
451
+ @spaces.GPU(size="xlarge", duration=120)
452
+ def infer(
453
+ images_b64_json,
454
+ prompt,
455
+ lora_adapter,
456
+ seed,
457
+ randomize_seed,
458
+ guidance_scale,
459
+ steps,
460
+ preserve_identity=True,
461
+ identity_strength=65,
462
+ output_size=1280,
463
+ fix_hands=False,
464
+ progress=gr.Progress(track_tqdm=True),
465
+ ):
466
+ pil_images = b64_to_pil_list(images_b64_json)
467
+ return _run_edit(pil_images, prompt, lora_adapter, seed, randomize_seed,
468
+ guidance_scale, steps, preserve_identity, identity_strength,
469
+ output_size, fix_hands)
470
+
471
+
472
+ def _as_pil(x):
473
+ if x is None:
474
+ return None
475
+ if isinstance(x, Image.Image):
476
+ return x.convert("RGB")
477
+ try:
478
+ return Image.fromarray(x).convert("RGB")
479
+ except Exception:
480
+ return None
481
+
482
+
483
+ @spaces.GPU(size="xlarge", duration=120)
484
+ def api_predict(image, prompt, lora_adapter="", image2=None, seed=0,
485
+ randomize_seed=True, guidance_scale=1.0, steps=4,
486
+ preserve_identity=True, identity_strength=65, output_size=1280,
487
+ fix_hands=False, progress=gr.Progress(track_tqdm=True)):
488
+ """Edit an image from a text prompt (Qwen-Image-Edit-2511). Returns [result_image, seed_used].
489
+
490
+ image : the image to edit (the person/subject whose identity is kept).
491
+ prompt : what to change, e.g. "change her outfit to a red dress; turn to look left".
492
+ lora_adapter : optional style name from the adapter list ("" = no style LoRA / base edit).
493
+ image2 : optional second reference image (e.g. a style or lighting reference).
494
+ seed : int seed (ignored if randomize_seed=True).
495
+ randomize_seed : pick a new random seed each call.
496
+ guidance_scale : true-CFG. 1.0 = fastest; >1 (e.g. 2.5) activates the negative prompt
497
+ that suppresses extra fingers / identity drift (slower).
498
+ steps : denoise steps. 4 is native for this distilled model.
499
+ preserve_identity : keep the person's face while allowing pose/head to change.
500
+ identity_strength : 0-100, how forcefully identity is locked.
501
+ output_size : long-side pixels 1024-2048. Higher = more HD detail, slower.
502
+ fix_hands : raise CFG/steps to suppress extra/fused fingers (slower).
503
+ """
504
+ imgs = [im for im in (_as_pil(image), _as_pil(image2)) if im is not None]
505
+ return _run_edit(imgs, prompt, lora_adapter, seed, randomize_seed,
506
+ guidance_scale, steps, preserve_identity, identity_strength,
507
+ output_size, fix_hands)
508
+
509
+
510
  css = r"""
511
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
512
  *{box-sizing:border-box;margin:0;padding:0}
 
1679
 
1680
  run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1681
 
1682
+ # ── Documented public API endpoint: /edit ──────────────────────────────────
1683
+ # Typed inputs so API/MCP callers pass a real image + prompt (not the UI's
1684
+ # internal base64 blob). Hidden from the UI; fully functional over the API.
1685
+ with gr.Row(visible=False):
1686
+ api_image = gr.Image(type="pil", label="image")
1687
+ api_image2 = gr.Image(type="pil", label="image2")
1688
+ api_prompt = gr.Textbox(label="prompt")
1689
+ api_lora = gr.Textbox(label="lora_adapter", value="")
1690
+ api_seed = gr.Number(label="seed", value=0, precision=0)
1691
+ api_rand = gr.Checkbox(label="randomize_seed", value=True)
1692
+ api_cfg = gr.Number(label="guidance_scale", value=1.0)
1693
+ api_steps = gr.Number(label="steps", value=4, precision=0)
1694
+ api_pres = gr.Checkbox(label="preserve_identity", value=True)
1695
+ api_idst = gr.Number(label="identity_strength", value=65, precision=0)
1696
+ api_size = gr.Number(label="output_size", value=1280, precision=0)
1697
+ api_fix = gr.Checkbox(label="fix_hands", value=False)
1698
+ api_out = gr.Image(label="result_image", type="pil", format="png")
1699
+ api_seed_o = gr.Number(label="seed_used", precision=0)
1700
+ api_btn = gr.Button("api_edit")
1701
+ api_btn.click(
1702
+ fn=api_predict,
1703
+ inputs=[api_image, api_prompt, api_lora, api_image2, api_seed, api_rand,
1704
+ api_cfg, api_steps, api_pres, api_idst, api_size, api_fix],
1705
+ outputs=[api_out, api_seed_o],
1706
+ api_name="edit",
1707
+ )
1708
+
1709
  demo.load(fn=None, js=gallery_js)
1710
  demo.load(fn=None, js=wire_outputs_js)
1711
 
 
1731
  const fhVal = fhEl ? fhEl.checked : fh;
1732
  return [imgsJson, promptVal, loraVal, s, rs, gs, st, presVal, idstVal, oszVal, fhVal];
1733
  }""",
1734
+ show_api=False, # internal UI path; the public API is /edit (api_predict)
1735
  )
1736
 
1737
  example_load_btn.click(
 
1739
  inputs=[example_idx],
1740
  outputs=[example_result],
1741
  queue=False,
1742
+ show_api=False,
1743
  )
1744
 
1745
  if __name__ == "__main__":