Text-to-Image
Diffusers
Safetensors
StableDiffusionPipeline
clover-image
diffusion
stable-diffusion
knowledge-distillation
compact
local-inference
Instructions to use neonforestmist/Clover-Image-Tiny with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use neonforestmist/Clover-Image-Tiny with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("neonforestmist/Clover-Image-Tiny", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
| #!/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()) | |