Spaces:
Running on Zero
Running on Zero
Merge branch 'main' of https://huggingface.co/spaces/Onise/qwen-image-edit-aio-lora
Browse files- app.py +47 -18
- qwenimage/pipeline_qwenimage_edit_plus.py +16 -2
app.py
CHANGED
|
@@ -30,6 +30,7 @@ import os
|
|
| 30 |
import base64
|
| 31 |
from io import BytesIO
|
| 32 |
import json
|
|
|
|
| 33 |
|
| 34 |
SYSTEM_PROMPT = '''
|
| 35 |
# Edit Instruction Rewriter
|
|
@@ -142,7 +143,7 @@ def polish_prompt_hf(original_prompt, img_list):
|
|
| 142 |
# If img is a PIL Image
|
| 143 |
if hasattr(img, 'save'): # Check if it's a PIL Image
|
| 144 |
buffered = BytesIO()
|
| 145 |
-
img.save(buffered, format="PNG")
|
| 146 |
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 147 |
image_url = f"data:image/png;base64,{img_base64}"
|
| 148 |
# If img is already a file path (string)
|
|
@@ -217,7 +218,7 @@ def polish_prompt_hf(original_prompt, img_list):
|
|
| 217 |
def encode_image(pil_image):
|
| 218 |
import io
|
| 219 |
buffered = io.BytesIO()
|
| 220 |
-
pil_image.save(buffered, format="PNG")
|
| 221 |
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 222 |
|
| 223 |
# --- Model Loading ---
|
|
@@ -398,17 +399,13 @@ torch.cuda.empty_cache()
|
|
| 398 |
# optimize_pipeline_(pipe, image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))], prompt="prompt")
|
| 399 |
|
| 400 |
# --- Custom LoRA loading (QWEN_MCNL) ---
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
pipe.fuse_lora()
|
| 409 |
-
print("custom LoRA fused successfully.")
|
| 410 |
-
except Exception as e:
|
| 411 |
-
print(f"Warning: failed to load/fuse custom LoRA Daverrrr75/QWEN_MCNL: {e}")
|
| 412 |
|
| 413 |
|
| 414 |
# --- UI Constants and Helpers ---
|
|
@@ -504,8 +501,33 @@ def infer(
|
|
| 504 |
except Exception:
|
| 505 |
continue
|
| 506 |
|
| 507 |
-
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
print(f"Calling pipeline with prompt: '{prompt}'")
|
| 510 |
print(f"Negative Prompt: '{negative_prompt}'")
|
| 511 |
print(f"Seed: {seed}, Steps: {num_inference_steps}, Guidance: {true_guidance_scale}, Size: {width}x{height}")
|
|
@@ -527,8 +549,15 @@ def infer(
|
|
| 527 |
num_images_per_prompt=num_images_per_prompt,
|
| 528 |
).images
|
| 529 |
|
| 530 |
-
#
|
| 531 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
# --- Examples and UI Layout ---
|
| 534 |
examples = []
|
|
@@ -568,7 +597,7 @@ with gr.Blocks(css=css) as demo:
|
|
| 568 |
interactive=True)
|
| 569 |
|
| 570 |
with gr.Column():
|
| 571 |
-
result = gr.Gallery(label="Result", show_label=False, type="
|
| 572 |
# Add this button right after the result gallery - initially hidden
|
| 573 |
use_output_btn = gr.Button("↗️ Use as input", variant="secondary", size="sm", visible=False)
|
| 574 |
|
|
|
|
| 30 |
import base64
|
| 31 |
from io import BytesIO
|
| 32 |
import json
|
| 33 |
+
import tempfile
|
| 34 |
|
| 35 |
SYSTEM_PROMPT = '''
|
| 36 |
# Edit Instruction Rewriter
|
|
|
|
| 143 |
# If img is a PIL Image
|
| 144 |
if hasattr(img, 'save'): # Check if it's a PIL Image
|
| 145 |
buffered = BytesIO()
|
| 146 |
+
img.save(buffered, format="PNG", compress_level=0, optimize=False)
|
| 147 |
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 148 |
image_url = f"data:image/png;base64,{img_base64}"
|
| 149 |
# If img is already a file path (string)
|
|
|
|
| 218 |
def encode_image(pil_image):
|
| 219 |
import io
|
| 220 |
buffered = io.BytesIO()
|
| 221 |
+
pil_image.save(buffered, format="PNG", compress_level=0, optimize=False)
|
| 222 |
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 223 |
|
| 224 |
# --- Model Loading ---
|
|
|
|
| 399 |
# optimize_pipeline_(pipe, image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))], prompt="prompt")
|
| 400 |
|
| 401 |
# --- Custom LoRA loading (QWEN_MCNL) ---
|
| 402 |
+
print("loading custom LoRA: Daverrrr75/QWEN_MCNL ...")
|
| 403 |
+
pipe.load_lora_weights(
|
| 404 |
+
"Daverrrr75/QWEN_MCNL",
|
| 405 |
+
weight_name="qwen_MCNL_v1.0.safetensors",
|
| 406 |
+
)
|
| 407 |
+
pipe.fuse_lora()
|
| 408 |
+
print("custom LoRA fused successfully.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
|
| 411 |
# --- UI Constants and Helpers ---
|
|
|
|
| 501 |
except Exception:
|
| 502 |
continue
|
| 503 |
|
| 504 |
+
# Adjust dimensions to preserve input resolution and ensure quality
|
| 505 |
+
if len(pil_images) > 0:
|
| 506 |
+
input_width, input_height = pil_images[0].size
|
| 507 |
+
aspect_ratio = input_width / input_height
|
| 508 |
+
else:
|
| 509 |
+
aspect_ratio = 1.0 # Default square if no image
|
| 510 |
+
|
| 511 |
+
# Preserve input resolution by default - use input dimensions directly
|
| 512 |
+
if height is None and width is None:
|
| 513 |
+
# Use input dimensions directly, rounded to required multiple
|
| 514 |
+
width = round(input_width / 32) * 32
|
| 515 |
+
height = round(input_height / 32) * 32
|
| 516 |
+
elif height is not None and width is None:
|
| 517 |
+
width = round(height * aspect_ratio / 32) * 32
|
| 518 |
+
elif width is not None and height is None:
|
| 519 |
+
height = round(width / aspect_ratio / 32) * 32
|
| 520 |
+
else:
|
| 521 |
+
# Both provided: adjust width to match height * aspect_ratio
|
| 522 |
+
width = round(height * aspect_ratio / 32) * 32
|
| 523 |
+
|
| 524 |
+
# Quality safeguard: ensure minimum size to avoid excessive quality loss
|
| 525 |
+
min_size = 512
|
| 526 |
+
if height < min_size or width < min_size:
|
| 527 |
+
# Scale up while preserving ratio if too small
|
| 528 |
+
scale_factor = max(min_size / height, min_size / width)
|
| 529 |
+
height = round(height * scale_factor / 32) * 32
|
| 530 |
+
width = round(width * scale_factor / 32) * 32
|
| 531 |
print(f"Calling pipeline with prompt: '{prompt}'")
|
| 532 |
print(f"Negative Prompt: '{negative_prompt}'")
|
| 533 |
print(f"Seed: {seed}, Steps: {num_inference_steps}, Guidance: {true_guidance_scale}, Size: {width}x{height}")
|
|
|
|
| 549 |
num_images_per_prompt=num_images_per_prompt,
|
| 550 |
).images
|
| 551 |
|
| 552 |
+
# Save images as PNG with 100% quality (lossless, no compression)
|
| 553 |
+
output_paths = []
|
| 554 |
+
for i, img in enumerate(image):
|
| 555 |
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
|
| 556 |
+
img.save(tmp_file.name, format="PNG", compress_level=0, optimize=False)
|
| 557 |
+
output_paths.append(tmp_file.name)
|
| 558 |
+
|
| 559 |
+
# Return paths, seed, and make button visible
|
| 560 |
+
return output_paths, seed, gr.update(visible=True)
|
| 561 |
|
| 562 |
# --- Examples and UI Layout ---
|
| 563 |
examples = []
|
|
|
|
| 597 |
interactive=True)
|
| 598 |
|
| 599 |
with gr.Column():
|
| 600 |
+
result = gr.Gallery(label="Result", show_label=False, type="filepath", interactive=False)
|
| 601 |
# Add this button right after the result gallery - initially hidden
|
| 602 |
use_output_btn = gr.Button("↗️ Use as input", variant="secondary", size="sm", visible=False)
|
| 603 |
|
qwenimage/pipeline_qwenimage_edit_plus.py
CHANGED
|
@@ -627,8 +627,18 @@ class QwenImageEditPlusPipeline(DiffusionPipeline, QwenImageLoraLoaderMixin):
|
|
| 627 |
[`~pipelines.qwenimage.QwenImagePipelineOutput`] if `return_dict` is True, otherwise a `tuple`. When
|
| 628 |
returning a tuple, the first element is a list with the generated images.
|
| 629 |
"""
|
|
|
|
| 630 |
image_size = image[-1].size if isinstance(image, list) else image.size
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
height = height or calculated_height
|
| 633 |
width = width or calculated_width
|
| 634 |
|
|
@@ -677,7 +687,11 @@ class QwenImageEditPlusPipeline(DiffusionPipeline, QwenImageLoraLoaderMixin):
|
|
| 677 |
condition_width, condition_height = calculate_dimensions(
|
| 678 |
CONDITION_IMAGE_SIZE, image_width / image_height
|
| 679 |
)
|
| 680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
condition_image_sizes.append((condition_width, condition_height))
|
| 682 |
vae_image_sizes.append((vae_width, vae_height))
|
| 683 |
condition_images.append(self.image_processor.resize(img, condition_height, condition_width))
|
|
|
|
| 627 |
[`~pipelines.qwenimage.QwenImagePipelineOutput`] if `return_dict` is True, otherwise a `tuple`. When
|
| 628 |
returning a tuple, the first element is a list with the generated images.
|
| 629 |
"""
|
| 630 |
+
# Preserve input resolution - use input dimensions directly
|
| 631 |
image_size = image[-1].size if isinstance(image, list) else image.size
|
| 632 |
+
input_width, input_height = image_size[0], image_size[1]
|
| 633 |
+
aspect_ratio = input_width / input_height
|
| 634 |
+
|
| 635 |
+
# Use input dimensions if not specified, rounded to required multiple
|
| 636 |
+
if height is None and width is None:
|
| 637 |
+
calculated_width = round(input_width / (self.vae_scale_factor * 2)) * (self.vae_scale_factor * 2)
|
| 638 |
+
calculated_height = round(input_height / (self.vae_scale_factor * 2)) * (self.vae_scale_factor * 2)
|
| 639 |
+
else:
|
| 640 |
+
calculated_width, calculated_height = calculate_dimensions(1024 * 1024, aspect_ratio)
|
| 641 |
+
|
| 642 |
height = height or calculated_height
|
| 643 |
width = width or calculated_width
|
| 644 |
|
|
|
|
| 687 |
condition_width, condition_height = calculate_dimensions(
|
| 688 |
CONDITION_IMAGE_SIZE, image_width / image_height
|
| 689 |
)
|
| 690 |
+
# Use input dimensions for VAE processing to preserve quality
|
| 691 |
+
# Round to required multiple for VAE compatibility
|
| 692 |
+
multiple_of = self.vae_scale_factor * 2
|
| 693 |
+
vae_width = round(image_width / multiple_of) * multiple_of
|
| 694 |
+
vae_height = round(image_height / multiple_of) * multiple_of
|
| 695 |
condition_image_sizes.append((condition_width, condition_height))
|
| 696 |
vae_image_sizes.append((vae_width, vae_height))
|
| 697 |
condition_images.append(self.image_processor.resize(img, condition_height, condition_width))
|