Spaces:
Sleeping
Sleeping
Commit
·
cd11413
1
Parent(s):
b0c5ec5
Add Gradio app (app.py), requirements for Space, and README
Browse files- README_gradio.md +18 -0
- app.py +165 -0
- requirements-gradio.txt +7 -0
- requirements.txt +7 -0
README_gradio.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Run locally
|
| 2 |
+
|
| 3 |
+
1) Install core requirements from the repo (CUDA-compatible torch recommended).
|
| 4 |
+
2) Install UI deps:
|
| 5 |
+
|
| 6 |
+
python3 -m pip install -r requirements-gradio.txt
|
| 7 |
+
|
| 8 |
+
3) Ensure pretrained weights are available at `pretrained_models/` (or symlink).
|
| 9 |
+
4) Launch:
|
| 10 |
+
|
| 11 |
+
python3 app.py
|
| 12 |
+
|
| 13 |
+
Hugging Face Space
|
| 14 |
+
|
| 15 |
+
- Add `app.py` as the entry point.
|
| 16 |
+
- Add `requirements.txt` and `requirements-gradio.txt` to the Space (merge if desired).
|
| 17 |
+
- Make sure all large weights are present in `pretrained_models/` (LFS) or fetched at build time.
|
| 18 |
+
|
app.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Tuple, Dict
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
from runners.simple_runner import SimpleRunner
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# -----------------------------------------------------------------------------
|
| 13 |
+
# Logging (use lazy % formatting as requested)
|
| 14 |
+
# -----------------------------------------------------------------------------
|
| 15 |
+
logging.basicConfig(level=logging.INFO)
|
| 16 |
+
logger = logging.getLogger("sfe-app")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# -----------------------------------------------------------------------------
|
| 20 |
+
# Model bootstrap (load once and reuse)
|
| 21 |
+
# -----------------------------------------------------------------------------
|
| 22 |
+
RUNNER: SimpleRunner | None = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def get_runner() -> SimpleRunner:
|
| 26 |
+
global RUNNER
|
| 27 |
+
if RUNNER is None:
|
| 28 |
+
logger.info("Initializing SimpleRunner with %s", "pretrained_models/sfe_editor_light.pt")
|
| 29 |
+
RUNNER = SimpleRunner(
|
| 30 |
+
editor_ckpt_pth="pretrained_models/sfe_editor_light.pt",
|
| 31 |
+
)
|
| 32 |
+
return RUNNER
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# -----------------------------------------------------------------------------
|
| 36 |
+
# Attribute catalog and recommended ranges
|
| 37 |
+
# -----------------------------------------------------------------------------
|
| 38 |
+
# Each entry maps a friendly attribute name to the internal editing name and a
|
| 39 |
+
# recommended power range for the slider.
|
| 40 |
+
ATTRIBUTE_MAP: Dict[str, Tuple[str, Tuple[float, float]]] = {
|
| 41 |
+
# Face semantics
|
| 42 |
+
"Smile": ("fs_smiling", (-10.0, 10.0)),
|
| 43 |
+
"Age": ("age", (-10.0, 10.0)), # interfacegan_directions
|
| 44 |
+
"Female features": ("gender", (-10.0, 7.0)), # stylespace_directions (positive adds femininity)
|
| 45 |
+
|
| 46 |
+
# Facial hair
|
| 47 |
+
# trimmed_beard removes beard for positive power; use negative to add
|
| 48 |
+
"Beard": ("trimmed_beard", (-30.0, 30.0)),
|
| 49 |
+
# goatee removes goatee for positive; negative tends to add
|
| 50 |
+
"Mustache/Goatee": ("goatee", (-7.0, 7.0)),
|
| 51 |
+
|
| 52 |
+
# Accessories & cosmetics
|
| 53 |
+
"Glasses": ("fs_glasses", (-20.0, 30.0)),
|
| 54 |
+
"Makeup": ("fs_makeup", (-10.0, 15.0)),
|
| 55 |
+
|
| 56 |
+
# Hair style (pretrained mappers)
|
| 57 |
+
"Curly hair": ("curly_hair", (0.0, 0.12)), # styleclip_directions
|
| 58 |
+
"Afro": ("afro", (0.0, 0.14)),
|
| 59 |
+
|
| 60 |
+
# Hair color via global text mapper
|
| 61 |
+
# You can also type custom prompts below
|
| 62 |
+
"Orange hair (text)": ("styleclip_global_a face_a face with orange hair_0.18", (0.0, 0.2)),
|
| 63 |
+
"Blonde hair (text)": ("styleclip_global_a face_a face with blonde hair_0.18", (0.0, 0.2)),
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def recommended_range(attr_name: str) -> Tuple[float, float]:
|
| 68 |
+
edit_name, rng = ATTRIBUTE_MAP[attr_name]
|
| 69 |
+
return rng
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def run_edit(
|
| 73 |
+
image: Image.Image,
|
| 74 |
+
attribute: str,
|
| 75 |
+
strength: float,
|
| 76 |
+
align_face: bool,
|
| 77 |
+
use_bg_mask: bool,
|
| 78 |
+
custom_text_edit: str,
|
| 79 |
+
) -> Image.Image:
|
| 80 |
+
"""Run a single attribute edit and return the edited image."""
|
| 81 |
+
runner = get_runner()
|
| 82 |
+
|
| 83 |
+
# Determine editing name and clip strength into the suggested range
|
| 84 |
+
edit_name, (lo, hi) = ATTRIBUTE_MAP[attribute]
|
| 85 |
+
if custom_text_edit and attribute.endswith("(text)"):
|
| 86 |
+
# Allow overriding the default text prompt
|
| 87 |
+
if custom_text_edit.strip():
|
| 88 |
+
edit_name = custom_text_edit.strip()
|
| 89 |
+
|
| 90 |
+
clipped_strength = max(lo, min(hi, strength))
|
| 91 |
+
if clipped_strength != strength:
|
| 92 |
+
logger.info("Clipped strength from %s to %s for %s", strength, clipped_strength, attribute)
|
| 93 |
+
|
| 94 |
+
# Persist input to a temp file for the runner
|
| 95 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 96 |
+
inp_path = os.path.join(tmpdir, "input.jpg")
|
| 97 |
+
out_path = os.path.join(tmpdir, "edited.jpg")
|
| 98 |
+
image.convert("RGB").save(inp_path)
|
| 99 |
+
|
| 100 |
+
logger.info("Editing %s with power %s", edit_name, clipped_strength)
|
| 101 |
+
_ = runner.edit(
|
| 102 |
+
orig_img_pth=inp_path,
|
| 103 |
+
editing_name=edit_name,
|
| 104 |
+
edited_power=clipped_strength,
|
| 105 |
+
save_pth=out_path,
|
| 106 |
+
align=align_face,
|
| 107 |
+
use_mask=use_bg_mask,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
return Image.open(out_path).convert("RGB")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def build_ui() -> gr.Blocks:
|
| 114 |
+
with gr.Blocks(css="footer {visibility: hidden}") as demo:
|
| 115 |
+
gr.Markdown("""
|
| 116 |
+
**StyleFeatureEditor – Facial Attribute Editing**
|
| 117 |
+
Upload a face and apply edits like smile, age, beard, hair style/color, glasses, and makeup.
|
| 118 |
+
Tip: For Beard/Goatee, negative strength tends to add facial hair.
|
| 119 |
+
""")
|
| 120 |
+
|
| 121 |
+
with gr.Row():
|
| 122 |
+
with gr.Column():
|
| 123 |
+
inp = gr.Image(type="pil", label="Input face", sources=["upload", "clipboard"])
|
| 124 |
+
attr = gr.Dropdown(
|
| 125 |
+
choices=list(ATTRIBUTE_MAP.keys()),
|
| 126 |
+
value="Smile",
|
| 127 |
+
label="Attribute",
|
| 128 |
+
)
|
| 129 |
+
strength = gr.Slider(-15, 15, value=5, step=0.01, label="Strength (p)")
|
| 130 |
+
align_face = gr.Checkbox(value=False, label="Align face before editing")
|
| 131 |
+
use_bg_mask = gr.Checkbox(value=False, label="Use background mask (reduce artifacts)")
|
| 132 |
+
custom_text = gr.Textbox(
|
| 133 |
+
value="",
|
| 134 |
+
label="Custom text edit (StyleCLIP Global Mapper)",
|
| 135 |
+
placeholder="styleclip_global_a face_a face with black hair_0.18",
|
| 136 |
+
)
|
| 137 |
+
run_btn = gr.Button("Run edit")
|
| 138 |
+
|
| 139 |
+
with gr.Column():
|
| 140 |
+
out = gr.Image(type="pil", label="Edited output")
|
| 141 |
+
|
| 142 |
+
# Update slider range based on attribute selection
|
| 143 |
+
def _on_attr_change(name: str):
|
| 144 |
+
lo, hi = recommended_range(name)
|
| 145 |
+
# Keep current value within new bounds
|
| 146 |
+
new_val = max(lo, min(hi, strength.value if hasattr(strength, "value") else 0))
|
| 147 |
+
return gr.Slider.update(minimum=lo, maximum=hi, value=new_val)
|
| 148 |
+
|
| 149 |
+
attr.change(_on_attr_change, inputs=attr, outputs=strength)
|
| 150 |
+
|
| 151 |
+
run_btn.click(
|
| 152 |
+
fn=run_edit,
|
| 153 |
+
inputs=[inp, attr, strength, align_face, use_bg_mask, custom_text],
|
| 154 |
+
outputs=out,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
return demo
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
app = build_ui()
|
| 162 |
+
# On Spaces, the port/host are managed by the platform; run local defaults otherwise
|
| 163 |
+
app.launch()
|
| 164 |
+
|
| 165 |
+
|
requirements-gradio.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.44.0
|
| 2 |
+
numpy>=1.23
|
| 3 |
+
Pillow>=9.5
|
| 4 |
+
torch
|
| 5 |
+
torchvision
|
| 6 |
+
omegaconf==2.1.2
|
| 7 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.44.0
|
| 2 |
+
numpy>=1.23
|
| 3 |
+
Pillow>=9.5
|
| 4 |
+
torch
|
| 5 |
+
torchvision
|
| 6 |
+
omegaconf==2.1.2
|
| 7 |
+
|