Files
vnassets/vnassets/cli.py
Michele Rossi 23135e62cb add Real-ESRGAN upscaling (2x/3x/4x) with anime model
Adds `vnasset upscale` CLI command, `VnAssetsSession.upscale()` / `.upscales()`
session methods, and a standalone `vnassets.upscale` module following the
existing remove-bg pattern.

Uses Real-ESRGAN RRDBNet with RealESRGAN_x4plus_anime_6B (~17 MB,
auto-downloaded). A 256x256 warmup tile at load time eliminates ~80s of
first-run CUDA JIT compilation on ROCm. Steady-state: ~1.8s per 1024->2048
upscale on Strix Halo. The upsampler is lazy-loaded on first call and coexists
with SDXL/Qwen in the same session.

Works around a basicsr/torchvision API incompatibility (rgb_to_grayscale moved
in torchvision 0.20+) with a 3-line module shim in upscale.py.
2026-07-08 12:36:10 +02:00

164 lines
5.9 KiB
Python

"""VNAsset — Visual Novel Asset Pipeline CLI."""
from pathlib import Path
import click
from .background import remove_background as _remove_bg
from .background import remove_backgrounds as _remove_bgs
from .edit import edit
from .generate import generate
from .upscale import upscale as _upscale_fn
from .upscale import upscales as _upscales_fn
def _parse_seed(ctx, param, value):
if value is None or value == "":
return None
if value.lower() == "random":
return None
try:
return int(value)
except ValueError:
raise click.BadParameter("seed must be an integer or 'random'")
@click.group()
def main():
"""VNAsset — fast CLI pipeline for visual novel image assets."""
@main.command()
@click.option("--checkpoint", required=True, help="Path to SDXL checkpoint (.safetensors)")
@click.option("--prompt", required=True, help="Positive prompt")
@click.option("--negative-prompt", default="", help="Negative prompt")
@click.option("--width", default=1024, type=int)
@click.option("--height", default=1024, type=int)
@click.option("--steps", default=20, type=int)
@click.option("--cfg", default=4.5, type=float)
@click.option(
"--seed", default="random", callback=_parse_seed,
help="RNG seed (integer or 'random')",
)
@click.option("--output", default="output.png", help="Output image path")
@click.option(
"--raw", is_flag=True,
help="Disable ComfyUI-style prompt weighting (use plain diffusers encoding)",
)
def generate_cmd(checkpoint, prompt, negative_prompt, width, height, steps, cfg, seed, output, raw):
"""Generate an image from an SDXL checkpoint."""
generate(
checkpoint_path=checkpoint,
prompt=prompt,
negative_prompt=negative_prompt,
width=width,
height=height,
steps=steps,
cfg=cfg,
seed=seed,
output_path=output,
raw=raw,
)
@main.command()
@click.option("--model", required=True, help="Path to Qwen Image Edit diffusion model (.safetensors)")
@click.option("--input", "input_path", required=True, help="Input image to edit")
@click.option("--prompt", required=True, help="Edit instruction")
@click.option("--steps", default=20, type=int, help="Inference steps")
@click.option("--cfg", default=4.0, type=float, help="CFG scale")
@click.option(
"--seed", default="random", callback=_parse_seed,
help="RNG seed (integer or 'random')",
)
@click.option("--output", default="output.png", help="Output image path")
@click.option("--lora", "lora_path", default=None,
help="Path to LoRA .safetensors (e.g. Lightning 4-step LoRA)")
def edit_cmd(model, input_path, prompt, steps, cfg, seed, output, lora_path):
"""Edit an image using Qwen Image Edit."""
edit(
model_path=model,
input_path=input_path,
prompt=prompt,
steps=steps,
cfg=cfg,
seed=seed,
output_path=output,
lora_path=lora_path,
)
REMOVE_BG_MODELS = ["isnet-anime", "u2net", "u2netp", "u2net_human_seg", "isnet-general-use", "sam"]
UPSALE_SCALES = [2, 3, 4]
@main.command("upscale")
@click.option("--input", "input_paths", multiple=True, required=True,
help="Input image path (repeat for batch).")
@click.option("--output", "output_path", default=None,
help="Output path (single-input mode).")
@click.option("--output-dir", default=None,
help="Output directory (batch mode; files named {stem}_x{scale}.png).")
@click.option("--scale", default=2, type=click.Choice(["2", "3", "4"]),
help="Upscale factor (default: 2).")
def upscale_cmd(input_paths, output_path, output_dir, scale):
"""Upscale images using Real-ESRGAN (anime model).
Single file mode:
vnasset upscale --input char.png --output char_2x.png --scale 2
Batch mode (reuses model across files):
vnasset upscale --input base.png --input happy.png --output-dir upscaled/ --scale 2
"""
scale = int(scale)
if len(input_paths) == 1 and output_path:
_upscale_fn(input_paths[0], output_path, scale=scale)
elif output_dir:
_upscales_fn(list(input_paths), output_dir, scale=scale)
elif len(input_paths) == 1:
# Single input, no output specified: auto-name
stem = Path(input_paths[0]).stem
out = Path(input_paths[0]).parent / f"{stem}_x{scale}.png"
_upscale_fn(input_paths[0], str(out), scale=scale)
else:
raise click.UsageError(
"For multiple inputs, use --output-dir. For single input, use --output."
)
@main.command("remove-bg")
@click.option("--input", "input_paths", multiple=True, required=True,
help="Input image path (repeat for batch).")
@click.option("--output", "output_path", default=None,
help="Output path (single-input mode).")
@click.option("--output-dir", default=None,
help="Output directory (batch mode; files named {stem}_nobg.png).")
@click.option("--model", default="isnet-anime", type=click.Choice(REMOVE_BG_MODELS),
help="Background removal model (default: isnet-anime).")
def remove_bg_cmd(input_paths, output_path, output_dir, model):
"""Remove background from images. Output is RGBA PNG.
Single file mode:
vnasset remove-bg --input char.png --output char_nobg.png
Batch mode (reuses model across files):
vnasset remove-bg --input base.png --input happy.png --output-dir transparent/
"""
if len(input_paths) == 1 and output_path:
_remove_bg(input_paths[0], output_path, model=model)
elif output_dir:
_remove_bgs(list(input_paths), output_dir, model=model)
elif len(input_paths) == 1:
# Single input, no output specified: auto-name
stem = Path(input_paths[0]).stem
out = Path(input_paths[0]).parent / f"{stem}_nobg.png"
_remove_bg(input_paths[0], str(out), model=model)
else:
raise click.UsageError(
"For multiple inputs, use --output-dir. For single input, use --output."
)