rainey's picture
Deploy from GitHub Actions
8547a3c verified
Raw
History Blame Contribute Delete
6.13 kB
"""
🎨 Klein LoRA Studio — restyle a photo into a named look, base vs your LoRA.
Pick a named look (Scandinavian, Cottagecore, Watercolor…) and see a before→after
restyle. Optionally load your own trained `.safetensors` LoRA so the look becomes
a consistent *signature* that's truly yours. Image -> Image on FLUX.2 [klein] 4B.
Build Small (Backyard AI). Built on the klein starter's ZeroGPU + LoRA pattern.
"""
from __future__ import annotations
import os
import random
import time
# --- ZeroGPU shim: import `spaces` BEFORE torch -----------------------------
try:
import spaces # type: ignore
GPU = spaces.GPU
except Exception: # local / non-ZeroGPU fallback
def GPU(*dargs, **dkwargs): # noqa: N802
if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:
return dargs[0]
def wrap(fn):
return fn
return wrap
import gradio as gr
import torch
from diffusers import Flux2KleinPipeline
from PIL import Image
MODEL_ID = "black-forest-labs/FLUX.2-klein-4B" # 4B, Apache 2.0, ungated
STEPS = 4
GUIDANCE = 1.0
MAX_SEED = 2**31 - 1
pipe = None
LOAD_ERR = ""
try:
print(f"Loading {MODEL_ID} on CPU…")
pipe = Flux2KleinPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16)
print(" loaded.")
except Exception as e: # noqa: BLE001
LOAD_ERR = str(e)
print("Model load failed:", e)
def klein_size(w: int, h: int, target_area: int = 1024 * 1024, divisor: int = 16):
aspect = w / h
nh = int((target_area / aspect) ** 0.5)
nw = int(nh * aspect)
nw = max(divisor, (nw // divisor) * divisor)
nh = max(divisor, (nh // divisor) * divisor)
return nw, nh
LOOKS = {
"Scandinavian": "Restyle as a clean Scandinavian interior, pale wood, white "
"walls, muted neutral palette, soft daylight",
"Cottagecore": "Restyle as a cozy cottagecore scene, warm florals, vintage "
"textiles, soft golden light",
"Mid-century modern": "Restyle as a mid-century modern interior, walnut tones, "
"warm accent colours, retro furniture",
"Cyberpunk neon": "Restyle with cyberpunk neon lighting, moody atmosphere, "
"magenta and cyan glow, rain-slick reflections",
"Watercolor": "Turn it into a soft watercolor painting, gentle washes, visible "
"paper texture",
"Risograph": "Restyle as a two-colour risograph print, grainy texture, bold "
"flat inks",
}
def _apply_lora(p, path: str, scale: float) -> None:
try:
p.unload_lora_weights()
except Exception:
pass
p.load_lora_weights(path, adapter_name="user")
p.set_adapters(["user"], adapter_weights=[float(scale)])
def _unload(p) -> None:
try:
p.unload_lora_weights()
except Exception:
pass
@GPU(duration=120)
def restyle(input_image: Image.Image | None, look_key: str, lora_file, scale: float):
if pipe is None:
raise gr.Error(f"Model isn't loaded (this Space needs a GPU). {LOAD_ERR[:200]}")
if input_image is None:
raise gr.Error("Upload a photo first (or pick an example).")
pipe.to("cuda")
img = input_image.convert("RGB")
w, h = klein_size(*img.size)
if img.size != (w, h):
img = img.resize((w, h), Image.LANCZOS)
prompt = LOOKS.get(look_key, next(iter(LOOKS.values())))
seed = random.randint(0, MAX_SEED)
t = time.time()
try:
_unload(pipe)
if lora_file is not None:
path = lora_file if isinstance(lora_file, str) else lora_file.name
_apply_lora(pipe, path, scale)
tag = f"+ LoRA {os.path.basename(path)} @ {scale}"
else:
tag = "base klein 4B"
out = pipe(
prompt=prompt,
image=img, # keyword — `image` is positional-first
width=w,
height=h,
num_inference_steps=STEPS,
guidance_scale=GUIDANCE,
generator=torch.Generator(device="cuda").manual_seed(seed),
).images[0]
finally:
_unload(pipe) # leave the shared pipe clean
return img, out, f"**{look_key}** · {tag} · seed {seed} · {time.time() - t:.1f}s"
THEME = gr.themes.Soft(
font=["system-ui", "-apple-system", "Segoe UI", "Roboto", "Helvetica", "Arial", "sans-serif"],
font_mono=["ui-monospace", "SFMono-Regular", "Consolas", "monospace"],
)
CSS = """
footer {visibility: hidden;}
.gradio-container, .gradio-container .prose, .gradio-container p,
.gradio-container h1, .gradio-container h2, .gradio-container h3 {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
}
"""
with gr.Blocks(title="Klein LoRA Studio", theme=THEME, css=CSS) as demo:
gr.Markdown(
"# 🎨 Klein LoRA Studio\n"
"Drop in a photo, pick a **named look**, and see a **before → after** "
"restyle. Want the look to be *yours* and consistent across a whole set? "
"Load a `.safetensors` LoRA you trained and it becomes your **signature** "
"style. Powered by **FLUX.2 [klein] 4B** (4B params, Apache 2.0)."
)
with gr.Row():
with gr.Column():
in_img = gr.Image(type="pil", label="Your photo", height=300)
look = gr.Dropdown(list(LOOKS), value="Scandinavian", label="Look")
lora = gr.File(label="Signature LoRA (optional .safetensors)", file_types=[".safetensors"])
scale = gr.Slider(0.0, 1.5, value=1.0, step=0.05, label="LoRA strength")
btn = gr.Button("🎨 Restyle", variant="primary")
with gr.Column():
with gr.Row():
before = gr.Image(label="Before", height=320)
after = gr.Image(label="After", height=320)
info = gr.Markdown()
btn.click(restyle, [in_img, look, lora, scale], [before, after, info])
if __name__ == "__main__":
# ssr_mode=False: Gradio-5 SSR commonly renders unstyled raw HTML on Spaces.
demo.queue(max_size=8).launch(
server_name="0.0.0.0", server_port=7860, show_error=True, ssr_mode=False
)