JSCPPProgrammer's picture
chore: sync local Space files
f3a60a0 verified
"""
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:
@staticmethod
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)))
@spaces.GPU(duration=_compute_gpu_duration) # 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
@functools.wraps(_base_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,
)