Spaces:
Paused
Paused
Upload app.py with huggingface_hub
Browse files
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 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 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 =
|
| 391 |
if not pil_images:
|
| 392 |
-
raise gr.Error("Please
|
| 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
|
| 398 |
-
#
|
| 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
|
| 405 |
-
# at Guidance=1).
|
| 406 |
if eff_strength > 0:
|
| 407 |
prompt_used = prompt_used + ANATOMY_POS + REALISM_POS
|
| 408 |
|
| 409 |
-
|
| 410 |
-
if
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
else:
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
|
|
|
| 425 |
|
| 426 |
-
# "Fix hands": the anti-extra-finger terms live in the NEGATIVE prompt,
|
| 427 |
-
#
|
| 428 |
-
#
|
| 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).
|
| 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__":
|