sprite-forge / backends.py
multimodalart's picture
multimodalart HF Staff
Un-mix against the raw matte (kills the last halo), real progress bar, native-res sprites, filter retry
af3622c verified
Raw History Blame Contribute Delete
7.81 kB
"""The two remote Spaces this one orchestrates.
This Space runs on `cpu-basic` and owns no model. Both generation steps are `gradio_client` calls into Spaces that
already host the weights — a text-to-image Space for the character design still, and
`MiniMaxAI/MiniMax-H3-Turbo-Lora` for the clips. Everything this Space does itself (keying, extreme detection,
packing) is numpy on the CPU.
**Whose GPU quota pays.** The visitor's, and that is the point of forwarding `x-ip-token`. ZeroGPU bills a booking to
whoever the token identifies; a call made without one is attributed to this Space's pod IP and its small shared
quota, which would put every visitor in the world in one queue. `gradio_client` picks the token up off gradio's
`LocalContext` when it is constructed inside an event listener, but that is a thread-local and the generation here
runs in a worker thread, so the header is read off the incoming `gr.Request` and passed explicitly instead.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from gradio_client import Client, handle_file
H3_SPACE = os.environ.get("H3_SPACE", "MiniMaxAI/MiniMax-H3-Turbo-Lora")
HF_TOKEN = os.environ.get("HF_TOKEN") or None
# H3 canvases, keyed by the label its API expects. Square is the default because a kick or a punch extends the
# silhouette sideways, and a portrait canvas that fits the idle pose clips the extremes off the moves that matter.
CANVASES: dict[str, tuple[str, int, int]] = {
"Square · fast (544²)": ("544x544 · 1:1 fast", 1024, 1024),
"Square · detailed (768²)": ("768x768 · 1:1 full", 1024, 1024),
"Portrait · fast (576x768)": ("576x768 · 3:4 fast", 768, 1024),
}
DEFAULT_CANVAS = "Square · fast (544²)"
@dataclass(frozen=True)
class ImageBackend:
label: str
space: str
api_name: str
note: str
def generate(self, prompt: str, width: int, height: int, seed: int, ip_token: str | None) -> str:
client = _client(self.space, ip_token)
if self.space == "mrfakename/Z-Image-Turbo":
result = client.predict(
prompt=prompt,
height=height,
width=width,
num_inference_steps=9,
seed=seed,
randomize_seed=False,
api_name=self.api_name,
)
elif self.space == "black-forest-labs/flux-klein-9b-kv":
result = client.predict(
prompt=prompt,
input_images=[],
seed=seed,
randomize_seed=False,
width=width,
height=height,
num_inference_steps=4,
prompt_upsampling=False,
api_name=self.api_name,
)
else: # FLUX.2 Klein 4B — same shape plus the distilled/base mode radio.
result = client.predict(
prompt=prompt,
input_images=[],
mode_choice="Distilled (4 steps)",
seed=seed,
randomize_seed=False,
width=width,
height=height,
num_inference_steps=4,
guidance_scale=1,
prompt_upsampling=False,
api_name=self.api_name,
)
return _as_path(result)
# Ordered best-first; these are the three that survived a bake-off on "flat 2D character on a magenta backdrop".
# The two rejects are worth recording: FLUX.1-schnell returns a rose backdrop that keys fine but is barely
# separable from warm fur, and FLUX.2-dev reliably drifts into recognizable trademarked cartoon characters.
IMAGE_BACKENDS: list[ImageBackend] = [
ImageBackend(
"FLUX.2 Klein 9B-KV — best flat art",
"black-forest-labs/flux-klein-9b-kv",
"/generate",
"Cleanest flat color separation and the most saturated backdrop of the three.",
),
ImageBackend(
"FLUX.2 Klein 4B — fast",
"black-forest-labs/FLUX.2-klein-4B",
"/infer",
"Nearly as clean, noticeably quicker.",
),
ImageBackend(
"Z-Image Turbo — fastest",
"mrfakename/Z-Image-Turbo",
"/generate_image",
"Very flat cel shading, least control over framing.",
),
]
BACKEND_LABELS = [backend.label for backend in IMAGE_BACKENDS]
BY_LABEL = {backend.label: backend for backend in IMAGE_BACKENDS}
def _client(space: str, ip_token: str | None) -> Client:
headers = {"x-ip-token": ip_token} if ip_token else None
return Client(space, token=HF_TOKEN, headers=headers, httpx_kwargs={"timeout": 1800})
def _as_path(result) -> str:
"""`predict` returns a tuple whose first element is the file — sometimes a path, sometimes a FileData dict."""
value = result[0] if isinstance(result, (list, tuple)) else result
if isinstance(value, (list, tuple)):
value = value[0]
if isinstance(value, dict):
value = value.get("path") or value.get("video") or value.get("url")
if not value:
raise RuntimeError("The remote Space returned no file.")
return value
def animate_job(
prompt: str,
image_path: str,
canvas_label: str,
duration: float = 3.0,
steps: int = 6,
seed: int = 42,
ip_token: str | None = None,
):
"""Same request as `animate`, submitted rather than awaited, so several can be in flight and polled.
Returns a `gradio_client` Job. `.done()` and `.result()` are the whole interface the caller needs.
"""
return _client(H3_SPACE, ip_token).submit(
prompt=prompt,
image_path=handle_file(image_path),
last_image_path=handle_file(image_path),
canvas=CANVASES[canvas_label][0],
duration=duration,
steps=steps,
seed=seed,
upsample=False,
use_lora=True,
lora="larry",
api_name="/generate",
)
def unwrap(result) -> tuple[str, str]:
"""`(video path, the report line the H3 Space emits)` out of a raw prediction tuple."""
report = result[1] if isinstance(result, (list, tuple)) and len(result) > 1 else ""
return _as_path(result), str(report)
def animate(
prompt: str,
image_path: str,
canvas_label: str,
duration: float = 3.0,
steps: int = 6,
seed: int = 42,
ip_token: str | None = None,
) -> tuple[str, str]:
"""One move clip. Returns `(video path, the report line the H3 Space emits)`.
`image_path` is passed as **both** the first and the last frame. That is the whole looping trick: H3's `fl2va`
branch interpolates between two conditioning keyframes, so making them the same image forces the motion to
depart from the rest pose and arrive back at it, and the clip can be played end-to-end forever without a seam.
`steps=6` is the turbo LoRA's comfortable setting — `larryvrh/MiniMax-H3-Turbo-Lora` is designed at 4 and its
card recommends 6-8 for anything that has to hold up frame by frame, which a sprite sheet very much does.
"""
client = _client(H3_SPACE, ip_token)
result = client.predict(
prompt=prompt,
image_path=handle_file(image_path),
last_image_path=handle_file(image_path),
canvas=CANVASES[canvas_label][0],
duration=duration,
steps=steps,
seed=seed,
upsample=False,
use_lora=True,
lora="larry",
api_name="/generate",
)
report = result[1] if isinstance(result, (list, tuple)) and len(result) > 1 else ""
return _as_path(result), str(report)
def ip_token_of(request) -> str | None:
"""The visitor's ZeroGPU token, so their own quota pays for the remote bookings."""
if request is None:
return None
try:
return request.headers.get("x-ip-token")
except Exception:
return None