sv3d-space / app.py
gubernac's picture
Initial SV3D Space scaffold
b9f8e91 verified
Raw
History Blame Contribute Delete
6.08 kB
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
import gradio as gr
import torch
from huggingface_hub import hf_hub_download
from PIL import Image
SPACE_ROOT = Path(__file__).resolve().parent
GEN_MODELS_ROOT = Path("/opt/generative-models")
CHECKPOINTS_DIR = GEN_MODELS_ROOT / "checkpoints"
WORK_DIR = Path("/tmp/sv3d-space")
MODEL_REPO_ID = "stabilityai/sv3d"
MODEL_FILES = {
"sv3d_u": "sv3d_u.safetensors",
"sv3d_p": "sv3d_p.safetensors",
}
def ensure_checkpoint(version: str) -> Path:
token = os.getenv("HF_TOKEN")
if not token:
raise gr.Error(
"Missing HF_TOKEN secret. Add a Hugging Face user access token to the Space "
"settings, and make sure that token belongs to an account that already accepted "
"the stabilityai/sv3d license."
)
CHECKPOINTS_DIR.mkdir(parents=True, exist_ok=True)
filename = MODEL_FILES[version]
target_path = CHECKPOINTS_DIR / filename
if target_path.exists():
return target_path
hf_hub_download(
repo_id=MODEL_REPO_ID,
filename=filename,
local_dir=CHECKPOINTS_DIR,
token=token,
)
return target_path
def newest_output_file(output_dir: Path) -> Path:
candidates = sorted(
output_dir.rglob("*.mp4"),
key=lambda path: path.stat().st_mtime,
reverse=True,
)
if candidates:
return candidates[0]
fallback = sorted(
output_dir.rglob("*"),
key=lambda path: path.stat().st_mtime,
reverse=True,
)
for path in fallback:
if path.is_file():
return path
raise gr.Error("The sampler finished without producing an output file.")
def run_sv3d(
image: Image.Image,
version: str,
elevation_deg: float,
num_steps: int,
seed: int,
remove_bg: bool,
progress=gr.Progress(track_tqdm=False),
):
if image is None:
raise gr.Error("Upload an input image first.")
if not torch.cuda.is_available():
raise gr.Error("This Space requires GPU hardware. Assign a GPU in Space settings and try again.")
progress(0.05, desc="Checking model weights")
ensure_checkpoint(version)
seed = int(seed)
job_id = time.strftime("%Y%m%d-%H%M%S")
job_dir = WORK_DIR / job_id
input_path = job_dir / "input.png"
output_dir = job_dir / "outputs"
output_dir.mkdir(parents=True, exist_ok=True)
image.convert("RGBA").save(input_path)
command = [
sys.executable,
"scripts/sampling/simple_video_sample.py",
"--input_path",
str(input_path),
"--version",
version,
"--output_folder",
str(output_dir),
"--num_steps",
str(num_steps),
"--seed",
str(seed),
"--encoding_t",
"1",
"--decoding_t",
"1",
]
if version == "sv3d_p":
command.extend(["--elevations_deg", str(elevation_deg)])
if remove_bg:
command.extend(["--remove_bg", "True"])
progress(0.15, desc="Running SV3D")
result = subprocess.run(
command,
cwd=GEN_MODELS_ROOT,
capture_output=True,
text=True,
env={**os.environ, "PYTHONUNBUFFERED": "1"},
)
logs = "\n".join(
part for part in [result.stdout.strip(), result.stderr.strip()] if part
).strip()
if result.returncode != 0:
raise gr.Error(f"SV3D failed.\n\n{logs[-4000:]}")
progress(0.95, desc="Collecting output")
output_path = newest_output_file(output_dir)
persisted_path = SPACE_ROOT / "latest_output" / output_path.name
persisted_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(output_path, persisted_path)
video_path = str(persisted_path) if persisted_path.suffix.lower() in {".mp4", ".webm"} else None
progress(1.0, desc="Done")
return video_path, str(persisted_path), logs or "Generation completed."
with gr.Blocks(title="SV3D Space") as demo:
gr.Markdown(
"""
# Stable Video 3D
This Space runs `stabilityai/sv3d` with Stability AI's `generative-models` sampler.
Requirements:
- GPU hardware must be attached to the Space.
- Add an `HF_TOKEN` Space secret from an account that has already accepted the
`stabilityai/sv3d` gated model license.
Input tips:
- Best results come from a single centered object on a simple or white background.
- `sv3d_u` generates an unconstrained orbital video from one image.
- `sv3d_p` lets you choose the orbit elevation.
"""
)
with gr.Row():
with gr.Column(scale=1):
image = gr.Image(type="pil", label="Input image")
version = gr.Radio(
choices=["sv3d_u", "sv3d_p"],
value="sv3d_u",
label="Model variant",
)
elevation_deg = gr.Slider(
minimum=-60,
maximum=60,
value=10,
step=1,
label="Elevation (sv3d_p only)",
)
num_steps = gr.Slider(
minimum=10,
maximum=50,
value=30,
step=1,
label="Sampling steps",
)
seed = gr.Number(value=23, precision=0, label="Seed")
remove_bg = gr.Checkbox(
value=False,
label="Remove background before sampling",
)
generate = gr.Button("Generate orbital video", variant="primary")
with gr.Column(scale=1):
output_video = gr.Video(label="Result")
output_file = gr.File(label="Download")
logs = gr.Textbox(label="Logs", lines=18)
generate.click(
fn=run_sv3d,
inputs=[image, version, elevation_deg, num_steps, seed, remove_bg],
outputs=[output_video, output_file, logs],
)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=1).launch(server_name="0.0.0.0", server_port=7860)