|
|
try: |
|
|
import spaces |
|
|
SPACES_AVAILABLE = True |
|
|
print("✅ Spaces available - ZeroGPU mode") |
|
|
except ImportError: |
|
|
SPACES_AVAILABLE = False |
|
|
print("⚠️ Spaces not available - running in regular mode") |
|
|
|
|
|
import gradio as gr |
|
|
import torch |
|
|
from diffusers import StableDiffusionPipeline |
|
|
from PIL import Image |
|
|
import datetime |
|
|
import io |
|
|
import json |
|
|
import os |
|
|
from typing import Optional |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BASE_MODEL = "SG161222/RealisticVisionV6.0" |
|
|
|
|
|
|
|
|
FIXED_LORAS = [ |
|
|
("Lykon/epiCRealism_LoRA", 0.8), |
|
|
("latent-consistency/lora-dreamshaper", 0.7), |
|
|
] |
|
|
|
|
|
|
|
|
STYLE_PROMPTS = { |
|
|
"None": "", |
|
|
"Realistic": "photorealistic, ultra-detailed skin, natural lighting, 8k, professional photography, f/1.8, shallow depth of field, Canon EOS R5, ", |
|
|
"Anime": "anime style, cel shading, vibrant colors, detailed eyes, studio ghibli, trending on pixiv, ", |
|
|
"Comic": "comic book style, bold outlines, dynamic angles, comic panel, Marvel style, inked lines, ", |
|
|
"Watercolor": "watercolor painting, soft brush strokes, translucent layers, artistic, painterly, paper texture, ", |
|
|
} |
|
|
|
|
|
|
|
|
OPTIONAL_LORAS = [ |
|
|
"None", |
|
|
"Add Detail: https://huggingface.co/latent-consistency/lora-add-detail", |
|
|
"Vintage Photo: https://huggingface.co/ckpt/LoRA-vintage-photo", |
|
|
"Cinematic: https://huggingface.co/latent-consistency/lora-cinematic", |
|
|
"Portrait Enhancer: https://huggingface.co/deforum/Portrait-Enhancer-LoRA", |
|
|
"Soft Focus: https://huggingface.co/latent-consistency/lora-soft-focus", |
|
|
] |
|
|
|
|
|
|
|
|
OPTIONAL_LORA_MAP = {} |
|
|
for item in OPTIONAL_LORAS: |
|
|
if item != "None": |
|
|
name, url = item.split(": ", 1) |
|
|
OPTIONAL_LORA_MAP[name] = url |
|
|
else: |
|
|
OPTIONAL_LORA_MAP["None"] = None |
|
|
|
|
|
|
|
|
DEFAULT_SEED = -1 |
|
|
DEFAULT_WIDTH = 1024 |
|
|
DEFAULT_HEIGHT = 1024 |
|
|
DEFAULT_LORA_SCALE = 0.8 |
|
|
DEFAULT_STEPS = 30 |
|
|
DEFAULT_CFG = 7.5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pipe = None |
|
|
device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
|
|
|
def load_pipeline(): |
|
|
global pipe |
|
|
if pipe is None: |
|
|
print("🚀 Loading base model...") |
|
|
pipe = StableDiffusionPipeline.from_pretrained( |
|
|
BASE_MODEL, |
|
|
torch_dtype=torch.float16, |
|
|
safety_checker=None, |
|
|
requires_safety_checker=False, |
|
|
).to(device) |
|
|
pipe.enable_attention_slicing() |
|
|
pipe.enable_vae_slicing() |
|
|
pipe.enable_model_cpu_offload() |
|
|
print("✅ Base model loaded.") |
|
|
return pipe |
|
|
|
|
|
def unload_pipeline(): |
|
|
global pipe |
|
|
if pipe is not None: |
|
|
del pipe |
|
|
torch.cuda.empty_cache() |
|
|
pipe = None |
|
|
print("🗑️ Pipeline unloaded.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_image( |
|
|
prompt, negative_prompt, style, seed, width, height, optional_lora_name, lora_scale, |
|
|
steps, cfg_scale |
|
|
): |
|
|
global pipe |
|
|
|
|
|
|
|
|
pipe = load_pipeline() |
|
|
|
|
|
|
|
|
if seed == -1: |
|
|
seed = torch.randint(0, 2**32, (1,)).item() |
|
|
generator = torch.Generator(device=device).manual_seed(seed) |
|
|
|
|
|
|
|
|
full_prompt = STYLE_PROMPTS[style] + prompt |
|
|
full_negative_prompt = negative_prompt |
|
|
|
|
|
|
|
|
for lora_id, scale in FIXED_LORAS: |
|
|
pipe.load_lora_weights(lora_id, adapter_name=lora_id) |
|
|
pipe.set_adapters([lora_id], adapter_weights=[scale]) |
|
|
|
|
|
|
|
|
if optional_lora_name != "None": |
|
|
lora_url = OPTIONAL_LORA_MAP[optional_lora_name] |
|
|
pipe.load_lora_weights(lora_url, adapter_name=optional_lora_name) |
|
|
pipe.set_adapters([lora_id for lora_id, _ in FIXED_LORAS] + [optional_lora_name], |
|
|
adapter_weights=[scale for _, scale in FIXED_LORAS] + [lora_scale]) |
|
|
else: |
|
|
|
|
|
pipe.set_adapters([lora_id for lora_id, _ in FIXED_LORAS], |
|
|
adapter_weights=[scale for _, scale in FIXED_LORAS]) |
|
|
|
|
|
|
|
|
image = pipe( |
|
|
prompt=full_prompt, |
|
|
negative_prompt=full_negative_prompt, |
|
|
num_inference_steps=steps, |
|
|
guidance_scale=cfg_scale, |
|
|
width=width, |
|
|
height=height, |
|
|
generator=generator, |
|
|
).images[0] |
|
|
|
|
|
|
|
|
metadata = { |
|
|
"prompt": full_prompt, |
|
|
"negative_prompt": full_negative_prompt, |
|
|
"base_model": BASE_MODEL, |
|
|
"fixed_loras": [lora_id for lora_id, _ in FIXED_LORAS], |
|
|
"optional_lora": optional_lora_name if optional_lora_name != "None" else None, |
|
|
"lora_scale": lora_scale, |
|
|
"seed": seed, |
|
|
"steps": steps, |
|
|
"cfg_scale": cfg_scale, |
|
|
"style": style, |
|
|
"width": width, |
|
|
"height": height, |
|
|
"timestamp": datetime.datetime.now().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
timestamp = datetime.datetime.now().strftime("%y%m%d%H%M") |
|
|
filename_base = f"{seed}-{timestamp}" |
|
|
|
|
|
|
|
|
img_buffer = io.BytesIO() |
|
|
image.save(img_buffer, format="WEBP", quality=95, method=6) |
|
|
img_buffer.seek(0) |
|
|
|
|
|
|
|
|
metadata_buffer = io.StringIO() |
|
|
json.dump(metadata, metadata_buffer, indent=2, ensure_ascii=False) |
|
|
metadata_buffer.seek(0) |
|
|
|
|
|
|
|
|
return ( |
|
|
image, |
|
|
json.dumps(metadata, indent=2, ensure_ascii=False), |
|
|
f"{filename_base}.webp", |
|
|
f"{filename_base}.txt", |
|
|
img_buffer.getvalue(), |
|
|
metadata_buffer.getvalue().encode('utf-8') |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
theme=gr.themes.Soft( |
|
|
primary_hue="blue", |
|
|
secondary_hue="green", |
|
|
neutral_hue="slate", |
|
|
).set( |
|
|
body_background_fill="linear-gradient(135deg, #1e40af, #059669)", |
|
|
button_primary_background_fill="white", |
|
|
button_primary_text_color="#1e40af", |
|
|
input_background_fill="rgba(255,255,255,0.9)", |
|
|
text_size="lg", |
|
|
), |
|
|
css=""" |
|
|
body { font-family: 'Helvetica Neue', 'Segoe UI', 'Arial', sans-serif; } |
|
|
.gr-button { font-family: 'Helvetica Neue', 'Arial', sans-serif; font-weight: 500; } |
|
|
.gr-textarea { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; } |
|
|
""", |
|
|
) as demo: |
|
|
gr.Markdown( |
|
|
""" |
|
|
# 🎨 AI Photo Generator (RealisticVision + LoRA) |
|
|
**PRO + ZeroGPU Optimized | Multi-LoRA | Style Templates | Metadata Export** |
|
|
""" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=3): |
|
|
|
|
|
prompt_input = gr.Textbox( |
|
|
label="Prompt (Positive)", |
|
|
placeholder="A beautiful woman, golden hour, soft sunlight...", |
|
|
lines=5, |
|
|
max_lines=20, |
|
|
elem_classes=["gr-textarea"] |
|
|
) |
|
|
|
|
|
|
|
|
negative_prompt_input = gr.Textbox( |
|
|
label="Negative Prompt", |
|
|
placeholder="blurry, low quality, deformed, cartoon, anime, text, watermark...", |
|
|
lines=5, |
|
|
max_lines=20, |
|
|
elem_classes=["gr-textarea"] |
|
|
) |
|
|
|
|
|
|
|
|
style_radio = gr.Radio( |
|
|
choices=list(STYLE_PROMPTS.keys()), |
|
|
label="Style", |
|
|
value="Realistic", |
|
|
elem_classes=["gr-radio"] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
seed_input = gr.Slider( |
|
|
minimum=-1, |
|
|
maximum=99999999, |
|
|
step=1, |
|
|
value=DEFAULT_SEED, |
|
|
label="Seed (-1 = Random)" |
|
|
) |
|
|
seed_reset = gr.Button("Reset Seed") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
width_input = gr.Slider( |
|
|
minimum=512, |
|
|
maximum=1536, |
|
|
step=64, |
|
|
value=DEFAULT_WIDTH, |
|
|
label="Width" |
|
|
) |
|
|
width_reset = gr.Button("Reset Width") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
height_input = gr.Slider( |
|
|
minimum=512, |
|
|
maximum=1536, |
|
|
step=64, |
|
|
value=DEFAULT_HEIGHT, |
|
|
label="Height" |
|
|
) |
|
|
height_reset = gr.Button("Reset Height") |
|
|
|
|
|
|
|
|
optional_lora_dropdown = gr.Dropdown( |
|
|
choices=list(OPTIONAL_LORA_MAP.keys()), |
|
|
label="Optional LoRA", |
|
|
value="None", |
|
|
elem_classes=["gr-dropdown"] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
lora_scale_slider = gr.Slider( |
|
|
minimum=0.0, |
|
|
maximum=1.5, |
|
|
step=0.05, |
|
|
value=DEFAULT_LORA_SCALE, |
|
|
label="LoRA Scale" |
|
|
) |
|
|
lora_reset = gr.Button("Reset LoRA Scale") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
steps_slider = gr.Slider( |
|
|
minimum=10, |
|
|
maximum=100, |
|
|
step=1, |
|
|
value=DEFAULT_STEPS, |
|
|
label="Steps" |
|
|
) |
|
|
cfg_slider = gr.Slider( |
|
|
minimum=1.0, |
|
|
maximum=20.0, |
|
|
step=0.5, |
|
|
value=DEFAULT_CFG, |
|
|
label="CFG Scale" |
|
|
) |
|
|
gen_reset = gr.Button("Reset Generation") |
|
|
|
|
|
|
|
|
generate_btn = gr.Button("✨ Generate Image", variant="primary", size="lg") |
|
|
|
|
|
with gr.Column(scale=2): |
|
|
|
|
|
image_output = gr.Image(label="Generated Image", height=768, format="webp") |
|
|
|
|
|
|
|
|
metadata_output = gr.Textbox( |
|
|
label="Metadata (JSON)", |
|
|
lines=12, |
|
|
max_lines=20, |
|
|
elem_classes=["gr-textarea"] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
download_img_btn = gr.Button("⬇️ Download Image (WebP)") |
|
|
download_meta_btn = gr.Button("⬇️ Download Metadata (TXT)") |
|
|
|
|
|
|
|
|
hidden_img_file = gr.File(visible=False) |
|
|
hidden_meta_file = gr.File(visible=False) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
seed_reset.click(fn=lambda: -1, outputs=seed_input) |
|
|
|
|
|
width_reset.click(fn=lambda: DEFAULT_WIDTH, outputs=width_input) |
|
|
|
|
|
height_reset.click(fn=lambda: DEFAULT_HEIGHT, outputs=height_input) |
|
|
|
|
|
lora_reset.click(fn=lambda: DEFAULT_LORA_SCALE, outputs=lora_scale_slider) |
|
|
|
|
|
gen_reset.click( |
|
|
fn=lambda: (DEFAULT_STEPS, DEFAULT_CFG), |
|
|
outputs=[steps_slider, cfg_slider] |
|
|
) |
|
|
|
|
|
|
|
|
generate_btn.click( |
|
|
fn=generate_image, |
|
|
inputs=[ |
|
|
prompt_input, negative_prompt_input, style_radio, |
|
|
seed_input, width_input, height_input, |
|
|
optional_lora_dropdown, lora_scale_slider, |
|
|
steps_slider, cfg_slider |
|
|
], |
|
|
outputs=[ |
|
|
image_output, metadata_output, |
|
|
hidden_img_file, hidden_meta_file, |
|
|
hidden_img_file, hidden_meta_file |
|
|
] |
|
|
) |
|
|
|
|
|
|
|
|
download_img_btn.click( |
|
|
fn=None, |
|
|
inputs=[hidden_img_file], |
|
|
outputs=None, |
|
|
js="(f) => { const a = document.createElement('a'); a.href = f; a.download = f.split('/').pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); }" |
|
|
) |
|
|
|
|
|
|
|
|
download_meta_btn.click( |
|
|
fn=None, |
|
|
inputs=[hidden_meta_file], |
|
|
outputs=None, |
|
|
js="(f) => { const a = document.createElement('a'); a.href = f; a.download = f.split('/').pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); }" |
|
|
) |
|
|
|
|
|
|
|
|
generate_btn.change( |
|
|
fn=lambda img_bytes, meta_bytes, img_name, meta_name: ( |
|
|
gr.File(value=io.BytesIO(img_bytes), label=img_name, visible=True), |
|
|
gr.File(value=io.BytesIO(meta_bytes), label=meta_name, visible=True) |
|
|
), |
|
|
inputs=[hidden_img_file, hidden_meta_file, hidden_img_file, hidden_meta_file], |
|
|
outputs=[hidden_img_file, hidden_meta_file] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch() |
|
|
``` |