Clover-Image-Tiny / examples /generate.py
neonforestmist's picture
Release Clover Image Tiny model card and banner
63b0e9f verified
Raw
History Blame Contribute Delete
9.76 kB
#!/usr/bin/env python3
"""Generate images with the Clover Image Tiny public release."""
from __future__ import annotations
import argparse
import hashlib
import io
import json
import math
from pathlib import Path
from typing import Any
import torch
from diffusers import (
DDIMScheduler,
DiffusionPipeline,
DPMSolverMultistepScheduler,
EulerAncestralDiscreteScheduler,
EulerDiscreteScheduler,
PNDMScheduler,
)
DEFAULT_STEPS = 50
DEFAULT_GUIDANCE_SCALE = 7.5
DEFAULT_RESOLUTION = 512
MIN_STEPS = 4
MAX_STEPS = 100
MIN_RESOLUTION = 256
MAX_RESOLUTION = 768
RESOLUTION_MULTIPLE = 64
MAX_IMAGES = 4
MAX_TEXT_LENGTH = 2_000
MAX_SEED = 2**63 - 1
SCHEDULERS: dict[str, type[Any]] = {
"pndm": PNDMScheduler,
"ddim": DDIMScheduler,
"euler": EulerDiscreteScheduler,
"euler-a": EulerAncestralDiscreteScheduler,
"dpmpp-2m": DPMSolverMultistepScheduler,
}
def _runtime(requested: str) -> tuple[torch.device, torch.dtype]:
if requested == "auto":
if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.backends.mps.is_available():
device = torch.device("mps")
else:
device = torch.device("cpu")
else:
device = torch.device(requested)
if device.type == "cuda" and not torch.cuda.is_available():
raise RuntimeError("CUDA was requested but is unavailable")
if device.type == "mps" and not torch.backends.mps.is_available():
raise RuntimeError("MPS was requested but is unavailable")
dtype = torch.float16 if device.type in {"cuda", "mps"} else torch.float32
return device, dtype
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Run Clover Image Tiny with configurable conventional Diffusers settings. "
"The defaults reproduce the validated 50-step reference recipe."
)
)
parser.add_argument("--model", required=True, help="Hub repository ID or local directory")
parser.add_argument("--prompt", required=True)
parser.add_argument("--negative-prompt", default="")
parser.add_argument("--seed", type=int, default=1337)
parser.add_argument(
"--steps",
type=int,
default=DEFAULT_STEPS,
help=f"requested inference steps ({MIN_STEPS}..{MAX_STEPS})",
)
parser.add_argument(
"--guidance-scale",
type=float,
default=DEFAULT_GUIDANCE_SCALE,
help="classifier-free guidance scale (0.0..20.0)",
)
parser.add_argument(
"--width",
type=int,
default=DEFAULT_RESOLUTION,
help=(
f"output width ({MIN_RESOLUTION}..{MAX_RESOLUTION}, multiple of {RESOLUTION_MULTIPLE})"
),
)
parser.add_argument(
"--height",
type=int,
default=DEFAULT_RESOLUTION,
help=(
f"output height ({MIN_RESOLUTION}..{MAX_RESOLUTION}, multiple of {RESOLUTION_MULTIPLE})"
),
)
parser.add_argument(
"--scheduler",
choices=tuple(SCHEDULERS),
default="pndm",
)
parser.add_argument(
"--num-images",
type=int,
default=1,
help=f"number of images (1..{MAX_IMAGES}); seeds increment from --seed",
)
parser.add_argument("--output", type=Path, default=Path("clover-image-tiny.png"))
parser.add_argument("--device", choices=("auto", "cuda", "mps", "cpu"), default="auto")
parser.add_argument("--local-files-only", action="store_true")
return parser
def _validate_args(args: argparse.Namespace) -> None:
if not isinstance(args.prompt, str) or not args.prompt.strip():
raise ValueError("--prompt must not be empty")
if len(args.prompt) > MAX_TEXT_LENGTH:
raise ValueError(f"--prompt must contain at most {MAX_TEXT_LENGTH} characters")
if len(args.negative_prompt) > MAX_TEXT_LENGTH:
raise ValueError(f"--negative-prompt must contain at most {MAX_TEXT_LENGTH} characters")
if not MIN_STEPS <= args.steps <= MAX_STEPS:
raise ValueError(f"--steps must be between {MIN_STEPS} and {MAX_STEPS}")
if not math.isfinite(args.guidance_scale) or not 0.0 <= args.guidance_scale <= 20.0:
raise ValueError("--guidance-scale must be finite and between 0.0 and 20.0")
for name in ("width", "height"):
value = int(getattr(args, name))
if not MIN_RESOLUTION <= value <= MAX_RESOLUTION or value % RESOLUTION_MULTIPLE != 0:
raise ValueError(
f"--{name} must be between {MIN_RESOLUTION} and {MAX_RESOLUTION} "
f"and divisible by {RESOLUTION_MULTIPLE}"
)
if not 1 <= args.num_images <= MAX_IMAGES:
raise ValueError(f"--num-images must be between 1 and {MAX_IMAGES}")
if not 0 <= args.seed <= MAX_SEED or args.seed + args.num_images - 1 > MAX_SEED:
raise ValueError("--seed and all incremented image seeds must be in [0, 2**63)")
if args.output.suffix.lower() != ".png":
raise ValueError("--output must use a .png filename")
def _output_paths(output: Path, count: int) -> tuple[list[Path], Path]:
images = [output]
images.extend(
output.with_name(f"{output.stem}-{index:02d}{output.suffix}")
for index in range(2, count + 1)
)
return images, output.with_suffix(output.suffix + ".json")
def _write_new(path: Path, payload: bytes) -> None:
with path.open("xb") as handle:
handle.write(payload)
def main() -> int:
parser = _parser()
args = parser.parse_args()
try:
_validate_args(args)
except ValueError as exc:
parser.error(str(exc))
device, dtype = _runtime(args.device)
output_paths, metadata_path = _output_paths(args.output, args.num_images)
args.output.parent.mkdir(parents=True, exist_ok=True)
for path in (*output_paths, metadata_path):
if path.exists() or path.is_symlink():
raise FileExistsError(f"planned output already exists: {path}")
pipe = DiffusionPipeline.from_pretrained(
args.model,
torch_dtype=dtype,
local_files_only=args.local_files_only,
)
scheduler_type = SCHEDULERS[args.scheduler]
pipe.scheduler = scheduler_type.from_config(pipe.scheduler.config)
pipe = pipe.to(device)
generator_device = "cuda" if device.type == "cuda" else "cpu"
seeds = [args.seed + index for index in range(args.num_images)]
generators = [torch.Generator(device=generator_device).manual_seed(seed) for seed in seeds]
generator: torch.Generator | list[torch.Generator]
generator = generators[0] if len(generators) == 1 else generators
with torch.inference_mode():
result = pipe(
prompt=args.prompt.strip(),
negative_prompt=args.negative_prompt,
num_inference_steps=args.steps,
guidance_scale=args.guidance_scale,
height=args.height,
width=args.width,
num_images_per_prompt=args.num_images,
generator=generator,
)
if not isinstance(result.images, list) or len(result.images) != args.num_images:
raise RuntimeError("the pipeline returned an unexpected number of images")
safety = getattr(result, "nsfw_content_detected", None)
if not isinstance(safety, list) or len(safety) != args.num_images:
raise RuntimeError("the pipeline returned an unexpected safety-check result")
image_records: list[dict[str, Any]] = []
image_payloads: list[bytes] = []
for image, output_path, seed, nsfw in zip(
result.images,
output_paths,
seeds,
safety,
strict=True,
):
buffer = io.BytesIO()
image.save(buffer, format="PNG")
payload = buffer.getvalue()
image_payloads.append(payload)
image_records.append(
{
"filename": output_path.name,
"seed": seed,
"sha256": hashlib.sha256(payload).hexdigest(),
"nsfw_content_detected": nsfw,
}
)
validated_gallery_recipe = (
args.scheduler == "pndm"
and args.steps == DEFAULT_STEPS
and args.guidance_scale == DEFAULT_GUIDANCE_SCALE
and args.negative_prompt == ""
and args.width == DEFAULT_RESOLUTION
and args.height == DEFAULT_RESOLUTION
and args.num_images == 1
)
metadata = {
"model": args.model,
"prompt": args.prompt.strip(),
"negative_prompt": args.negative_prompt,
"seed": args.seed,
"scheduler": type(pipe.scheduler).__name__,
"scheduler_key": args.scheduler,
"num_inference_steps": args.steps,
"guidance_scale": args.guidance_scale,
"height": args.height,
"width": args.width,
"num_images": args.num_images,
"images": image_records,
"device": device.type,
"dtype": str(dtype).removeprefix("torch."),
"nsfw_content_detected": safety,
"validated_gallery_recipe": validated_gallery_recipe,
"leaf_steps": None,
"cross_device_pixel_identity_claimed": False,
"usage_label": "CLOVER IMAGE TINY - PUBLIC CHECKPOINT RELEASE",
}
for output_path, payload in zip(output_paths, image_payloads, strict=True):
_write_new(output_path, payload)
_write_new(metadata_path, (json.dumps(metadata, indent=2) + "\n").encode("utf-8"))
print(
json.dumps(
{
"output": str(output_paths[0]),
"outputs": [str(path) for path in output_paths],
"metadata": str(metadata_path),
**metadata,
}
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())