Spaces:
Running
Running
multimodalart HF Staff
Un-mix against the raw matte (kills the last halo), real progress bar, native-res sprites, filter retry
af3622c verified Download backends.py from multimodalart/sprite-forge: direct link, hf CLI and curl.
- Browser
- Download file 7.81 kB
-
https://huggingface.co/spaces/multimodalart/sprite-forge/resolve/main/backends.py
- Command line
-
hf download hf://spaces/multimodalart/sprite-forge/backends.py
-
curl -L -o backends.py https://huggingface.co/spaces/multimodalart/sprite-forge/resolve/main/backends.py
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²)" | |
| 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 | |