BareGit
"""Command line interface for standalone diffusion generation."""

from __future__ import annotations

import argparse
import gc
from pathlib import Path
import sys

from diffusion_cli.checkpoint import inspectSourceTorchDtype
from diffusion_cli.api_profiles import API_PROFILES
from diffusion_cli.config import (
    UserConfig,
    buildGenerationConfig,
    loadUserConfig,
)
from diffusion_cli.errors import DiffusionCliError
from diffusion_cli.image_io import saveImages
from diffusion_cli.model_inspect import formatSummary, inspectModelSource
from diffusion_cli.paths import resolveModelSources
from diffusion_cli.sampling import sampleLatents
from diffusion_cli.server import serve, validateServerConfig
from diffusion_cli.text_encoder import ZImageTextEncoder
from diffusion_cli.vae import ZImageVae
from diffusion_cli.zimage_model import ZImageModel


def buildParser() -> argparse.ArgumentParser:
    """Build the argparse command line parser."""

    parser = argparse.ArgumentParser(
        prog="diffusion-cli",
        description="Standalone local Z-Image Turbo CLI.",
    )
    parser.add_argument("--prompt", help="Positive prompt.")
    parser.add_argument(
        "--negative-prompt",
        help="Negative prompt.",
    )
    parser.add_argument("--seed", type=int, help="Noise seed.")
    parser.add_argument("--width", type=int)
    parser.add_argument("--height", type=int)
    parser.add_argument("--batch-size", type=int)
    parser.add_argument("--steps", type=int)
    parser.add_argument("--cfg", type=float)
    parser.add_argument("--output", type=Path)
    parser.add_argument("--output-extension")
    parser.add_argument("--output-quality", type=int)
    parser.add_argument("--device")
    parser.add_argument(
        "--checkpoint",
        type=Path,
        help="Local all-in-one safetensors checkpoint file.",
    )
    parser.add_argument(
        "--diffusion-model",
        type=Path,
        help="Local Z-Image diffusion model safetensors file.",
    )
    parser.add_argument(
        "--text-encoder",
        type=Path,
        help="Local Qwen text encoder safetensors file.",
    )
    parser.add_argument(
        "--vae",
        type=Path,
        help="Local VAE safetensors file.",
    )
    parser.add_argument(
        "--tokenizer-path",
        type=Path,
        help="Local Qwen tokenizer directory.",
    )
    parser.add_argument(
        "--dtype",
        choices=("auto", "bf16", "fp16", "fp32"),
    )
    parser.add_argument(
        "--inspect-models",
        action="store_true",
        help="Inspect local safetensors metadata and exit.",
    )
    subparsers = parser.add_subparsers(dest="command")
    serve_parser = subparsers.add_parser(
        "serve",
        help="Start a long-running HTTP API server.",
    )
    serve_parser.add_argument(
        "--api-profile",
        required=True,
        choices=tuple(API_PROFILES),
        help="HTTP API profile to expose.",
    )
    serve_parser.add_argument(
        "--host",
        default="127.0.0.1",
        help="Host interface to bind.",
    )
    serve_parser.add_argument(
        "--port",
        default=7860,
        type=int,
        help="TCP port to bind.",
    )
    serve_parser.add_argument(
        "--model-residency",
        default="cpu-cache",
        choices=("staged", "cpu-cache"),
        help="How server mode keeps model components resident.",
    )
    return parser


def inspectModels(args, user_config: UserConfig) -> None:
    """Print metadata summaries for the required model files."""

    model_sources = resolveModelSources(args, user_config)
    summaries = [
        ("diffusion model", inspectModelSource(model_sources.diffusion_model)),
        ("text encoder", inspectModelSource(model_sources.text_encoder)),
        ("VAE", inspectModelSource(model_sources.vae)),
    ]
    output = "\n\n".join(
        formatSummary(name, summary) for name, summary in summaries
    )
    print(output)


def releaseMemory() -> None:
    """Release Python and CUDA caches between large model stages."""

    gc.collect()
    try:
        import torch

        if torch.cuda.is_available():
            torch.cuda.empty_cache()
    except ImportError:
        pass


def componentDtype(config, source):
    """Return the runtime dtype for one component source."""

    if config.dtype_name != "auto":
        return config.dtype
    return inspectSourceTorchDtype(source) or config.dtype


def generate(args, user_config: UserConfig) -> list[Path]:
    """Validate generation arguments and run the current milestone."""

    config = buildGenerationConfig(args, user_config)
    model_sources = resolveModelSources(args, user_config)
    text_dtype = componentDtype(config, model_sources.text_encoder)
    diffusion_dtype = componentDtype(config, model_sources.diffusion_model)
    vae_dtype = componentDtype(config, model_sources.vae)

    text_encoder = ZImageTextEncoder(
        model_sources.text_encoder,
        config.tokenizer_path,
        config.device,
        text_dtype,
    )
    conditioning = text_encoder.encodePrompts(
        config.prompt,
        config.negative_prompt,
    )
    del text_encoder
    releaseMemory()

    model = ZImageModel(
        model_sources.diffusion_model,
        config.device,
        diffusion_dtype,
    )
    latent = sampleLatents(
        model,
        conditioning,
        batch_size=config.batch_size,
        height=config.height,
        width=config.width,
        seed=config.seed,
        steps=config.steps,
        cfg=config.cfg,
        device=config.device,
        dtype=diffusion_dtype,
    )
    del model
    releaseMemory()

    vae = ZImageVae(model_sources.vae, config.device, vae_dtype)
    images = vae.decode(latent)
    return saveImages(
        images,
        config.output,
        config.output_extension,
        config.output_quality,
    )


def main(argv: list[str] | None = None) -> int:
    """Run the CLI entry point."""

    parser = buildParser()
    args = parser.parse_args(argv)

    try:
        user_config = loadUserConfig()
        if args.command == "serve":
            server_config = validateServerConfig(
                args.api_profile,
                args.host,
                args.port,
                args.model_residency,
            )
            serve(server_config, user_config)
            return 0

        if args.inspect_models:
            inspectModels(args, user_config)
            return 0

        paths = generate(args, user_config)
    except DiffusionCliError as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 2

    for path in paths:
        print(path)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())