MANGAMORPH / app.py
OLIVE2403's picture
Update app.py
5549415 verified
# app.py — MangaMorph (Gradio) — backwards-compatible, CPU-friendly
import os
import random
import time
import numpy as np
from PIL import Image, ImageOps
import gradio as gr
import torch
from diffusers import DiffusionPipeline, EulerDiscreteScheduler
# ---------- CONFIG ----------
MODEL_ID = os.getenv("MODEL_ID", "hakurei/waifu-diffusion") # change if needed
HF_TOKEN = os.getenv("HUGGINGFACE_HUB_TOKEN", None)
device = "cuda" if torch.cuda.is_available() else "cpu"
torch_dtype = torch.float16 if device == "cuda" else torch.float32
# CPU-friendly defaults & limits
DEFAULT_WIDTH = 384
DEFAULT_HEIGHT = 384
DEFAULT_STEPS = 10
DEFAULT_GUIDANCE = 5.5
MAX_SEED = np.iinfo(np.int32).max
# ---------- Load pipeline (lazy) ----------
PIPE = None
def load_pipeline():
global PIPE
if PIPE is not None:
return PIPE
try:
pipe = DiffusionPipeline.from_pretrained(
MODEL_ID,
torch_dtype=torch_dtype,
use_auth_token=HF_TOKEN,
)
# Try to set EulerDiscreteScheduler if provided by model repo
try:
scheduler = EulerDiscreteScheduler.from_pretrained(MODEL_ID, subfolder="scheduler")
pipe.scheduler = scheduler
except Exception:
pass
pipe = pipe.to(device)
# Optional: disable safety checker on CPU for speed (non-ideal but common)
try:
pipe.safety_checker = None
except Exception:
pass
PIPE = pipe
return PIPE
except Exception as e:
raise RuntimeError(f"Model load failed: {e}")
# ---------- Helpers ----------
DEFAULT_NEG = (
"low quality, bad anatomy, blurry, extra limbs, malformed, deformed, "
"watermark, text, signature, lowres"
)
def tidy_image(img: Image.Image, max_side=1024):
img = img.convert("RGB")
if max(img.size) > max_side:
img = ImageOps.contain(img, (max_side, max_side))
return img
# ---------- Inference ----------
def infer(
prompt: str,
negative_prompt: str,
seed: int,
randomize_seed: bool,
width: int,
height: int,
guidance_scale: float,
num_inference_steps: int,
):
start = time.time()
if not prompt or prompt.strip() == "":
return None, "Enter a prompt."
if randomize_seed or int(seed) == 0:
seed = random.randint(0, MAX_SEED)
else:
seed = int(seed) % MAX_SEED
try:
pipe = load_pipeline()
except Exception as e:
return None, f"Model load error: {e}"
# enforce CPU-friendly caps
width = int(min(max(256, width), 512))
height = int(min(max(256, height), 512))
steps = int(min(max(4, num_inference_steps), 20))
gen = torch.Generator(device=device).manual_seed(seed)
try:
out = pipe(
prompt=prompt,
negative_prompt=(negative_prompt or DEFAULT_NEG),
width=width,
height=height,
guidance_scale=float(guidance_scale),
num_inference_steps=steps,
generator=gen,
)
image = tidy_image(out.images[0], max_side=1024)
elapsed = time.time() - start
return image, f"Done — Seed: {seed}{int(elapsed)}s"
except Exception:
# lighter retry
try:
out = pipe(
prompt=prompt,
negative_prompt=(negative_prompt or DEFAULT_NEG),
width=width,
height=height,
guidance_scale=max(3.0, float(guidance_scale) - 1.0),
num_inference_steps=max(4, steps - 4),
generator=gen,
)
image = tidy_image(out.images[0], max_side=1024)
elapsed = time.time() - start
return image, f"Recovered (retry) — Seed: {seed}{int(elapsed)}s"
except Exception as e2:
return None, f"Generation failed: {e2}"
# ---------- UI (compatible with older/newer Gradio) ----------
css = """
/* Vibrant purple-pink gradient background */
body, .gradio-container {
background: linear-gradient(135deg, #d946ef 0%, #a855f7 25%, #8b5cf6 50%, #7c3aed 75%, #6366f1 100%) !important;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
color: #ffffff !important;
min-height: 100vh;
}
/* Main container styling */
.contain, .gradio-container > div {
background: transparent !important;
}
/* Header card - bright gradient with glow */
.header {
padding: 20px 24px;
border-radius: 16px;
background: linear-gradient(135deg, #ec4899 0%, #f97316 50%, #facc15 100%);
color: white;
box-shadow: 0 8px 32px rgba(236, 72, 153, 0.4), 0 0 60px rgba(249, 115, 22, 0.3);
margin-bottom: 20px;
}
/* Brand/title */
.brand {
font-weight: 900;
font-size: 28px;
letter-spacing: 0.5px;
color: #fff;
text-shadow: 2px 2px 8px rgba(0,0,0,0.3);
}
/* Subtitle under brand */
.small {
font-size: 14px;
color: rgba(255,255,255,0.95);
margin-top: 8px;
font-weight: 500;
}
/* All blocks and containers - vibrant semi-transparent cards */
.gr-block, .gr-box, .gr-form, .gr-panel {
background: rgba(255, 255, 255, 0.15) !important;
backdrop-filter: blur(10px) !important;
border-radius: 16px !important;
border: 2px solid rgba(255, 255, 255, 0.25) !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1) !important;
padding: 16px !important;
}
/* Input fields - bright with good contrast */
.gr-textbox, .gr-input, textarea, input {
background: rgba(255, 255, 255, 0.95) !important;
color: #1f2937 !important;
border: 2px solid rgba(236, 72, 153, 0.3) !important;
border-radius: 12px !important;
padding: 12px !important;
font-size: 15px !important;
font-weight: 500 !important;
}
.gr-textbox::placeholder, textarea::placeholder, input::placeholder {
color: rgba(31, 41, 55, 0.5) !important;
}
/* Labels - bright and visible */
label, .gr-label, .gr-box label {
color: #ffffff !important;
font-weight: 700 !important;
font-size: 15px !important;
text-shadow: 1px 1px 3px rgba(0,0,0,0.3) !important;
margin-bottom: 8px !important;
}
/* Buttons - super vibrant gradient */
.gr-button, button {
background: linear-gradient(135deg, #ff0080 0%, #ff8c00 50%, #ffd700 100%) !important;
color: white !important;
font-weight: 800 !important;
border: none !important;
box-shadow: 0 6px 24px rgba(255, 0, 128, 0.4), 0 0 40px rgba(255, 140, 0, 0.3) !important;
border-radius: 12px !important;
padding: 14px 24px !important;
font-size: 16px !important;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.3s ease !important;
}
.gr-button:hover, button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(255, 0, 128, 0.6), 0 0 60px rgba(255, 140, 0, 0.5) !important;
}
/* Sliders - bright colors */
.gr-slider input[type="range"] {
background: rgba(255, 255, 255, 0.2) !important;
}
.gr-slider input[type="range"]::-webkit-slider-thumb {
background: linear-gradient(135deg, #ff0080, #ff8c00) !important;
border: 3px solid white !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
}
/* Accordion - vibrant */
.gr-accordion {
background: rgba(255, 255, 255, 0.1) !important;
border: 2px solid rgba(255, 255, 255, 0.2) !important;
border-radius: 12px !important;
}
.gr-accordion summary {
color: #ffffff !important;
font-weight: 700 !important;
background: rgba(236, 72, 153, 0.3) !important;
padding: 12px !important;
border-radius: 10px !important;
}
/* Image containers - bright white background */
.gr-image, .gr-gallery {
background: rgba(255, 255, 255, 0.95) !important;
border-radius: 12px !important;
padding: 12px !important;
border: 2px solid rgba(236, 72, 153, 0.3) !important;
}
/* Status textbox - bright and visible */
.gr-textbox[aria-label="Status"] {
background: rgba(255, 255, 255, 0.9) !important;
color: #1f2937 !important;
border: 2px solid rgba(16, 185, 129, 0.5) !important;
font-weight: 600 !important;
}
/* Examples - vibrant cards */
.gr-examples {
background: rgba(255, 255, 255, 0.1) !important;
border-radius: 12px !important;
padding: 12px !important;
}
.gr-examples .gr-button {
background: rgba(139, 92, 246, 0.8) !important;
font-size: 13px !important;
padding: 10px 16px !important;
}
/* Checkbox styling */
.gr-checkbox {
color: #ffffff !important;
}
.gr-checkbox input[type="checkbox"] {
border: 2px solid rgba(255, 255, 255, 0.5) !important;
background: rgba(255, 255, 255, 0.2) !important;
}
/* Number inputs */
.gr-number input {
background: rgba(255, 255, 255, 0.95) !important;
color: #1f2937 !important;
}
/* Markdown text */
.gr-markdown, .markdown {
color: #ffffff !important;
}
.gr-markdown strong {
color: #fbbf24 !important;
font-weight: 800 !important;
}
/* Mobile-friendly adjustments */
@media (max-width: 720px) {
.header { text-align: center; }
.brand { font-size: 24px; }
.gr-button, button { font-size: 14px !important; padding: 12px 20px !important; }
}
"""
examples = [
"anime girl standing on a cherry-blossom bridge at sunset, cinematic lighting, detailed eyes",
"young samurai on a misty mountain path, dramatic clouds, anime style",
"cozy studio apartment with anime character reading by window, warm lighting",
]
with gr.Blocks(css=css, title="MangaMorph — Anime Scene Generator") as demo:
with gr.Row():
with gr.Column(scale=2):
gr.Markdown(
"<div class='header'><div class='brand'>MangaMorph</div>"
"<div class='small'>Text → Anime image • CPU-optimized • Try 384×384 & 10 steps for speed</div></div>"
)
prompt = gr.Textbox(lines=3, label="Describe your anime scene", placeholder="e.g. A cyberpunk anime girl on a rainy street...")
with gr.Row():
run_btn = gr.Button("Generate")
download_btn = gr.Button("Download")
with gr.Accordion("Advanced settings", open=False):
negative = gr.Textbox(lines=2, label="Negative prompt (optional)", value=DEFAULT_NEG)
with gr.Row():
seed = gr.Number(label="Seed (0 = random)", value=0)
randomize = gr.Checkbox(label="Randomize seed", value=True)
with gr.Row():
width = gr.Slider(label="Width", minimum=256, maximum=512, step=64, value=DEFAULT_WIDTH)
height = gr.Slider(label="Height", minimum=256, maximum=512, step=64, value=DEFAULT_HEIGHT)
with gr.Row():
guidance = gr.Slider(label="Guidance scale", minimum=1.0, maximum=12.0, step=0.1, value=DEFAULT_GUIDANCE)
steps = gr.Slider(label="Steps", minimum=4, maximum=20, step=1, value=DEFAULT_STEPS)
gr.Examples(examples=examples, inputs=[prompt], label="Try examples")
status = gr.Textbox(label="Status", value="Ready", interactive=False)
with gr.Column(scale=1):
gr.Markdown("**Preview**")
result = gr.Image(label="Generated image")
gallery = gr.Gallery(label="History (latest first)", columns=1)
gr.Markdown("<div style='font-size:12px;color:#fff;margin-top:6px;font-weight:600;'>Tip: Use lower resolution & fewer steps for faster results on CPU</div>")
def generate_and_update(prompt_text, negative_prompt_text, seed_val, randomize_val, w, h, g, s):
img, msg = infer(prompt_text, negative_prompt_text, seed_val, randomize_val, w, h, g, s)
history = [] if img is None else [img]
return img, msg, history
run_btn.click(
fn=generate_and_update,
inputs=[prompt, negative, seed, randomize, width, height, guidance, steps],
outputs=[result, status, gallery],
show_progress=True,
)
def download_current(image):
# return image to trigger download
return image
download_btn.click(fn=download_current, inputs=[result], outputs=[result])
if __name__ == "__main__":
demo.launch()