from __future__ import annotations import argparse from pathlib import Path from qwen_image_edit_lite.backend import ( DEFAULT_CONFIG_PATH, AppConfig, DEFAULT_DOWNLOAD_CACHE_DIR, DEFAULT_LORAS_DIR, InferenceRequest, ensure_valid_resolution, flatten_extra_args, load_config, normalize_lora_scales, resolve_path_from_base, run_inference, ) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Minimal Qwen Image Edit 2511 inference wrapper built on copied ComfyUI runtime code." ) parser.add_argument("--input", nargs="+", required=True, help="One or more input images. image1 is required.") parser.add_argument("--prompt", required=True, help="Edit instruction.") parser.add_argument("--negative-prompt", default=None, help="Optional negative prompt.") parser.add_argument("--output-dir", default="outputs", help="Directory for generated images.") parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Optional config.ini path.") parser.add_argument("--models-dir", default=None, help="Override base model directory.") parser.add_argument("--diffusion-models-dir", default=None, help="Override diffusion model directory.") parser.add_argument("--vae-dir", default=None, help="Override VAE directory.") parser.add_argument("--text-encoder-dir", default=None, help="Override text encoder directory.") parser.add_argument("--loras-dir", default=None, help="Override LoRA directory.") parser.add_argument("--lora", nargs="*", default=[], help="Zero or more Qwen Image Edit LoRA files.") parser.add_argument( "--lora-scale", nargs="*", type=float, default=None, help="LoRA multipliers. If omitted, all LoRA files use 1.0.", ) parser.add_argument("--width", type=int, default=1024, help="Output width. Must be divisible by 16.") parser.add_argument("--height", type=int, default=1024, help="Output height. Must be divisible by 16.") parser.add_argument("--steps", type=int, default=30, help="Number of denoising steps.") parser.add_argument("--cfg", type=float, default=4.0, help="Classifier-free guidance scale.") parser.add_argument("--seed", type=int, default=None, help="Random seed.") parser.add_argument("--flow-shift", type=float, default=None, help="Optional AuraFlow shift override.") parser.add_argument("--mask", default=None, help="Optional inpainting mask path.") parser.add_argument("--device", default=None, help="Torch device, for example cuda, cuda:0, or cpu.") parser.add_argument("--attn-mode", choices=("torch", "sdpa", "flash", "xformers"), default=None) parser.add_argument("--disable-xformers-qwen-patch", action="store_true", help="Use stock ComfyUI xformers without the Lite Qwen compact patch.") parser.add_argument("--text-encoder-cpu", action="store_true", help="Run the text encoder on CPU.") parser.add_argument("--extra-arg", action="append", default=[], help="Reserved compatibility option.") return parser def build_runtime_config(args: argparse.Namespace) -> AppConfig: base_config = load_config(args.config) project_root = base_config.project_root if args.models_dir: models_dir = resolve_path_from_base(args.models_dir, project_root, "models_dir") diffusion_models_dir = resolve_path_from_base("diffusion_models", models_dir, "diffusion_models") vae_dir = resolve_path_from_base("vae", models_dir, "vae") text_encoders_dir = resolve_path_from_base("text_encoders", models_dir, "text_encoders") else: diffusion_models_dir = base_config.diffusion_models_dir vae_dir = base_config.vae_dir text_encoders_dir = base_config.text_encoders_dir if args.diffusion_models_dir: diffusion_models_dir = resolve_path_from_base(args.diffusion_models_dir, project_root, "diffusion_models") if args.vae_dir: vae_dir = resolve_path_from_base(args.vae_dir, project_root, "vae") if args.text_encoder_dir: text_encoders_dir = resolve_path_from_base(args.text_encoder_dir, project_root, "text_encoders") loras_dir = base_config.loras_dir if args.loras_dir: loras_dir = resolve_path_from_base(args.loras_dir, project_root, "loras") return AppConfig( project_root=project_root, diffusion_models_ref=str(diffusion_models_dir), vae_ref=str(vae_dir), text_encoders_ref=str(text_encoders_dir), loras_dir=loras_dir, download_cache_dir=(project_root / DEFAULT_DOWNLOAD_CACHE_DIR).resolve(), hf_token=base_config.hf_token, text_encoder_cpu=args.text_encoder_cpu if args.text_encoder_cpu else base_config.text_encoder_cpu, device=args.device or base_config.device, attn_mode=args.attn_mode or base_config.attn_mode, xformers_qwen_patch=False if args.disable_xformers_qwen_patch else base_config.xformers_qwen_patch, default_steps=base_config.default_steps, default_cfg=base_config.default_cfg, default_flow_shift=base_config.default_flow_shift, presets=base_config.presets, ) def resolve_lora_paths(lora_args: list[str], loras_dir: Path) -> list[Path]: resolved = [] for raw in lora_args: path = Path(raw).expanduser() if path.is_absolute(): resolved.append(path.resolve()) continue candidate = (loras_dir / path).resolve() if candidate.exists(): resolved.append(candidate) continue resolved.append((Path.cwd() / path).resolve()) return resolved def main() -> None: args = build_parser().parse_args() ensure_valid_resolution(args.width, args.height) config = build_runtime_config(args) input_paths = [Path(raw).expanduser().resolve() for raw in args.input] lora_paths = resolve_lora_paths(args.lora, config.loras_dir) request = InferenceRequest( prompt=args.prompt, negative_prompt=args.negative_prompt, control_images=input_paths, lora_paths=lora_paths, lora_scales=normalize_lora_scales(lora_paths, args.lora_scale), width=args.width, height=args.height, steps=args.steps, cfg=args.cfg, seed=args.seed, flow_shift=args.flow_shift, mask=Path(args.mask).expanduser().resolve() if args.mask else None, output_dir=Path(args.output_dir).expanduser().resolve(), extra_args=flatten_extra_args(args.extra_arg), ) run_inference(config, request) if __name__ == "__main__": main()