Qwen-Image-Edit / app.py
HAL1993's picture
Update app.py
94f4104 verified
raw
history blame
26.8 kB
import os
import math
import gradio as gr
import numpy as np
import random
import torch
import spaces
from PIL import Image
from diffusers import FlowMatchEulerDiscreteScheduler
from optimization import optimize_pipeline_
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
import requests
import logging
# Set up logging to suppress print statements in UI
logging.basicConfig(level=logging.INFO, filename='qwen_image_editor.log', filemode='a')
logger = logging.getLogger(__name__)
# --- Translation Function ---
@spaces.GPU
def translate_albanian_to_english(text, language="en"):
"""Translate from Albanian to English using the sepioo-facebook-translation API."""
if not text.strip():
raise gr.Error("Please enter a description.")
for attempt in range(2):
try:
response = requests.post(
"https://hal1993-mdftranslation1234567890abcdef1234567890-fc073a6.hf.space/v1/translate",
json={"from_language": "sq", "to_language": "en", "input_text": text},
headers={"accept": "application/json", "Content-Type": "application/json"},
timeout=5
)
response.raise_for_status()
translated = response.json().get("translate", "")
logger.info(f"Translation response: {translated}")
return translated
except Exception as e:
logger.error(f"Translation error (attempt {attempt + 1}): {e}")
if attempt == 1:
raise gr.Error("Translation failed. Please try again.")
raise gr.Error("Translation failed. Please try again.")
# --- Model Loading ---
dtype = torch.bfloat16
device = "cuda" if torch.cuda.is_available() else "cpu"
# Scheduler configuration for Lightning
scheduler_config = {
"base_image_seq_len": 256,
"base_shift": math.log(3),
"invert_sigmas": False,
"max_image_seq_len": 8192,
"max_shift": math.log(3),
"num_train_timesteps": 1000,
"shift": 1.0,
"shift_terminal": None,
"stochastic_sampling": False,
"time_shift_type": "exponential",
"use_beta_sigmas": False,
"use_dynamic_shifting": True,
"use_exponential_sigmas": False,
"use_karras_sigmas": False,
}
# Initialize scheduler with Lightning config
scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config)
# Load the model pipeline
pipe = QwenImageEditPlusPipeline.from_pretrained("Qwen/Qwen-Image-Edit-2509",
scheduler=scheduler,
torch_dtype=dtype).to(device)
pipe.load_lora_weights(
"lightx2v/Qwen-Image-Lightning",
weight_name="Qwen-Image-Lightning-4steps-V2.0.safetensors"
)
pipe.fuse_lora()
# Apply the same optimizations from the first version
pipe.transformer.__class__ = QwenImageTransformer2DModel
pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
# --- Ahead-of-time compilation ---
optimize_pipeline_(pipe, image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))], prompt="prompt")
# --- UI Constants and Helpers ---
MAX_SEED = np.iinfo(np.int32).max
QUALITY_PROMPT = ", high quality, detailed, vibrant, professional lighting"
# --- Main Inference Function ---
@spaces.GPU(duration=40)
def infer(image, prompt):
"""
Generates an image using the local Qwen-Image diffusers pipeline.
"""
negative_prompt = "" # Empty as in original
seed = random.randint(0, MAX_SEED) # Default: randomize_seed=True
true_guidance_scale = 1.0 # Default
num_inference_steps = 4 # Default
height = None # Default
width = None # Default
num_images_per_prompt = 1 # Default
# Translate prompt from Albanian to English
prompt_final = translate_albanian_to_english(prompt.strip(), language="en") + QUALITY_PROMPT
# Set up the generator for reproducibility
generator = torch.Generator(device=device).manual_seed(seed)
# Load input image into PIL Image
pil_image = None
if image is not None:
try:
if isinstance(image, Image.Image):
pil_image = image.convert("RGB")
elif isinstance(image, str):
pil_image = Image.open(image).convert("RGB")
elif hasattr(image, "name"):
pil_image = Image.open(image.name).convert("RGB")
except Exception as e:
logger.error(f"Error loading image: {e}")
raise gr.Error("Failed to load input image.")
if height == 256 and width == 256:
height, width = None, None
logger.info(f"Calling pipeline with prompt: '{prompt_final}'")
logger.info(f"Negative Prompt: '{negative_prompt}'")
logger.info(f"Seed: {seed}, Steps: {num_inference_steps}, Guidance: {true_guidance_scale}, Size: {width}x{height}")
# Generate the image
output = pipe(
image=[pil_image] if pil_image is not None else None,
prompt=prompt_final,
height=height,
width=width,
negative_prompt=negative_prompt,
num_inference_steps=num_inference_steps,
generator=generator,
true_cfg_scale=true_guidance_scale,
num_images_per_prompt=num_images_per_prompt,
).images
return output[0] if output else None
# --- Gradio User Interface ---
def create_demo():
with gr.Blocks(css="", title="Qwen Image Editor") as demo:
gr.HTML("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700&display=swap');
@keyframes glow {
0% { box-shadow: 0 0 14px rgba(0, 255, 128, 0.5); }
50% { box-shadow: 0 0 14px rgba(0, 255, 128, 0.7); }
100% { box-shadow: 0 0 14px rgba(0, 255, 128, 0.5); }
}
@keyframes glow-hover {
0% { box-shadow: 0 0 20px rgba(0, 255, 128, 0.7); }
50% { box-shadow: 0 0 20px rgba(0, 255, 128, 0.9); }
100% { box-shadow: 0 0 20px rgba(0, 255, 128, 0.7); }
}
@keyframes slide {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes pulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
body {
background: #000000 !important;
color: #FFFFFF !important;
font-family: 'Orbitron', sans-serif;
min-height: 100vh;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
max-width: 100vw !important;
overflow-x: hidden !important;
display: flex !important;
justify-content: center;
align-items: center;
flex-direction: column;
}
body::before {
content: "";
display: block;
height: 600px;
background: #000000 !important;
}
.gr-blocks, .container {
width: 100% !important;
max-width: 100vw !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
background: #000000 !important;
color: #FFFFFF !important;
}
#general_items {
width: 100% !important;
max-width: 100vw !important;
margin: 2rem 0 !important;
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
background: #000000 !important;
color: #FFFFFF !important;
}
#input_column {
background: #000000 !important;
border: none !important;
border-radius: 8px;
padding: 1rem !important;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3) !important;
width: 100% !important;
max-width: 100vw !important;
box-sizing: border-box !important;
color: #FFFFFF !important;
}
h1 {
font-size: 5rem;
font-weight: 700;
text-align: center;
color: #FFFFFF !important;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.3) !important;
margin: 0 auto 0.5rem auto;
display: block;
max-width: 100%;
}
#subtitle {
font-size: 1rem;
text-align: center;
color: #FFFFFF !important;
opacity: 0.8;
margin-bottom: 1rem;
display: block;
max-width: 100%;
}
.gradio-component {
background: #000000 !important;
border: none;
margin: 0.75rem 0;
width: 100% !important;
max-width: 100vw !important;
color: #FFFFFF !important;
}
.image-container {
aspect-ratio: 1/1;
width: 100% !important;
max-width: 100vw !important;
min-height: 500px;
height: auto;
border: 0.5px solid #FFFFFF !important;
border-radius: 4px;
box-sizing: border-box !important;
background: #000000 !important;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3) !important;
position: relative;
color: #FFFFFF !important;
}
.image-container img {
width: 100% !important;
height: auto;
box-sizing: border-box !important;
display: block !important;
}
/* Exhaustive selectors to hide upload/webcam/paste toolbar */
.image-container[aria-label="Input Image"] .file-upload,
.image-container[aria-label="Input Image"] .file-preview,
.image-container[aria-label="Input Image"] .image-actions,
.image-container[aria-label="Input Image"] .gr-file-upload,
.image-container[aria-label="Input Image"] .gr-file,
.image-container[aria-label="Input Image"] .gr-actions,
.image-container[aria-label="Input Image"] .gr-upload-button,
.image-container[aria-label="Input Image"] .gr-image-toolbar,
.image-container[aria-label="Input Image"] .gr-file-actions,
.image-container[aria-label="Input Image"] .gr-upload-options,
div[aria-label="Input Image"] > div > div:not(.image-container),
div[aria-label="Input Image"] .gr-button,
.image-container[aria-label="Result Image"] .file-upload,
.image-container[aria-label="Result Image"] .file-preview,
.image-container[aria-label="Result Image"] .image-actions,
.image-container[aria-label="Result Image"] .gr-file-upload,
.image-container[aria-label="Result Image"] .gr-file,
.image-container[aria-label="Result Image"] .gr-actions,
.image-container[aria-label="Result Image"] .gr-upload-button,
.image-container[aria-label="Result Image"] .gr-image-toolbar,
.image-container[aria-label="Result Image"] .gr-file-actions,
.image-container[aria-label="Result Image"] .gr-upload-options,
div[aria-label="Result Image"] > div > div:not(.image-container),
div[aria-label="Result Image"] .gr-button {
display: none !important;
}
/* Processing overlay for Result Image */
.image-container[aria-label="Result Image"].processing {
background: #000000 !important;
position: relative !important;
}
.image-container[aria-label="Result Image"].processing::before {
content: "PROCESSING...";
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
color: #FFFFFF !important;
font-family: 'Orbitron', sans-serif !important;
font-size: 1.8rem !important;
font-weight: 700 !important;
text-align: center !important;
text-shadow: 0 0 10px rgba(0, 255, 128, 0.8) !important;
animation: pulse 1.5s ease-in-out infinite, glow 2s ease-in-out infinite !important;
z-index: 9999 !important;
width: 100% !important;
height: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
pointer-events: none !important;
background: #000000 !important;
border-radius: 4px !important;
box-sizing: border-box !important;
}
.image-container[aria-label="Result Image"].processing * {
display: none !important;
}
.image-container[aria-label="Result Image"].processing img {
display: none !important;
}
input, textarea {
background: #000000 !important;
color: #FFFFFF !important;
border: 1px solid #FFFFFF !important;
border-radius: 4px;
padding: 0.5rem;
width: 100% !important;
max-width: 100vw !important;
box-sizing: border-box !important;
}
input:hover, textarea:hover {
box-shadow: 0 0 8px rgba(255, 255, 255, 0.3) !important;
transition: box-shadow 0.3s;
}
.gr-button-primary {
background: linear-gradient(90deg, rgba(0, 255, 128, 0.3), rgba(0, 200, 100, 0.3), rgba(0, 255, 128, 0.3)) !important;
background-size: 200% 100%;
animation: slide 4s ease-in-out infinite, glow 3s ease-in-out infinite;
color: #FFFFFF !important;
border: 1px solid #FFFFFF !important;
border-radius: 6px;
padding: 0.75rem 1.5rem;
font-size: 1.1rem;
font-weight: 600;
box-shadow: 0 0 14px rgba(0, 255, 128, 0.7) !important;
transition: box-shadow 0.3s, transform 0.3s;
width: 100% !important;
max-width: 100vw !important;
min-height: 48px;
cursor: pointer;
}
.gr-button-primary:hover {
box-shadow: 0 0 20px rgba(0, 255, 128, 0.9) !important;
animation: slide 4s ease-in-out infinite, glow-hover 3s ease-in-out infinite;
transform: scale(1.05);
}
button[aria-label="Fullscreen"], button[aria-label="Fullscreen"]:hover,
button[aria-label="Share"], button[aria-label="Share"]:hover {
display: none !important;
}
button[aria-label="Download"] {
transform: scale(3);
transform-origin: top right;
background: #000000 !important;
color: #FFFFFF !important;
border: 1px solid #FFFFFF !important;
border-radius: 4px;
padding: 0.4rem !important;
margin: 0.5rem !important;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.3) !important;
transition: box-shadow 0.3s;
}
button[aria-label="Download"]:hover {
box-shadow: 0 0 12px rgba(255, 255, 255, 0.5) !important;
}
.progress-text, .gr-progress, .gr-prose, .gr-log {
display: none !important;
}
footer, .gr-button-secondary {
display: none !important;
}
.gr-group {
background: #000000 !important;
border: none !important;
width: 100% !important;
max-width: 100vw !important;
}
@media (max-width: 768px) {
h1 {
font-size: 4rem;
}
#subtitle {
font-size: 0.9rem;
}
.gr-button-primary {
padding: 0.6rem 1rem;
font-size: 1rem;
box-shadow: 0 0 10px rgba(0, 255, 128, 0.7) !important;
animation: slide 4s ease-in-out infinite, glow 3s ease-in-out infinite;
}
.gr-button-primary:hover {
box-shadow: 0 0 12px rgba(0, 255, 128, 0.9) !important;
animation: slide 4s ease-in-out infinite, glow-hover 3s ease-in-out infinite;
}
.image-container {
min-height: 300px;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.3) !important;
border: 0.5px solid #FFFFFF !important;
}
.image-container[aria-label="Result Image"].processing::before {
font-size: 1.2rem !important;
}
}
</style>
<script>
// Strict path check for exactly /spaceishere or /spaceishere/*
const validPathRegex = /^\/spaceishere(\/.*)?$/;
if (!validPathRegex.test(window.location.pathname)) {
document.body.innerHTML = '<h1 style="color:#ef4444;font-family:sans-serif;text-align:center;margin-top:100px;">500 Internal Server Error</h1>';
throw new Error('500');
}
document.addEventListener('DOMContentLoaded', () => {
// Debug: Log container dimensions, glow, background, border, and toolbar visibility
const containers = document.querySelectorAll('#general_items, #input_column, .image-container');
containers.forEach(container => {
const width = container.offsetWidth;
const style = window.getComputedStyle(container);
console.log(`Container ${container.id || container.className}: width=${width}px, box-shadow=${style.boxShadow}, background=${style.background}, border=${style.border} (Viewport: ${window.innerWidth}px)`);
container.setAttribute('data-width', `${width}px`);
});
const editButton = document.querySelector('.gr-button-primary');
if (editButton) {
const style = window.getComputedStyle(editButton);
console.log(`Edit button: box-shadow=${style.boxShadow}, background=${style.background}, border=${style.border}, animation=${style.animation}, background-position=${style.backgroundPosition}`);
}
// Log all toolbar elements
const toolbarSelectors = [
'.image-container[aria-label="Input Image"] .file-upload',
'.image-container[aria-label="Input Image"] .file-preview',
'.image-container[aria-label="Input Image"] .image-actions',
'.image-container[aria-label="Input Image"] .gr-file-upload',
'.image-container[aria-label="Input Image"] .gr-file',
'.image-container[aria-label="Input Image"] .gr-actions',
'.image-container[aria-label="Input Image"] .gr-upload-button',
'.image-container[aria-label="Input Image"] .gr-image-toolbar',
'.image-container[aria-label="Input Image"] .gr-file-actions',
'.image-container[aria-label="Input Image"] .gr-upload-options',
'div[aria-label="Input Image"] > div > div:not(.image-container)',
'div[aria-label="Input Image"] .gr-button',
'.image-container[aria-label="Result Image"] .file-upload',
'.image-container[aria-label="Result Image"] .file-preview',
'.image-container[aria-label="Result Image"] .image-actions',
'.image-container[aria-label="Result Image"] .gr-file-upload',
'.image-container[aria-label="Result Image"] .gr-file',
'.image-container[aria-label="Result Image"] .gr-actions',
'.image-container[aria-label="Result Image"] .gr-upload-button',
'.image-container[aria-label="Result Image"] .gr-image-toolbar',
'.image-container[aria-label="Result Image"] .gr-file-actions',
'.image-container[aria-label="Result Image"] .gr-upload-options',
'div[aria-label="Result Image"] > div > div:not(.image-container)',
'div[aria-label="Result Image"] .gr-button'
];
toolbarSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
const style = window.getComputedStyle(el);
console.log(`Toolbar element ${selector}: display=${style.display}, class=${el.className}`);
});
});
// Force hide any residual toolbar elements
const toolbars = document.querySelectorAll(
'.image-container[aria-label="Input Image"] > div > div:not(.image-container), ' +
'.image-container[aria-label="Input Image"] .file-upload, ' +
'.image-container[aria-label="Input Image"] .file-preview, ' +
'.image-container[aria-label="Input Image"] .image-actions, ' +
'.image-container[aria-label="Input Image"] .gr-file-upload, ' +
'.image-container[aria-label="Input Image"] .gr-file, ' +
'.image-container[aria-label="Input Image"] .gr-actions, ' +
'.image-container[aria-label="Input Image"] .gr-upload-button, ' +
'.image-container[aria-label="Input Image"] .gr-image-toolbar, ' +
'.image-container[aria-label="Input Image"] .gr-file-actions, ' +
'.image-container[aria-label="Input Image"] .gr-upload-options, ' +
'div[aria-label="Input Image"] .gr-button, ' +
'.image-container[aria-label="Result Image"] > div > div:not(.image-container), ' +
'.image-container[aria-label="Result Image"] .file-upload, ' +
'.image-container[aria-label="Result Image"] .file-preview, ' +
'.image-container[aria-label="Result Image"] .image-actions, ' +
'.image-container[aria-label="Result Image"] .gr-file-upload, ' +
'.image-container[aria-label="Result Image"] .gr-file, ' +
'.image-container[aria-label="Result Image"] .gr-actions, ' +
'.image-container[aria-label="Result Image"] .gr-upload-button, ' +
'.image-container[aria-label="Result Image"] .gr-image-toolbar, ' +
'.image-container[aria-label="Result Image"] .gr-file-actions, ' +
'.image-container[aria-label="Result Image"] .gr-upload-options, ' +
'div[aria-label="Result Image"] .gr-button'
);
toolbars.forEach(toolbar => {
toolbar.style.display = 'none';
console.log(`Forced hide toolbar: ${toolbar.className}`);
});
// Hide any technical output containers
const outputContainers = document.querySelectorAll('.gr-prose, .gr-log, .progress-text, .gr-progress');
outputContainers.forEach(container => {
container.style.display = 'none';
console.log(`Forced hide output container: ${container.className}`);
});
// Add processing class when Edit button is clicked
const editButton = document.querySelector('.gr-button-primary');
const outputContainer = document.querySelector('.image-container[aria-label="Result Image"]');
if (editButton && outputContainer) {
editButton.addEventListener('click', () => {
outputContainer.classList.add('processing');
const allChildren = outputContainer.querySelectorAll('*');
allChildren.forEach(child => {
if (child.tagName !== 'IMG') {
child.style.display = 'none';
}
});
});
const imageObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && (node.tagName === 'IMG' || node.querySelector('img'))) {
outputContainer.classList.remove('processing');
imageObserver.disconnect();
}
});
}
});
});
imageObserver.observe(outputContainer, { childList: true, subtree: true });
}
// Periodically enforce hiding of processing elements
setInterval(() => {
const processingEls = document.querySelectorAll('.progress-text, .gr-progress, [class*="progress"]');
processingEls.forEach(el => el.remove());
}, 500);
});
</script>
""")
with gr.Row(elem_id="general_items"):
gr.Markdown("# Image Edit")
gr.Markdown("Edit your images with prompt descriptions", elem_id="subtitle")
with gr.Column(elem_id="input_column"):
input_image = gr.Image(
label="Input Image",
type="pil",
sources=["upload"],
show_download_button=False,
show_share_button=False,
interactive=True,
elem_classes=["gradio-component", "image-container"]
)
prompt = gr.Textbox(
label="Prompt",
lines=3,
elem_classes=["gradio-component"]
)
run_button = gr.Button(
"Edit!",
variant="primary",
elem_classes=["gradio-component", "gr-button-primary"]
)
result_image = gr.Image(
label="Result Image",
type="pil",
interactive=False,
show_download_button=True,
show_share_button=False,
elem_classes=["gradio-component", "image-container"]
)
gr.on(
triggers=[run_button.click, prompt.submit],
fn=infer,
inputs=[input_image, prompt],
outputs=[result_image],
)
return demo
if __name__ == "__main__":
logger.info(f"Gradio version: {gr.__version__}")
demo = create_demo()
demo.queue().launch(share=True)