Spaces:
Running on Zero
Running on Zero
Sking image-to-Minecraft-skin pipeline (AGPL-3.0)
Browse filesTwo-stage reproduction of EntropyDrop's open image-to-skin path.
stage 1 FLUX.2 [klein] 4B Base + sking_v73_flux_4b_000027000 LoRA
stage 2 EntropyDrop SKING_DDJ_v104 (BiRefNet foreground + Dense UV Parser)
Stage 2 runs in-process via runpy rather than subprocess so it works on
ZeroGPU, where child processes get no CUDA context. Dependency pins are
deviated from the bundle's tested_environments.json in three places to keep
the resolver satisfiable alongside diffusers==0.40.0; see DEPLOY.md.
- app.py +139 -112
- sking_pipeline/weights.py +14 -0
app.py
CHANGED
|
@@ -1,22 +1,18 @@
|
|
| 1 |
"""Gradio front end for the Sking image -> Minecraft skin pipeline.
|
| 2 |
|
| 3 |
Startup order is load-bearing:
|
| 4 |
-
0. import spaces
|
| 5 |
-
before anything touches torch)
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
# --- 0. `spaces` MUST be imported before torch ------------------------------
|
| 12 |
-
# On ZeroGPU there is no GPU at container start. The `spaces` package installs a
|
| 13 |
-
# PyTorch CUDA *emulation* shim when it is imported, which is what allows
|
| 14 |
-
# `.to("cuda")` at module scope to succeed and be materialised later when a real
|
| 15 |
-
# GPU is attached inside an @spaces.GPU function. Importing torch first (or
|
| 16 |
-
# touching cuda before this import) fails with:
|
| 17 |
-
# UserWarning: Can't initialize NVML
|
| 18 |
-
# RuntimeError: No CUDA GPUs are available
|
| 19 |
-
# so this block has to stay above the weights bootstrap, which pulls in torch.
|
| 20 |
try:
|
| 21 |
import spaces # type: ignore
|
| 22 |
|
|
@@ -32,51 +28,109 @@ except ImportError: # dedicated-GPU Docker Space, or a local run
|
|
| 32 |
|
| 33 |
ZERO_GPU = False
|
| 34 |
|
| 35 |
-
#
|
| 36 |
-
from sking_pipeline import weights # must come after `spaces`, before torch
|
| 37 |
-
|
| 38 |
-
BOOT = weights.bootstrap(progress=lambda m: print(m, flush=True))
|
| 39 |
-
|
| 40 |
import html # noqa: E402
|
| 41 |
import tempfile # noqa: E402
|
|
|
|
|
|
|
| 42 |
from pathlib import Path # noqa: E402
|
| 43 |
|
| 44 |
import gradio as gr # noqa: E402
|
| 45 |
from PIL import Image # noqa: E402
|
| 46 |
|
| 47 |
-
from sking_pipeline import stage1_flux, stage2_uv # noqa: E402
|
| 48 |
-
|
| 49 |
-
# --- 2. load stage 1 at module scope, if this container can hold it ---------
|
| 50 |
-
# FLUX.2 [klein] 4B Base needs ~16 GB of host RAM (8.05 GB Mistral text encoder
|
| 51 |
-
# + 7.75 GB transformer, held simultaneously). The free ZeroGPU container has
|
| 52 |
-
# 16 GB total and gets OOM-killed during weight materialisation -- no traceback,
|
| 53 |
-
# the process just dies. weights.stage1_viable() gates on measured RAM so the
|
| 54 |
-
# Space still boots and serves stage 2, which is the byte-identical production
|
| 55 |
-
# UV parser and fits easily.
|
| 56 |
-
STAGE1_OK = bool(BOOT.get("stage1_viable")) and BOOT.get("flux") is not None
|
| 57 |
-
if STAGE1_OK:
|
| 58 |
-
stage1_flux.load(
|
| 59 |
-
BOOT["flux"], BOOT["lora"], progress=lambda m: print(m, flush=True)
|
| 60 |
-
)
|
| 61 |
-
else:
|
| 62 |
-
print(
|
| 63 |
-
f"stage 1 disabled: {BOOT.get('stage1_reason')}\n"
|
| 64 |
-
" stage 2 (SKING_DDJ_v104 render -> 64x64 skin) remains available.\n"
|
| 65 |
-
" to enable stage 1, move to a GPU tier with >= 20 GiB RAM\n"
|
| 66 |
-
" (1x L4 = 30 GiB, A10G-large = 46 GiB) or set SKING_STAGE1_MIN_RAM_GB\n"
|
| 67 |
-
" lower to force an attempt.",
|
| 68 |
-
flush=True,
|
| 69 |
-
)
|
| 70 |
|
| 71 |
GPU_SECONDS = 180 # Flux @ 50 steps + BiRefNet + Dense UV Parser, with headroom
|
| 72 |
STAGE2_SECONDS = 120
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
# ---------------------------------------------------------------------------
|
| 76 |
# helpers
|
| 77 |
# ---------------------------------------------------------------------------
|
| 78 |
|
| 79 |
-
# Fixed paths are safe because we run with default_concurrency_limit=1.
|
| 80 |
_OUT_DIR = Path(tempfile.gettempdir()) / "sking-out"
|
| 81 |
_OUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 82 |
SKIN_OUT = _OUT_DIR / "skin.png"
|
|
@@ -84,7 +138,6 @@ RENDER_OUT = _OUT_DIR / "render.png"
|
|
| 84 |
|
| 85 |
|
| 86 |
def _upscale(img: Image.Image, factor: int = 8) -> Image.Image:
|
| 87 |
-
"""Nearest-neighbour upscale so a 64x64 skin is actually visible."""
|
| 88 |
w, h = img.size
|
| 89 |
return img.resize((w * factor, h * factor), Image.NEAREST)
|
| 90 |
|
|
@@ -99,12 +152,11 @@ def _empty_viewer() -> str:
|
|
| 99 |
|
| 100 |
|
| 101 |
def _viewer_iframe(skin_b64: str) -> str:
|
| 102 |
-
"""3D preview via skinview3d
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
unreachable -- the 2D outputs are unaffected.
|
| 108 |
"""
|
| 109 |
if not skin_b64:
|
| 110 |
return _empty_viewer()
|
|
@@ -158,17 +210,10 @@ def _validate_square(img: Image.Image) -> Image.Image:
|
|
| 158 |
@gpu(GPU_SECONDS)
|
| 159 |
def image_to_skin(image, seed, steps, guidance, size):
|
| 160 |
"""Full pipeline: reference image -> edited render -> 64x64 RGBA skin."""
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
if not STAGE1_OK:
|
| 164 |
-
raise gr.Error(
|
| 165 |
-
"Stage 1 (FLUX.2 [klein] 4B) is disabled on this hardware: the "
|
| 166 |
-
"container does not have enough RAM to hold the text encoder and "
|
| 167 |
-
"transformer together. Use the 'Render → Skin (debug)' tab, which "
|
| 168 |
-
"runs the identical production UV parser on a render you supply."
|
| 169 |
-
)
|
| 170 |
if image is None:
|
| 171 |
raise gr.Error("Upload a reference image first.")
|
|
|
|
| 172 |
|
| 173 |
render = stage1_flux.generate(
|
| 174 |
image, seed=int(seed), steps=int(steps),
|
|
@@ -177,11 +222,10 @@ def image_to_skin(image, seed, steps, guidance, size):
|
|
| 177 |
render.save(RENDER_OUT, format="PNG")
|
| 178 |
|
| 179 |
stage2_uv.render_to_uv(
|
| 180 |
-
RENDER_OUT, SKIN_OUT,
|
| 181 |
)
|
| 182 |
skin = Image.open(SKIN_OUT).convert("RGBA")
|
| 183 |
skin.load()
|
| 184 |
-
|
| 185 |
with open(SKIN_OUT, "rb") as fh:
|
| 186 |
skin_b64 = base64.b64encode(fh.read()).decode()
|
| 187 |
|
|
@@ -195,16 +239,20 @@ def image_to_skin(image, seed, steps, guidance, size):
|
|
| 195 |
|
| 196 |
@gpu(STAGE2_SECONDS)
|
| 197 |
def render_to_skin_only(render):
|
| 198 |
-
"""
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
| 201 |
if render is None:
|
| 202 |
raise gr.Error("Upload a combined front/back render (even square, 256-4096 px).")
|
|
|
|
| 203 |
|
| 204 |
src = _OUT_DIR / "debug_render.png"
|
| 205 |
_validate_square(render).save(src, format="PNG")
|
| 206 |
out = _OUT_DIR / "debug_skin.png"
|
| 207 |
-
stage2_uv.render_to_uv(src, out,
|
| 208 |
|
| 209 |
skin = Image.open(out).convert("RGBA")
|
| 210 |
skin.load()
|
|
@@ -234,8 +282,8 @@ Co., Ltd.) — [entropydrop.com](https://entropydrop.com) ·
|
|
| 234 |
- `EntropyDrop/Sking` and `SKING_DDJ_v104` — **AGPL-3.0**. This Space is
|
| 235 |
therefore AGPL-3.0 too; its complete source is this public repository.
|
| 236 |
- `black-forest-labs/FLUX.2-klein-base-4B` — **Apache-2.0**.
|
| 237 |
-
- BiRefNet and SigLIP2 keep their own licenses; see `NOTICE.md`
|
| 238 |
-
|
| 239 |
|
| 240 |
Not affiliated with or endorsed by EntropyDrop, Mojang or Microsoft.
|
| 241 |
Uploads are processed in memory and are not stored or used for training.
|
|
@@ -250,51 +298,25 @@ with gr.Blocks(
|
|
| 250 |
"""
|
| 251 |
# Sking · image → Minecraft skin
|
| 252 |
|
| 253 |
-
An open reimplementation of **EntropyDrop**'s two-stage pipeline
|
| 254 |
-
on free ZeroGPU.
|
| 255 |
|
| 256 |
-
**Stage 1** — FLUX.2 [klein] 4B Base + the `sking_v73` LoRA turns
|
| 257 |
reference image into a combined front/back character render.
|
| 258 |
**Stage 2** — `SKING_DDJ_v104` (BiRefNet foreground + Dense UV Parser)
|
| 259 |
-
converts that render into a real **64×64 RGBA** skin with inner and
|
| 260 |
-
layers, ready to drop into Minecraft.
|
| 261 |
-
|
| 262 |
-
>
|
| 263 |
-
>
|
| 264 |
-
>
|
| 265 |
-
>
|
| 266 |
-
> 🐢 First load after a rebuild re-downloads the weights and can take
|
| 267 |
-
> several minutes before the UI appears.
|
| 268 |
"""
|
| 269 |
)
|
| 270 |
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
> ### ⚠️ Stage 1 is disabled on this hardware
|
| 275 |
-
>
|
| 276 |
-
> {BOOT.get('stage1_reason','')}
|
| 277 |
-
>
|
| 278 |
-
> FLUX.2 [klein] 4B Base holds an **8.05 GB** Mistral text encoder and
|
| 279 |
-
> a **7.75 GB** transformer in host memory at the same time. The text
|
| 280 |
-
> encoder cannot be dropped or cached — FLUX.2 conditions on your
|
| 281 |
-
> reference *image* through it, so its output differs per request.
|
| 282 |
-
> Loading it here gets the container OOM-killed.
|
| 283 |
-
>
|
| 284 |
-
> **Still fully working:** the *Render → Skin* tab below runs
|
| 285 |
-
> `SKING_DDJ_v104`, which is byte-identical to EntropyDrop's
|
| 286 |
-
> production UV parser. Feed it any combined front/back render —
|
| 287 |
-
> including the real production examples provided — and it returns a
|
| 288 |
-
> genuine 64×64 RGBA skin.
|
| 289 |
-
>
|
| 290 |
-
> **To enable stage 1:** Settings → Space hardware → **1× L4**
|
| 291 |
-
> ($0.80/hr, 30 GiB RAM) or **A10G-large** ($1.50/hr, 46 GiB), then
|
| 292 |
-
> restart. Nothing else needs to change; the code detects the RAM
|
| 293 |
-
> and turns stage 1 on.
|
| 294 |
-
"""
|
| 295 |
-
)
|
| 296 |
|
| 297 |
-
with gr.Tab("Image → Skin"
|
| 298 |
with gr.Row():
|
| 299 |
with gr.Column(scale=5):
|
| 300 |
inp = gr.Image(type="pil", label="Reference image", height=340)
|
|
@@ -329,15 +351,18 @@ with gr.Blocks(
|
|
| 329 |
fn=image_to_skin,
|
| 330 |
inputs=[inp, seed, steps, guidance, size],
|
| 331 |
outputs=[out_render, out_preview, out_file, out_info, out_3d],
|
| 332 |
-
)
|
| 333 |
|
| 334 |
-
with gr.Tab("Render → Skin
|
| 335 |
gr.Markdown(
|
| 336 |
-
"
|
| 337 |
-
"
|
| 338 |
-
"
|
| 339 |
-
"
|
| 340 |
-
"
|
|
|
|
|
|
|
|
|
|
| 341 |
)
|
| 342 |
with gr.Row():
|
| 343 |
with gr.Column(scale=5):
|
|
@@ -354,18 +379,20 @@ with gr.Blocks(
|
|
| 354 |
examples=[[str(p)] for p in _fixtures],
|
| 355 |
inputs=[d_in],
|
| 356 |
label=(
|
| 357 |
-
"Real production renders
|
| 358 |
"(CC BY-NC 4.0 — see testdata/README.md)"
|
| 359 |
),
|
| 360 |
)
|
| 361 |
d_run.click(
|
| 362 |
fn=render_to_skin_only, inputs=[d_in], outputs=[d_preview, d_file, d_3d]
|
| 363 |
-
)
|
| 364 |
|
| 365 |
gr.Markdown(_CREDITS)
|
| 366 |
|
| 367 |
|
| 368 |
if __name__ == "__main__":
|
|
|
|
|
|
|
| 369 |
demo.queue(default_concurrency_limit=1).launch(
|
| 370 |
server_name="0.0.0.0", server_port=7860, show_api=False
|
| 371 |
)
|
|
|
|
| 1 |
"""Gradio front end for the Sking image -> Minecraft skin pipeline.
|
| 2 |
|
| 3 |
Startup order is load-bearing:
|
| 4 |
+
0. import spaces installs ZeroGPU's CUDA emulation shim -- MUST be
|
| 5 |
+
before anything touches torch, or `.to("cuda")`
|
| 6 |
+
dies with "No CUDA GPUs are available".
|
| 7 |
+
1. build + launch Gradio immediately, so the container binds port 7860 well
|
| 8 |
+
inside HF's health-check window.
|
| 9 |
+
2. bootstrap in a thread downloads ~19 GB and verifies SHA-256 in the
|
| 10 |
+
background. Blocking startup on this is what caused
|
| 11 |
+
RUNTIME_ERROR with no traceback: the platform killed
|
| 12 |
+
the container before it ever became healthy.
|
| 13 |
"""
|
| 14 |
|
| 15 |
# --- 0. `spaces` MUST be imported before torch ------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
try:
|
| 17 |
import spaces # type: ignore
|
| 18 |
|
|
|
|
| 28 |
|
| 29 |
ZERO_GPU = False
|
| 30 |
|
| 31 |
+
import base64 # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
import html # noqa: E402
|
| 33 |
import tempfile # noqa: E402
|
| 34 |
+
import threading # noqa: E402
|
| 35 |
+
import traceback # noqa: E402
|
| 36 |
from pathlib import Path # noqa: E402
|
| 37 |
|
| 38 |
import gradio as gr # noqa: E402
|
| 39 |
from PIL import Image # noqa: E402
|
| 40 |
|
| 41 |
+
from sking_pipeline import stage1_flux, stage2_uv, weights # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
GPU_SECONDS = 180 # Flux @ 50 steps + BiRefNet + Dense UV Parser, with headroom
|
| 44 |
STAGE2_SECONDS = 120
|
| 45 |
|
| 46 |
+
# ---------------------------------------------------------------------------
|
| 47 |
+
# background bootstrap
|
| 48 |
+
# ---------------------------------------------------------------------------
|
| 49 |
+
|
| 50 |
+
STATE = {
|
| 51 |
+
"stage": "starting",
|
| 52 |
+
"log": [],
|
| 53 |
+
"ready": False,
|
| 54 |
+
"stage1": False,
|
| 55 |
+
"boot": None,
|
| 56 |
+
"error": None,
|
| 57 |
+
}
|
| 58 |
+
_LOCK = threading.Lock()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _progress(msg: str) -> None:
|
| 62 |
+
with _LOCK:
|
| 63 |
+
STATE["log"].append(msg)
|
| 64 |
+
if len(STATE["log"]) > 200:
|
| 65 |
+
del STATE["log"][:100]
|
| 66 |
+
print(msg, flush=True)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _bootstrap() -> None:
|
| 70 |
+
try:
|
| 71 |
+
boot = weights.bootstrap(progress=_progress)
|
| 72 |
+
with _LOCK:
|
| 73 |
+
STATE["boot"] = boot
|
| 74 |
+
if boot.get("stage1_viable") and boot.get("flux"):
|
| 75 |
+
_progress("loading FLUX.2 [klein] 4B Base (background thread)")
|
| 76 |
+
stage1_flux.load(boot["flux"], boot["lora"], progress=_progress)
|
| 77 |
+
with _LOCK:
|
| 78 |
+
STATE["stage1"] = True
|
| 79 |
+
else:
|
| 80 |
+
_progress(f"stage 1 skipped: {boot.get('stage1_reason')}")
|
| 81 |
+
with _LOCK:
|
| 82 |
+
STATE["stage"] = "ready"
|
| 83 |
+
STATE["ready"] = True
|
| 84 |
+
_progress("READY")
|
| 85 |
+
except BaseException: # noqa: BLE001 - surface anything, incl. MemoryError
|
| 86 |
+
with _LOCK:
|
| 87 |
+
STATE["error"] = traceback.format_exc()
|
| 88 |
+
STATE["stage"] = "error"
|
| 89 |
+
print(STATE["error"], flush=True)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
threading.Thread(target=_bootstrap, name="sking-bootstrap", daemon=True).start()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _bundle():
|
| 96 |
+
boot = STATE.get("boot")
|
| 97 |
+
if not boot:
|
| 98 |
+
raise gr.Error("Models are still downloading — see the status panel.")
|
| 99 |
+
return boot
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _require_stage1():
|
| 103 |
+
if not STATE["ready"]:
|
| 104 |
+
raise gr.Error("Models are still downloading — see the status panel.")
|
| 105 |
+
if not STATE["stage1"]:
|
| 106 |
+
raise gr.Error(
|
| 107 |
+
"Stage 1 (FLUX.2 [klein] 4B) is not available on this hardware. "
|
| 108 |
+
"Use the 'Render → Skin' tab, which runs the identical production "
|
| 109 |
+
"UV parser on a render you supply."
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def status_md() -> str:
|
| 114 |
+
with _LOCK:
|
| 115 |
+
stage, err, log = STATE["stage"], STATE["error"], list(STATE["log"])
|
| 116 |
+
s1, ready = STATE["stage1"], STATE["ready"]
|
| 117 |
+
icon = {"starting": "⏳", "ready": "✅", "error": "❌"}.get(stage, "•")
|
| 118 |
+
tail = "\n".join(f" {line}" for line in log[-14:])
|
| 119 |
+
head = (
|
| 120 |
+
f"### {icon} Status: `{stage}`\n\n"
|
| 121 |
+
f"- stage 1 (Flux + LoRA): **{'available' if s1 else 'not loaded'}**\n"
|
| 122 |
+
f"- stage 2 (SKING_DDJ_v104): **{'available' if ready else 'loading'}**\n"
|
| 123 |
+
f"- ZeroGPU: **{'yes' if ZERO_GPU else 'no (dedicated/local)'}**\n"
|
| 124 |
+
)
|
| 125 |
+
if err:
|
| 126 |
+
head += f"\n**Error**\n\n```\n{err.strip()[-2000:]}\n```\n"
|
| 127 |
+
return head + f"\n<details><summary>startup log</summary>\n\n```\n{tail}\n```\n</details>"
|
| 128 |
+
|
| 129 |
|
| 130 |
# ---------------------------------------------------------------------------
|
| 131 |
# helpers
|
| 132 |
# ---------------------------------------------------------------------------
|
| 133 |
|
|
|
|
| 134 |
_OUT_DIR = Path(tempfile.gettempdir()) / "sking-out"
|
| 135 |
_OUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 136 |
SKIN_OUT = _OUT_DIR / "skin.png"
|
|
|
|
| 138 |
|
| 139 |
|
| 140 |
def _upscale(img: Image.Image, factor: int = 8) -> Image.Image:
|
|
|
|
| 141 |
w, h = img.size
|
| 142 |
return img.resize((w * factor, h * factor), Image.NEAREST)
|
| 143 |
|
|
|
|
| 152 |
|
| 153 |
|
| 154 |
def _viewer_iframe(skin_b64: str) -> str:
|
| 155 |
+
"""3D preview via skinview3d inside an iframe srcdoc.
|
| 156 |
|
| 157 |
+
Gradio updates gr.HTML through innerHTML, and browsers do not execute
|
| 158 |
+
<script> inserted that way; a srcdoc document is parsed normally, so its
|
| 159 |
+
scripts do run. Degrades to a blank box if the CDN is unreachable.
|
|
|
|
| 160 |
"""
|
| 161 |
if not skin_b64:
|
| 162 |
return _empty_viewer()
|
|
|
|
| 210 |
@gpu(GPU_SECONDS)
|
| 211 |
def image_to_skin(image, seed, steps, guidance, size):
|
| 212 |
"""Full pipeline: reference image -> edited render -> 64x64 RGBA skin."""
|
| 213 |
+
_require_stage1()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
if image is None:
|
| 215 |
raise gr.Error("Upload a reference image first.")
|
| 216 |
+
boot = _bundle()
|
| 217 |
|
| 218 |
render = stage1_flux.generate(
|
| 219 |
image, seed=int(seed), steps=int(steps),
|
|
|
|
| 222 |
render.save(RENDER_OUT, format="PNG")
|
| 223 |
|
| 224 |
stage2_uv.render_to_uv(
|
| 225 |
+
RENDER_OUT, SKIN_OUT, boot["bundle"], release=boot.get("release")
|
| 226 |
)
|
| 227 |
skin = Image.open(SKIN_OUT).convert("RGBA")
|
| 228 |
skin.load()
|
|
|
|
| 229 |
with open(SKIN_OUT, "rb") as fh:
|
| 230 |
skin_b64 = base64.b64encode(fh.read()).decode()
|
| 231 |
|
|
|
|
| 239 |
|
| 240 |
@gpu(STAGE2_SECONDS)
|
| 241 |
def render_to_skin_only(render):
|
| 242 |
+
"""Feed a combined front/back render straight into v104."""
|
| 243 |
+
if not STATE["ready"]:
|
| 244 |
+
raise gr.Error(
|
| 245 |
+
"Stage 2 is still loading its 2.58 GB bundle — check the status "
|
| 246 |
+
"panel and refresh. This usually takes 1-3 minutes."
|
| 247 |
+
)
|
| 248 |
if render is None:
|
| 249 |
raise gr.Error("Upload a combined front/back render (even square, 256-4096 px).")
|
| 250 |
+
boot = _bundle()
|
| 251 |
|
| 252 |
src = _OUT_DIR / "debug_render.png"
|
| 253 |
_validate_square(render).save(src, format="PNG")
|
| 254 |
out = _OUT_DIR / "debug_skin.png"
|
| 255 |
+
stage2_uv.render_to_uv(src, out, boot["bundle"], release=boot.get("release"))
|
| 256 |
|
| 257 |
skin = Image.open(out).convert("RGBA")
|
| 258 |
skin.load()
|
|
|
|
| 282 |
- `EntropyDrop/Sking` and `SKING_DDJ_v104` — **AGPL-3.0**. This Space is
|
| 283 |
therefore AGPL-3.0 too; its complete source is this public repository.
|
| 284 |
- `black-forest-labs/FLUX.2-klein-base-4B` — **Apache-2.0**.
|
| 285 |
+
- BiRefNet and SigLIP2 keep their own licenses; see `NOTICE.md`.
|
| 286 |
+
- The images in `testdata/` are real entropydrop.com generations, **CC BY-NC 4.0**.
|
| 287 |
|
| 288 |
Not affiliated with or endorsed by EntropyDrop, Mojang or Microsoft.
|
| 289 |
Uploads are processed in memory and are not stored or used for training.
|
|
|
|
| 298 |
"""
|
| 299 |
# Sking · image → Minecraft skin
|
| 300 |
|
| 301 |
+
An open reimplementation of **EntropyDrop**'s two-stage pipeline.
|
|
|
|
| 302 |
|
| 303 |
+
**Stage 1** — FLUX.2 [klein] 4B Base + the `sking_v73` LoRA turns a
|
| 304 |
reference image into a combined front/back character render.
|
| 305 |
**Stage 2** — `SKING_DDJ_v104` (BiRefNet foreground + Dense UV Parser)
|
| 306 |
+
converts that render into a real **64×64 RGBA** skin with inner and
|
| 307 |
+
outer layers, ready to drop into Minecraft.
|
| 308 |
+
|
| 309 |
+
> ⏳ **Models download in the background on first start** (~19 GB, 1–4
|
| 310 |
+
> min). Watch the status panel below and hit *Refresh* — the UI is live
|
| 311 |
+
> before the weights are.
|
|
|
|
|
|
|
|
|
|
| 312 |
"""
|
| 313 |
)
|
| 314 |
|
| 315 |
+
status = gr.Markdown(status_md())
|
| 316 |
+
refresh = gr.Button("Refresh status", size="sm")
|
| 317 |
+
refresh.click(fn=status_md, inputs=None, outputs=status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
+
with gr.Tab("Image → Skin"):
|
| 320 |
with gr.Row():
|
| 321 |
with gr.Column(scale=5):
|
| 322 |
inp = gr.Image(type="pil", label="Reference image", height=340)
|
|
|
|
| 351 |
fn=image_to_skin,
|
| 352 |
inputs=[inp, seed, steps, guidance, size],
|
| 353 |
outputs=[out_render, out_preview, out_file, out_info, out_3d],
|
| 354 |
+
).then(fn=status_md, inputs=None, outputs=status)
|
| 355 |
|
| 356 |
+
with gr.Tab("Render → Skin"):
|
| 357 |
gr.Markdown(
|
| 358 |
+
"Runs **stage 2 only** — EntropyDrop's `SKING_DDJ_v104` UV parser, "
|
| 359 |
+
"byte-identical to their production release. Input must be an **even "
|
| 360 |
+
"square between 256 and 4096 px**, left half = front view, right "
|
| 361 |
+
"half = back view (their `front_left` / `back_left` layout). "
|
| 362 |
+
"Uploads are centre-cropped to a square.\n\n"
|
| 363 |
+
"This tab works even when stage 1 is unavailable, and the examples "
|
| 364 |
+
"below are **real renders captured from entropydrop.com**, so you "
|
| 365 |
+
"can compare directly against their published output."
|
| 366 |
)
|
| 367 |
with gr.Row():
|
| 368 |
with gr.Column(scale=5):
|
|
|
|
| 379 |
examples=[[str(p)] for p in _fixtures],
|
| 380 |
inputs=[d_in],
|
| 381 |
label=(
|
| 382 |
+
"Real production renders from entropydrop.com "
|
| 383 |
"(CC BY-NC 4.0 — see testdata/README.md)"
|
| 384 |
),
|
| 385 |
)
|
| 386 |
d_run.click(
|
| 387 |
fn=render_to_skin_only, inputs=[d_in], outputs=[d_preview, d_file, d_3d]
|
| 388 |
+
).then(fn=status_md, inputs=None, outputs=status)
|
| 389 |
|
| 390 |
gr.Markdown(_CREDITS)
|
| 391 |
|
| 392 |
|
| 393 |
if __name__ == "__main__":
|
| 394 |
+
# Launch immediately: binding 7860 fast is what keeps HF's health check
|
| 395 |
+
# happy while ~19 GB downloads in the background thread.
|
| 396 |
demo.queue(default_concurrency_limit=1).launch(
|
| 397 |
server_name="0.0.0.0", server_port=7860, show_api=False
|
| 398 |
)
|
sking_pipeline/weights.py
CHANGED
|
@@ -218,6 +218,17 @@ def system_memory_gb() -> float:
|
|
| 218 |
return min(candidates) / (1024 ** 3)
|
| 219 |
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
def memory_report() -> str:
|
| 222 |
"""Diagnostic line for the startup log."""
|
| 223 |
try:
|
|
@@ -267,6 +278,7 @@ def bootstrap(progress=print) -> dict:
|
|
| 267 |
|
| 268 |
viable, ram, reason = stage1_viable(progress)
|
| 269 |
progress(f"memory: {memory_report()}")
|
|
|
|
| 270 |
progress(f"stage 1 {'ENABLED' if viable else 'DISABLED'} ({reason})")
|
| 271 |
|
| 272 |
bundle = download_bundle(progress)
|
|
@@ -275,10 +287,12 @@ def bootstrap(progress=print) -> dict:
|
|
| 275 |
f" verified {len(release['sha256'])} files "
|
| 276 |
f"({release['model_version']})"
|
| 277 |
)
|
|
|
|
| 278 |
link_offline_cache(bundle, progress)
|
| 279 |
|
| 280 |
if viable:
|
| 281 |
flux_dir, lora = download_flux(progress)
|
|
|
|
| 282 |
else:
|
| 283 |
# Do not waste 16 GB of ephemeral disk and cold-start time on weights
|
| 284 |
# this container cannot hold in memory. Stage 2 alone is the
|
|
|
|
| 218 |
return min(candidates) / (1024 ** 3)
|
| 219 |
|
| 220 |
|
| 221 |
+
def disk_report(path: str = "/tmp") -> str:
|
| 222 |
+
"""Free/total disk on the volume holding the weights."""
|
| 223 |
+
import shutil
|
| 224 |
+
|
| 225 |
+
try:
|
| 226 |
+
u = shutil.disk_usage(path)
|
| 227 |
+
return f"disk({path}) free={u.free/1024**3:.1f} GiB total={u.total/1024**3:.1f} GiB"
|
| 228 |
+
except OSError as exc:
|
| 229 |
+
return f"disk({path}) unreadable: {exc}"
|
| 230 |
+
|
| 231 |
+
|
| 232 |
def memory_report() -> str:
|
| 233 |
"""Diagnostic line for the startup log."""
|
| 234 |
try:
|
|
|
|
| 278 |
|
| 279 |
viable, ram, reason = stage1_viable(progress)
|
| 280 |
progress(f"memory: {memory_report()}")
|
| 281 |
+
progress(disk_report(str(WEIGHTS_ROOT.parent)))
|
| 282 |
progress(f"stage 1 {'ENABLED' if viable else 'DISABLED'} ({reason})")
|
| 283 |
|
| 284 |
bundle = download_bundle(progress)
|
|
|
|
| 287 |
f" verified {len(release['sha256'])} files "
|
| 288 |
f"({release['model_version']})"
|
| 289 |
)
|
| 290 |
+
progress(" " + disk_report(str(WEIGHTS_ROOT.parent)))
|
| 291 |
link_offline_cache(bundle, progress)
|
| 292 |
|
| 293 |
if viable:
|
| 294 |
flux_dir, lora = download_flux(progress)
|
| 295 |
+
progress(" " + disk_report(str(WEIGHTS_ROOT.parent)))
|
| 296 |
else:
|
| 297 |
# Do not waste 16 GB of ephemeral disk and cold-start time on weights
|
| 298 |
# this container cannot hold in memory. Stage 2 alone is the
|