Spaces:
Running on Zero
Running on Zero
| """ | |
| Anima RDBT — Gradio front-end for ZeroGPU Hugging Face Spaces, with an embedded | |
| ComfyUI backend for native RDBT KSampler execution. | |
| """ | |
| from __future__ import annotations | |
| import functools | |
| import os | |
| import traceback | |
| from typing import Any | |
| import gradio as gr | |
| # ZeroGPU: https://huggingface.co/docs/hub/main/spaces-zerogpu | |
| try: | |
| import spaces | |
| except ImportError: # local / tests without the runtime wheel | |
| class _Spaces: | |
| def GPU(*args: Any, **kwargs: Any): | |
| if args and callable(args[0]) and not kwargs: | |
| return args[0] | |
| def _deco(fn: Any) -> Any: | |
| return fn | |
| return _deco | |
| spaces = _Spaces() # type: ignore[misc, assignment] | |
| from src.comfy_backend import run_generation | |
| from src.errors import UserFacingError | |
| from src.validation import validate_and_clamp | |
| from src import config | |
| LICENSE_BANNER = ( | |
| "**License:** Anima is distributed under the **CircleStone Labs Non-Commercial License** " | |
| "and is a derivative of **Cosmos-Predict2-2B-Text2Image** (NVIDIA terms where applicable). " | |
| "Use **non-commercially** unless you have a separate license. See the " | |
| "[Anima model card](https://huggingface.co/circlestone-labs/Anima)." | |
| ) | |
| FALLBACK_NOTE = ( | |
| "Generation uses ComfyUI's native nodes and KSampler under the Gradio UI. " | |
| "Sampler, scheduler, cfg, denoise, seed, size, and batch are passed through after validation." | |
| ) | |
| def _compute_gpu_duration( | |
| _prompt: str, | |
| _neg: str, | |
| _w: int, | |
| _h: int, | |
| steps: float, | |
| _cfg: float, | |
| batch: float, | |
| _s: str, | |
| _sc: str, | |
| _d: float, | |
| _seed: float, | |
| _randomize_seed: bool, | |
| _progress: Any = None, | |
| ) -> int: | |
| """Must accept the same positional args as _generate_task (including Gradio-injected progress).""" | |
| try: | |
| s = int(steps) | |
| except (TypeError, ValueError): | |
| s = 16 | |
| try: | |
| b = int(batch) | |
| except (TypeError, ValueError): | |
| b = 1 | |
| s = max(1, min(50, s)) | |
| b = max(1, min(config.MAX_BATCH, b)) | |
| # Queue-friendly: high-res + batch → more time; cap 300s (ZeroGPU allowlist) | |
| est = 25 + s * 4 * b | |
| w = int(os.environ.get("ANIMA_MAX_GPU_DURATION", "300")) | |
| return min(int(w), max(30, int(est))) | |
| # type: ignore[misc] | |
| def _generate_task( | |
| prompt: str, | |
| negative_prompt: str, | |
| width: float, | |
| height: float, | |
| steps: float, | |
| cfg: float, | |
| batch_size: float, | |
| sampler_name: str, | |
| scheduler: str, | |
| denoise: float, | |
| seed: float, | |
| randomize_seed: bool, | |
| progress: gr.Progress = gr.Progress(), | |
| ) -> tuple[Any, str, str, str]: | |
| """Returns (gallery or None, status markdown, actual positive str, actual negative str).""" | |
| try: | |
| params = validate_and_clamp( | |
| prompt=prompt, | |
| negative_prompt=negative_prompt, | |
| width=width, | |
| height=height, | |
| steps=steps, | |
| cfg=cfg, | |
| batch_size=batch_size, | |
| sampler_name=sampler_name, | |
| scheduler=scheduler, | |
| denoise=denoise, | |
| seed=seed, | |
| randomize_seed=randomize_seed, | |
| ) | |
| except UserFacingError as e: | |
| lines = [f"**Error:** {e.user_message}"] | |
| if e.details: | |
| lines.append(f"`{e.details}`") | |
| return ( | |
| None, | |
| "\n\n".join(lines), | |
| "— (nothing sent — validation failed before the pipeline)", | |
| "— (nothing sent — validation failed before the pipeline)", | |
| ) | |
| try: | |
| images, det, pos_sent, neg_sent = run_generation(params, progress=progress) | |
| except UserFacingError as e: | |
| return ( | |
| None, | |
| f"**Error:** {e.user_message}", | |
| params.prompt, | |
| params.negative_prompt, | |
| ) | |
| except Exception as e: # noqa: BLE001 | |
| tb = traceback.format_exc() | |
| return ( | |
| None, | |
| f"**Error:** Generation failed: `{e!s}`\n\n```\n{tb[-4000:]}\n```", | |
| params.prompt, | |
| params.negative_prompt, | |
| ) | |
| status_parts = [ | |
| "**Done.** " + det, | |
| ] | |
| if params.warnings: | |
| status_parts.append("**Notices:**\n" + "\n".join(f"- {w}" for w in params.warnings)) | |
| status_text = "\n\n".join(status_parts) | |
| if images: | |
| return images, status_text, pos_sent, neg_sent | |
| return None, status_text, pos_sent, neg_sent | |
| def build_ui() -> gr.Blocks: | |
| # Gradio 6.0: theme and related UI options belong on launch(), not Blocks(). | |
| with gr.Blocks( | |
| title="Anima RDBT (Gradio)", | |
| ) as demo: | |
| gr.Markdown( | |
| f"# Anima RDBT — {config.RDBT_UNET_NAME}\n\n{LICENSE_BANNER}\n\n{FALLBACK_NOTE}" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| prompt = gr.Textbox( | |
| label="prompt", | |
| lines=3, | |
| value="digital anime illustration, 1girl, smile", | |
| ) | |
| neg = gr.Textbox( | |
| label="negative_prompt", | |
| lines=2, | |
| value="", | |
| ) | |
| with gr.Row(): | |
| w = gr.Slider( | |
| config.MIN_WH, | |
| config.MAX_WH, | |
| value=config.DEFAULT_WIDTH, | |
| step=64, | |
| label="width", | |
| ) | |
| h = gr.Slider( | |
| config.MIN_WH, | |
| config.MAX_WH, | |
| value=config.DEFAULT_HEIGHT, | |
| step=64, | |
| label="height", | |
| ) | |
| st = gr.Slider( | |
| config.MIN_STEPS, | |
| config.MAX_STEPS, | |
| value=config.DEFAULT_STEPS, | |
| step=1, | |
| label="steps", | |
| ) | |
| cfg = gr.Slider( | |
| config.MIN_CFG, | |
| config.MAX_CFG, | |
| value=config.DEFAULT_CFG, | |
| step=config.CFG_STEP, | |
| label="cfg", | |
| ) | |
| with gr.Column(scale=1): | |
| with gr.Accordion("Advanced", open=True): | |
| batch = gr.Slider( | |
| config.MIN_BATCH, | |
| config.MAX_BATCH, | |
| value=config.DEFAULT_BATCH_SIZE, | |
| step=1, | |
| label="batch_size", | |
| ) | |
| with gr.Row(): | |
| seed = gr.Number( | |
| value=config.DEFAULT_SEED, | |
| label="seed", | |
| precision=0, | |
| minimum=config.MIN_SEED, | |
| maximum=config.MAX_SEED, | |
| step=1, | |
| info="Used when randomize_seed is off.", | |
| ) | |
| randomize_seed = gr.Checkbox( | |
| value=config.DEFAULT_RANDOMIZE_SEED, | |
| label="randomize_seed", | |
| info="Generate a fresh seed for each run.", | |
| ) | |
| sampler = gr.Dropdown( | |
| choices=list(config.SAMPLER_CHOICES), | |
| value=config.DEFAULT_SAMPLER, | |
| label="sampler_name", | |
| info="RDBT default: euler_ancestral. Unknown values fall back with a notice.", | |
| ) | |
| sched = gr.Dropdown( | |
| choices=list(config.SCHEDULER_CHOICES), | |
| value=config.DEFAULT_SCHEDULER, | |
| label="scheduler", | |
| info="RDBT default: simple.", | |
| ) | |
| denoise = gr.Slider( | |
| config.MIN_DENOISE, | |
| config.MAX_DENOISE, | |
| value=config.DEFAULT_DENOISE, | |
| step=config.DENOISE_STEP, | |
| label="denoise", | |
| ) | |
| with gr.Accordion( | |
| "Actual strings sent to ComfyUI (post-validation)", | |
| open=True, | |
| ): | |
| gr.Markdown( | |
| "These are the exact `prompt` and `negative_prompt` strings passed to " | |
| "ComfyUI's `CLIPTextEncode` nodes for the last run (not raw textbox state if validation changed things)." | |
| ) | |
| actual_prompt_out = gr.Textbox( | |
| label="prompt → ComfyUI", | |
| lines=4, | |
| max_lines=30, | |
| interactive=False, | |
| value="", | |
| ) | |
| actual_neg_out = gr.Textbox( | |
| label="negative_prompt → ComfyUI", | |
| lines=3, | |
| max_lines=20, | |
| interactive=False, | |
| value="", | |
| ) | |
| gallery = gr.Gallery( | |
| label="output", | |
| columns=2, | |
| object_fit="contain", | |
| height=600, | |
| ) | |
| status = gr.Markdown( | |
| "**While the Space starts:** the RDBT checkpoint downloads (and optional hub assets from config; no GPU needed). " | |
| "**On first Generate:** ZeroGPU assigns a GPU, then ComfyUI loads UNET/CLIP/VAE and runs the native graph — " | |
| "that first click can take extra time to build and load the model objects." | |
| ) | |
| go = gr.Button("Generate", variant="primary") | |
| go.click( | |
| fn=_generate_task, | |
| inputs=[ | |
| prompt, | |
| neg, | |
| w, | |
| h, | |
| st, | |
| cfg, | |
| batch, | |
| sampler, | |
| sched, | |
| denoise, | |
| seed, | |
| randomize_seed, | |
| ], | |
| outputs=[gallery, status, actual_prompt_out, actual_neg_out], | |
| show_progress="full", | |
| ) | |
| return demo | |
| # Hugging Face imports this module as `import app` (not __main__), so the block | |
| # below must NOT be the only place that calls `queue()`. Otherwise the Space | |
| # serves `demo` without a queue and shutdown can get noisy async teardown. | |
| demo = build_ui() | |
| demo.queue() | |
| # Prepare ComfyUI source + weights at container import time so the first HTTP | |
| # request does not pay download cost; the Space "Starting..." state covers this. | |
| if not config.skip_startup_bootstrap(): | |
| from src.comfy_backend import run_at_container_startup | |
| run_at_container_startup() | |
| # Gradio 6: `theme` is a launch() argument. ZeroGPU calls `demo.launch()` without | |
| # running our `if __name__` block, so wrap to always default the Soft theme. | |
| _base_launch = demo.launch | |
| # type: ignore[misc] | |
| def _launch_with_default_theme(*args: Any, **kwargs: Any): | |
| kwargs.setdefault("theme", gr.themes.Soft()) | |
| return _base_launch(*args, **kwargs) | |
| demo.launch = _launch_with_default_theme # type: ignore[method-assign, assignment] | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", "7860")) | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=port, | |
| share=False, | |
| ) | |