amv-diffusion 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. amv_diffusion-2.1.0.dist-info/METADATA +180 -0
  2. amv_diffusion-2.1.0.dist-info/RECORD +39 -0
  3. amv_diffusion-2.1.0.dist-info/WHEEL +5 -0
  4. amv_diffusion-2.1.0.dist-info/entry_points.txt +7 -0
  5. amv_diffusion-2.1.0.dist-info/licenses/LICENSE +21 -0
  6. amv_diffusion-2.1.0.dist-info/top_level.txt +1 -0
  7. termux_diffusion/__init__.py +185 -0
  8. termux_diffusion/__main__.py +7 -0
  9. termux_diffusion/_version.py +3 -0
  10. termux_diffusion/adapter.py +41 -0
  11. termux_diffusion/canvas.py +364 -0
  12. termux_diffusion/cli.py +492 -0
  13. termux_diffusion/cluster.py +340 -0
  14. termux_diffusion/control/__init__.py +5 -0
  15. termux_diffusion/control/component.py +309 -0
  16. termux_diffusion/control/status.py +15 -0
  17. termux_diffusion/core.py +2936 -0
  18. termux_diffusion/data/bootstrap-manifest.json +15 -0
  19. termux_diffusion/data/prompt_cache/108a7149c4e70550.json +10 -0
  20. termux_diffusion/data/release-public-keys.json +10 -0
  21. termux_diffusion/data/validated-vulkan-profiles.json +298 -0
  22. termux_diffusion/detailer.py +283 -0
  23. termux_diffusion/doctor.py +128 -0
  24. termux_diffusion/downloader.py +111 -0
  25. termux_diffusion/engine.py +226 -0
  26. termux_diffusion/exceptions.py +158 -0
  27. termux_diffusion/hardware.py +985 -0
  28. termux_diffusion/hub.py +686 -0
  29. termux_diffusion/installer.py +499 -0
  30. termux_diffusion/locking.py +134 -0
  31. termux_diffusion/manifest.py +128 -0
  32. termux_diffusion/npu.py +267 -0
  33. termux_diffusion/platform.py +533 -0
  34. termux_diffusion/purger.py +145 -0
  35. termux_diffusion/py.typed +1 -0
  36. termux_diffusion/safety.py +150 -0
  37. termux_diffusion/selftest.py +178 -0
  38. termux_diffusion/web.py +3370 -0
  39. termux_diffusion/workflow.py +1345 -0
@@ -0,0 +1,492 @@
1
+ # Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0 OR Commercial-License
3
+ """Command-line interface entry points for termux-diffusion."""
4
+
5
+ import argparse
6
+ import sys
7
+ from typing import List, Optional
8
+
9
+ import logging as _logging
10
+
11
+ if hasattr(sys.stdout, "reconfigure"):
12
+ try:
13
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
14
+ except Exception as _e:
15
+ _logging.getLogger(__name__).debug("stdout reconfigure failed: %s", _e)
16
+ if hasattr(sys.stderr, "reconfigure"):
17
+ try:
18
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
19
+ except Exception as _e:
20
+ _logging.getLogger(__name__).debug("stderr reconfigure failed: %s", _e)
21
+
22
+ from .core import generate
23
+ from .hub import clear_cache, download_model, list_cached_models, list_presets
24
+ from .exceptions import TermuxDiffusionError, RuntimeRequiredError, ErrorCode, ExitCode
25
+ from .engine import DiffusionEngine, get_binary_path
26
+ from .doctor import run_doctor
27
+ from .installer import provision_engine
28
+
29
+
30
+ def main(argv: Optional[List[str]] = None) -> int:
31
+ """Main CLI router for termux-diffusion.
32
+
33
+ A GPU request without a usable ameva-runtime ends here with the shared explanation on
34
+ stderr and exit status 69, whichever sub-command raised it.
35
+ """
36
+ try:
37
+ return _main_impl(argv)
38
+ except RuntimeRequiredError as err:
39
+ sys.stderr.write(str(err))
40
+ sys.stderr.flush()
41
+ return ExitCode.RUNTIME_REQUIRED
42
+
43
+
44
+ def _main_impl(argv: Optional[List[str]] = None) -> int:
45
+ """Main CLI router for termux-diffusion."""
46
+ if argv is None:
47
+ argv = sys.argv[1:]
48
+
49
+ parser = argparse.ArgumentParser(
50
+ prog="termux-diffusion",
51
+ description="Production On-Device AI Image Generation for Android Termux & Samsung Galaxy."
52
+ )
53
+ subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
54
+
55
+ # generate command
56
+ gen_parser = subparsers.add_parser("generate", help="Generate an AI image from a text prompt")
57
+ gen_parser.add_argument("prompt", type=str, help="Text description of image")
58
+ gen_parser.add_argument("-m", "--model", type=str, default="realistic", help="Model preset or .gguf path (default: realistic)")
59
+ gen_parser.add_argument("-n", "--negative", type=str, default=None, help="Negative text prompt guidance")
60
+ gen_parser.add_argument("-d", "--device", type=str, default="cpu", choices=["auto", "gpu", "vulkan", "opencl", "cpu"], help="Computing device: auto, gpu, vulkan, opencl, cpu (default: cpu)")
61
+ gen_parser.add_argument("-b", "--backend", type=str, dest="device", choices=["auto", "gpu", "vulkan", "opencl", "cpu"], help="Alias for --device")
62
+ gen_parser.add_argument("--gpu", action="store_const", const="gpu", dest="device", help="Force GPU hardware acceleration mode")
63
+ gen_parser.add_argument("--cpu", action="store_const", const="cpu", dest="device", help="Force CPU baseline execution mode")
64
+ gen_parser.add_argument("-s", "--steps", type=int, default=None, help="Denoising steps (default determined by preset, e.g. 10)")
65
+ gen_parser.add_argument("-c", "--cfg", type=float, default=None, help="CFG guidance scale")
66
+ gen_parser.add_argument("-g", "--guidance", type=float, default=None, help="Distilled guidance scale (e.g. 3.5 for SDXS)")
67
+ gen_parser.add_argument("-W", "--width", type=int, default=512, help="Image width (default: 512)")
68
+ gen_parser.add_argument("-H", "--height", type=int, default=512, help="Image height (default: 512)")
69
+ gen_parser.add_argument("-t", "--threads", type=int, default=None, help="CPU threads")
70
+ gen_parser.add_argument("--seed", type=int, default=-1, help="RNG seed (-1 for random, 0 to 4294967295)")
71
+ gen_parser.add_argument("-o", "--output", type=str, default=None, help="Output file path")
72
+ gen_parser.add_argument("--schedule", "--scheduler", dest="schedule", type=str, default=None, help="Noise schedule (flux, karras, exponential, ays, gits, discrete, etc.)")
73
+ gen_parser.add_argument("--flow-shift", type=float, default=None, help="Shift value for Flow models like Flux/Z-Image")
74
+ gen_parser.add_argument("--detail-faces", "--face-detailer", action="store_true", default=False, help="Enable Mobile FaceDetailer (auto face crop, local Img2Img, seamless alpha blend)")
75
+ gen_parser.add_argument("--detailer-strength", type=float, default=0.35, help="FaceDetailer denoising strength (default: 0.35)")
76
+ gen_parser.add_argument("--detailer-steps", type=int, default=2, help="FaceDetailer localized sampling steps (default: 2)")
77
+ gen_parser.add_argument("--detailer-padding", type=float, default=1.45, help="FaceDetailer bounding box padding factor (default: 1.45)")
78
+ gen_parser.add_argument("--vae-tiling", action="store_true", help="Enable VAE tiling for ~70%% lower peak memory")
79
+ gen_parser.add_argument("-i", "--init-img", type=str, default=None, help="Source image for Img2Img synthesis")
80
+ gen_parser.add_argument("--strength", type=float, default=None, help="Img2Img denoising strength (0.0 to 1.0, default: 0.75)")
81
+ gen_parser.add_argument("--lora-dir", type=str, default=None, help="Directory path containing LoRA weights")
82
+ gen_parser.add_argument("--clip-skip", type=int, default=None, help="CLIP layers to skip (1 or 2)")
83
+ gen_parser.add_argument("--control-net", type=str, default=None, help="Path to ControlNet model")
84
+ gen_parser.add_argument("--control-image", type=str, default=None, help="Path to ControlNet guide image")
85
+ gen_parser.add_argument("--control-strength", type=float, default=None, help="ControlNet strength (0.0 to 2.0, default: 0.9)")
86
+ gen_parser.add_argument("--taesd", "--tae", "--fast-vae", dest="taesd", nargs="?", const="auto", type=str, default=None, help="Enable Tiny AutoEncoder (TAESD) for ~1s fast decoding. Pass optional path or 'auto'.")
87
+ gen_parser.add_argument("--strict-vulkan", action="store_true", help="Disallow CPU fallback and fail if Vulkan physical GPU discovery fails")
88
+ gen_parser.add_argument("--preset", type=str, default=None, choices=["fast", "balanced", "anime-experimental", "anime_experimental", "z-image-turbo", "z-image", "turbo-6b", "sdxs"], help="Verified production preset (fast, balanced, anime-experimental, z-image-turbo)")
89
+ gen_parser.add_argument("--llm", type=str, default=None, help="Path or preset for LLM text encoder (e.g. Qwen for Z-Image)")
90
+ gen_parser.add_argument("--diffusion-model", type=str, default=None, help="Path or preset for standalone diffusion model")
91
+ gen_parser.add_argument("--vae", type=str, default=None, help="Path or preset for standalone VAE model")
92
+ gen_parser.add_argument("--clip-l", type=str, default=None, help="Path to standalone CLIP text encoder")
93
+ gen_parser.add_argument("--diffusion-fa", action="store_true", default=False, help="Enable Flash Attention in diffusion model")
94
+ gen_parser.add_argument("--offload-to-cpu", action="store_true", default=False, help="Offload idle modules to CPU RAM to minimize peak VRAM")
95
+ gen_parser.add_argument("--clip-on-cpu", action="store_true", default=False, help="Keep CLIP / LLM text encoder on CPU to preserve GPU VRAM")
96
+ gen_parser.add_argument("--vae-on-cpu", action="store_true", default=False, help="Keep VAE decoder on CPU to preserve GPU VRAM")
97
+ gen_parser.add_argument("--mmap", action="store_true", default=True, help="Memory-map model weights (default: True)")
98
+ gen_parser.add_argument("--no-mmap", action="store_false", dest="mmap", help="Disable memory-mapping")
99
+ gen_parser.add_argument("--max-vram", type=str, default=None, help="Maximum VRAM budget in GiB (e.g. '0' for full in-VRAM residency, '1' or 'vulkan0=1' for 1GiB lifeboat streaming, '1.5' for balanced chunk buffer). Auto-scales deterministically by kernel RAM tier if omitted.")
100
+ gen_parser.add_argument("--stream-layers", action="store_true", default=False, help="Enable layer residency prefetch streaming on top of --max-vram (Tier 3 fallback; disabled in Tier 1/2)")
101
+ gen_parser.add_argument("--params-backend", type=str, default=None, help="Parameter backend assignment (e.g. 'diffusion=cpu' or 'disk')")
102
+ gen_parser.add_argument("--runtime-backend", type=str, default=None, dest="runtime_backend", help="Custom runtime backend assignment (e.g. 'clip=cpu,vae=cpu,diffusion=vulkan0')")
103
+ gen_parser.add_argument("--rpc", type=str, default=None, help="Distributed RPC server addresses (e.g. '192.168.0.220:50052,192.168.0.253:50052')")
104
+ gen_parser.add_argument("-ts", "--tensor-split", type=str, default=None, help="Fraction of the model to offload across devices (e.g. '50,50' or '60,40')")
105
+ gen_parser.add_argument("--vae-format", type=str, default=None, help="VAE latent format override (auto, flux, sd3, flux2, wan)")
106
+ gen_parser.add_argument("--no-cache", "--non-cache", "--no-cached", dest="no_cache", action="store_true", default=False, help="Bypass prompt embedding cache and force full real-time text encoder execution")
107
+ gen_parser.add_argument("--progress", "--show-progress", action="store_true", default=False, help="Display real-time interactive terminal progress bar with step/speed/ETA metrics")
108
+ gen_parser.add_argument("--json-progress", "--progress-json", action="store_true", default=False, help="Stream structured NDJSON progress events for IPC and programmatic consumers")
109
+ gen_parser.add_argument("--progress-file", type=str, default=None, help="Path to write atomic JSON progress snapshots for external monitoring")
110
+ gen_parser.add_argument("--sampling-method", type=str, dest="sampler", help=argparse.SUPPRESS)
111
+ gen_parser.add_argument("--cfg-scale", type=float, dest="cfg", help=argparse.SUPPRESS)
112
+ gen_parser.add_argument("--isolate-text-encoder", "--sequential-stages", dest="isolate_text_encoder", action="store_true", default=False, help="Enable 2-stage memory isolation: execute text encoder independently, kill process to recover RAM, then execute DiT inference")
113
+ gen_parser.add_argument("--safe-mode", action="store_true", default=False, help="Preserve SurfaceFlinger 60/120fps UI and background apps (nice +10, VAE Tiling, 90%% GPU duty cycle)")
114
+ gen_parser.add_argument("--keep-apps-alive", action="store_true", default=False, help="Alias for --safe-mode")
115
+ gen_parser.add_argument("--gpu-duty-cycle", type=int, default=None, help="GPU load percentage cap (e.g. 90 for 90%% duty cycle, 10%% idle gap for SurfaceFlinger)")
116
+ # Safe-By-Default & Clean-Room Purger Flags (SCRUM-524)
117
+ gen_parser.add_argument("--kill", dest="kill_threshold", nargs="?", const=50, default=None, type=int, help="Optional clean-room purge: terminate background processes exceeding RSS threshold in MB, excluding whitelist (default: 50MB)")
118
+ gen_parser.add_argument("--kill-threshold", dest="kill_threshold", type=int, help="Explicit memory threshold in MB for --kill clean-room purge")
119
+ gen_parser.add_argument("--swap-allowance", type=str, default="auto", help="Allowed swap headroom limit: 'auto', '0', '1G', '2G' (default: auto)")
120
+ gen_parser.add_argument("--safe-budget", type=float, default=0.95, help="Safe memory budget ratio of MemAvailable (default: 0.95 for 5%% OS margin)")
121
+ gen_parser.add_argument("--full-accel", "--danger-force", dest="full_accel", action="store_true", default=False, help="Bypass all heuristic memory safety rails and run hardware at maximum unconstrained throttle")
122
+ gen_parser.add_argument("--checkpoint-latent", action="store_true", default=False, help="Save intermediate 4D latent tensor to disk immediately upon DiT completion")
123
+ gen_parser.add_argument("--resume-latent", type=str, default=None, help="Bypass DiT and resume from pre-computed .bin latent tensor for VAE decode")
124
+ gen_parser.add_argument("--temporal-chunking", type=int, default=None, help="Optional fast preview: chunk frames into N-frame slices (e.g. 9) to accelerate 3D attention")
125
+ gen_parser.add_argument("--tile-size", type=int, default=None, help="Explicit spatial VAE tile size override in pixels (e.g. 128, 256)")
126
+ gen_parser.add_argument("--temp-tile", type=int, default=None, help="Explicit temporal VAE tile size override in frames (e.g. 2, 4)")
127
+
128
+ # install command
129
+ inst_parser = subparsers.add_parser("install", help="Provision native Bionic CPU engine (sd-cli-cpu & libomp.so)")
130
+ inst_parser.add_argument("-f", "--force", action="store_true", default=False, help="Force re-installation")
131
+ inst_parser.add_argument("--dedicate", action="store_true", default=False, help="Smart inspection mode: preserve AMEVA runtime symlink, auto-upgrade legacy binaries")
132
+
133
+ # doctor command
134
+ subparsers.add_parser("doctor", help="Run 7-tier pre-flight diagnostic checks")
135
+
136
+ # models command
137
+ subparsers.add_parser("models", help="List available model presets and locally cached weights")
138
+
139
+ # download command
140
+ dl_parser = subparsers.add_parser("download", help="Pre-download a model preset or HF repo")
141
+ dl_parser.add_argument("model", type=str, help="Model preset name (e.g. realistic, speed, sdxs, turbo, anime, fast, balanced)")
142
+
143
+ # clear-cache command
144
+ subparsers.add_parser("clear-cache", help="Purge cached model weights to reclaim disk space")
145
+
146
+ # workflow command (ComfyUI DAG execution)
147
+ wf_parser = subparsers.add_parser("workflow", help="Execute ComfyUI workflows (UI & API JSON formats)")
148
+ wf_sub = wf_parser.add_subparsers(dest="workflow_command", help="Workflow subcommands")
149
+
150
+ wf_run = wf_sub.add_parser("run", help="Execute ComfyUI workflow JSON end-to-end")
151
+ wf_run.add_argument("workflow_file", type=str, help="Path to ComfyUI workflow JSON (UI or API format)")
152
+ wf_run.add_argument("-o", "--output-dir", type=str, default=None, help="Directory to save generated outputs")
153
+ wf_run.add_argument("-d", "--device", type=str, default="auto", choices=["auto", "gpu", "vulkan", "opencl", "cpu"], help="Compute device (default: auto)")
154
+ wf_run.add_argument("--force-run", action="store_true", default=False, help="Bypass hardware limits for high-RAM (16GB/24GB+) devices")
155
+ wf_run.add_argument("--progress", action="store_true", default=True, help="Display progress bar")
156
+ wf_run.add_argument("--json-progress", action="store_true", default=False, help="Stream NDJSON progress events for Node.js / IPC")
157
+
158
+ wf_inspect = wf_sub.add_parser("inspect", help="Inspect and validate ComfyUI workflow nodes and execution order")
159
+ wf_inspect.add_argument("workflow_file", type=str, help="Path to ComfyUI workflow JSON")
160
+
161
+ # web command (Mobile Web UI server - AmfyUI)
162
+ web_parser = subparsers.add_parser("web", aliases=["ui", "amfy", "amfyui"], help="Launch AmfyUI Mobile Studio & ComfyUI-compatible DAG Visualizer")
163
+ web_parser.add_argument("-p", "--port", type=int, nargs="?", const=11553, default=11553, help="Web server port (default: 11553)")
164
+ web_parser.add_argument("-H", "--host", type=str, default="0.0.0.0", help="Host interface to bind (default: 0.0.0.0)")
165
+
166
+ # ── AMEVA Component Protocol v1 ─────────────────────────────────────────
167
+ try:
168
+ from ameva_component.cli_support import build_protocol_subcommands
169
+ build_protocol_subcommands(subparsers)
170
+ except ImportError as _proto_err:
171
+ import logging
172
+ logging.getLogger("termux_diffusion.cli").debug(
173
+ "Optional AMEVA component CLI protocol subcommands omitted: %s", _proto_err
174
+ )
175
+ # ────────────────────────────────────────────────────────────────────────
176
+
177
+ if len(argv) == 0:
178
+ parser.print_help()
179
+ return 0
180
+
181
+ args, unknown_args = parser.parse_known_args(argv)
182
+
183
+ if args.command == "generate":
184
+ if args.seed < -1 or args.seed > 4294967295:
185
+ parser.error("--seed must be between -1 and 4294967295.")
186
+
187
+ model_name = args.model
188
+ steps = args.steps
189
+ cfg = args.cfg
190
+ width = args.width
191
+ height = args.height
192
+ sampler = args.sampler
193
+ device = args.device or "cpu"
194
+ vae_tiling = args.vae_tiling
195
+
196
+ if args.preset:
197
+ p_key = args.preset.replace("_", "-").lower()
198
+ if p_key in ("z-image", "turbo-6b"):
199
+ p_key = "z-image-turbo"
200
+ from .hub import DEFAULT_PRESETS
201
+ if p_key in DEFAULT_PRESETS:
202
+ p_cfg = DEFAULT_PRESETS[p_key]
203
+ if "alias" in p_cfg and p_cfg["alias"] in DEFAULT_PRESETS:
204
+ p_cfg = DEFAULT_PRESETS[p_cfg["alias"]]
205
+ model_name = p_cfg.get("alias", p_key)
206
+ if steps is None:
207
+ steps = p_cfg.get("default_steps")
208
+ if cfg is None:
209
+ cfg = p_cfg.get("default_cfg")
210
+ if width == 512 and "default_width" in p_cfg:
211
+ width = p_cfg["default_width"]
212
+ if height == 512 and "default_height" in p_cfg:
213
+ height = p_cfg["default_height"]
214
+ if sampler is None:
215
+ sampler = p_cfg.get("default_sampler")
216
+ if device in ("auto", "cpu") and "default_device" in p_cfg:
217
+ device = p_cfg["default_device"]
218
+ if not vae_tiling and p_cfg.get("default_vae_tiling"):
219
+ vae_tiling = True
220
+ # Auto-bind DiT multi-component weights
221
+ if not args.diffusion_model and "default_diffusion_model" in p_cfg:
222
+ args.diffusion_model = p_cfg["default_diffusion_model"]
223
+ elif not args.diffusion_model and p_key == "z-image-turbo":
224
+ args.diffusion_model = p_cfg.get("filename", "z_image_turbo-Q2_K.gguf")
225
+ if not args.llm and "default_llm" in p_cfg:
226
+ args.llm = p_cfg["default_llm"]
227
+ if not args.vae and not args.taesd and "default_vae" in p_cfg:
228
+ args.vae = p_cfg["default_vae"]
229
+ if not args.vae_format and "default_vae_format" in p_cfg:
230
+ args.vae_format = p_cfg["default_vae_format"]
231
+
232
+ # Safe-By-Default & Clean-Room Purger Execution (SCRUM-524)
233
+ if getattr(args, "kill_threshold", None) is not None:
234
+ from .purger import purge_idle_processes
235
+ purge_summary = purge_idle_processes(threshold_mb=float(args.kill_threshold))
236
+ print(f"[Purger] Clean-room purge: reclaimed {purge_summary.total_reclaimed_mb:.1f} MB across {len(purge_summary.killed_processes)} processes.")
237
+
238
+ from .safety import evaluate_safe_memory_budget
239
+ mem_assessment = evaluate_safe_memory_budget(
240
+ safe_margin=getattr(args, "safe_budget", 0.95),
241
+ swap_allowance=getattr(args, "swap_allowance", "auto")
242
+ )
243
+
244
+ # Safe-By-Default: 타일링 자동 인젝션 (풀악셀 명시적 지정 시에만 생략 가능)
245
+ if not getattr(args, "full_accel", False) and not vae_tiling:
246
+ vae_tiling = True
247
+
248
+ try:
249
+ res = generate(
250
+ prompt=args.prompt,
251
+ model=model_name,
252
+ negative_prompt=args.negative,
253
+ device=device,
254
+ steps=steps,
255
+ cfg_scale=cfg,
256
+ guidance=args.guidance,
257
+ width=width,
258
+ height=height,
259
+ threads=args.threads,
260
+ seed=args.seed,
261
+ output=args.output,
262
+ sampling_method=sampler,
263
+ schedule=args.schedule,
264
+ vae_tiling=vae_tiling,
265
+ init_img=args.init_img,
266
+ strength=args.strength,
267
+ lora_dir=args.lora_dir,
268
+ clip_skip=args.clip_skip,
269
+ control_net=args.control_net,
270
+ control_image=args.control_image,
271
+ control_strength=args.control_strength,
272
+ taesd=args.taesd,
273
+ llm=args.llm,
274
+ diffusion_model=args.diffusion_model,
275
+ vae=args.vae,
276
+ clip_l=args.clip_l,
277
+ diffusion_fa=args.diffusion_fa,
278
+ offload_to_cpu=args.offload_to_cpu,
279
+ clip_on_cpu=args.clip_on_cpu,
280
+ vae_on_cpu=args.vae_on_cpu,
281
+ mmap=args.mmap,
282
+ max_vram=args.max_vram,
283
+ stream_layers=args.stream_layers,
284
+ params_backend=args.params_backend,
285
+ custom_backend=args.runtime_backend,
286
+ rpc=args.rpc,
287
+ tensor_split=args.tensor_split,
288
+ vae_format=args.vae_format,
289
+ flow_shift=getattr(args, "flow_shift", None),
290
+ detail_faces=getattr(args, "detail_faces", False),
291
+ detailer_strength=getattr(args, "detailer_strength", 0.35),
292
+ detailer_steps=getattr(args, "detailer_steps", 2),
293
+ detailer_padding=getattr(args, "detailer_padding", 1.45),
294
+ strict_vulkan=args.strict_vulkan,
295
+ no_cache=getattr(args, "no_cache", False),
296
+ progress=getattr(args, "progress", False),
297
+ json_progress=getattr(args, "json_progress", False),
298
+ progress_file=getattr(args, "progress_file", None),
299
+ extra_args=unknown_args if unknown_args else None,
300
+ isolate_text_encoder=getattr(args, "isolate_text_encoder", False),
301
+ safe_mode=getattr(args, "safe_mode", False),
302
+ keep_apps_alive=getattr(args, "keep_apps_alive", False),
303
+ gpu_duty_cycle=getattr(args, "gpu_duty_cycle", None),
304
+ checkpoint_latent=getattr(args, "checkpoint_latent", False),
305
+ resume_latent=getattr(args, "resume_latent", None),
306
+ temporal_chunking=getattr(args, "temporal_chunking", None),
307
+ tile_size=getattr(args, "tile_size", None),
308
+ temp_tile=getattr(args, "temp_tile", None),
309
+ full_accel=getattr(args, "full_accel", False),
310
+ kill_threshold=getattr(args, "kill_threshold", None),
311
+ swap_allowance=getattr(args, "swap_allowance", "auto"),
312
+ safe_budget=getattr(args, "safe_budget", 0.95),
313
+ auto_provision=True
314
+ )
315
+ return 0 if res.path.exists() else 1
316
+ except FileNotFoundError as fnf_err:
317
+ print(f"[ERROR] [FILE_NOT_FOUND]: {fnf_err}", file=sys.stderr)
318
+ return ExitCode.CLI_ERROR
319
+ except ValueError as val_err:
320
+ print(f"[ERROR] [INVALID_ARGUMENT]: {val_err}", file=sys.stderr)
321
+ return ExitCode.CLI_ERROR
322
+ except RuntimeRequiredError:
323
+ raise
324
+ except TermuxDiffusionError as td_err:
325
+ print(f"[ERROR] [{td_err.code}]: {td_err}", file=sys.stderr)
326
+ return ExitCode.BUILD_ERROR
327
+
328
+ elif args.command == "install":
329
+ provision_engine(force=args.force, dedicate=getattr(args, "dedicate", False))
330
+ return 0
331
+
332
+ elif args.command == "doctor":
333
+ ok = run_doctor()
334
+ return 0 if ok else 1
335
+
336
+ elif args.command == "models":
337
+ print("\n--- [Presets] Available Presets ---")
338
+ for k, v in list_presets().items():
339
+ desc = v.get('description', f"Alias to {v.get('alias')}")
340
+ size = f" ({v.get('size_mb')}MB)" if v.get('size_mb') else ""
341
+ print(f" - {k:15} : {desc}{size}")
342
+ print("\n--- [Models] Locally Cached Models ---")
343
+ cached = list_cached_models()
344
+ if not cached:
345
+ print(" (No models cached yet. Run 'termux-diffusion download <model>' or generate)")
346
+ for m in cached:
347
+ print(f" - {m['name']:25} [{m['size_mb']} MB] -> {m['path']}")
348
+ print()
349
+ return 0
350
+
351
+ elif args.command == "download":
352
+ download_model(args.model)
353
+ return 0
354
+
355
+ elif args.command == "clear-cache":
356
+ removed = clear_cache()
357
+ print(f"[Clean] Removed {removed} cached model files.")
358
+ return 0
359
+
360
+ elif args.command == "workflow":
361
+ from .workflow import WorkflowExecutor, WorkflowParser, DAGEngine, WorkflowError
362
+ if getattr(args, "workflow_command", None) == "run":
363
+ executor = WorkflowExecutor()
364
+ try:
365
+ res = executor.run(
366
+ workflow=args.workflow_file,
367
+ output_dir=args.output_dir,
368
+ device=args.device,
369
+ force_run=args.force_run,
370
+ progress=args.progress,
371
+ json_progress=args.json_progress
372
+ )
373
+ print(f"[Workflow] Completed in {res.elapsed_seconds:.2f}s! Generated {len(res.output_images)} image(s):")
374
+ for img in res.output_images:
375
+ print(f" -> {img}")
376
+ return 0 if res.success else 1
377
+ except WorkflowError as wf_err:
378
+ print(f"[ERROR] [WORKFLOW_FAILED]: {wf_err}", file=sys.stderr)
379
+ return 1
380
+ except FileNotFoundError as fnf:
381
+ print(f"[ERROR] [FILE_NOT_FOUND]: {fnf}", file=sys.stderr)
382
+ return 1
383
+ elif getattr(args, "workflow_command", None) == "inspect":
384
+ try:
385
+ nodes = WorkflowParser.parse(args.workflow_file)
386
+ order = DAGEngine.topological_sort(nodes)
387
+ raw_json = WorkflowParser.load_json(args.workflow_file)
388
+ fmt = WorkflowParser.detect_format(raw_json)
389
+ print(f"\n--- [Workflow Inspector] ---")
390
+ print(f"File: {args.workflow_file}")
391
+ print(f"Detected Format: {fmt}")
392
+ print(f"Total Nodes: {len(nodes)}")
393
+ print(f"Execution Order ({len(order)} steps):")
394
+ for idx, nid in enumerate(order, 1):
395
+ n = nodes[nid]
396
+ title_str = f" ('{n.title}')" if n.title else ""
397
+ print(f" {idx}. [Node #{nid}] {n.class_type}{title_str}")
398
+ print()
399
+ return 0
400
+ except WorkflowError as wf_err:
401
+ print(f"[ERROR] [INSPECT_FAILED]: {wf_err}", file=sys.stderr)
402
+ return 1
403
+ except FileNotFoundError as fnf:
404
+ print(f"[ERROR] [FILE_NOT_FOUND]: {fnf}", file=sys.stderr)
405
+ return 1
406
+ else:
407
+ wf_parser.print_help()
408
+ return 0
409
+
410
+ elif args.command in ("web", "ui", "amfy", "amfyui"):
411
+ from .web import run_web_server
412
+ run_web_server(host=args.host, port=args.port, blocking=True)
413
+ return 0
414
+
415
+ elif args.command in ("component", "model", "instance"):
416
+ try:
417
+ from ameva_component.cli_support import dispatch_protocol
418
+ from termux_diffusion.control import DiffusionControl
419
+ dispatch_protocol(args, DiffusionControl())
420
+ except ImportError:
421
+ print("[ERROR] ameva-component-sdk not installed.", file=sys.stderr)
422
+ return 1
423
+ return 0
424
+
425
+ parser.print_help()
426
+ return 0
427
+
428
+
429
+ def run_install_cli(argv: Optional[List[str]] = None):
430
+ """Entry point for termux-diffusion-install with CLI mutex validation and mode control."""
431
+ if argv is None:
432
+ argv = sys.argv[1:]
433
+
434
+ parser = argparse.ArgumentParser(
435
+ prog="termux-diffusion-install",
436
+ description="Provision native ARM64 engine binaries with Prebuilt-First pipeline, self-test, and fallback."
437
+ )
438
+ parser.add_argument("--prebuilt", action="store_true", help="Install pre-compiled binary (prebuilt-only mode)")
439
+ parser.add_argument("--prebuilt-only", action="store_true", help="Install pre-compiled binary (do not fall back to source build on failure)")
440
+ parser.add_argument("--build-from-source", action="store_true", help="Skip prebuilt binary and compile from source")
441
+ parser.add_argument("-b", "--backend", type=str, default="cpu", choices=["auto", "gpu", "vulkan", "opencl", "cpu"], help="Target compute backend (default: cpu)")
442
+ parser.add_argument("-f", "--force", "--force-reinstall", dest="force", action="store_true", help="Force re-installation")
443
+ parser.add_argument("--print-diagnostics", action="store_true", help="Print system, ABI, CPU, and Vulkan diagnostic information and continue")
444
+ parser.add_argument("--diagnostics-only", action="store_true", help="Print system, ABI, CPU, and Vulkan diagnostic information and exit immediately")
445
+
446
+ args = parser.parse_args(argv)
447
+
448
+ # Mutually Exclusive Flag Check
449
+ is_prebuilt_flag = args.prebuilt or args.prebuilt_only
450
+ if is_prebuilt_flag and args.build_from_source:
451
+ print("[ERROR] E_CLI_EXCLUSIVE_MUTEX: --prebuilt / --prebuilt-only and --build-from-source are mutually exclusive options.", file=sys.stderr)
452
+ sys.exit(ExitCode.CLI_ERROR)
453
+
454
+ if is_prebuilt_flag:
455
+ install_mode = "prebuilt-only"
456
+ elif args.build_from_source:
457
+ install_mode = "source-only"
458
+ else:
459
+ install_mode = "prebuilt-first"
460
+
461
+ print(f"[termux-diffusion] Selected Install Mode: {install_mode}")
462
+
463
+ if getattr(args, "print_diagnostics", False) or getattr(args, "diagnostics_only", False):
464
+ print("\n=== [termux-diffusion] System Diagnostics ===")
465
+ from .hardware import detect_hardware_profile, format_hardware_report
466
+ hw = detect_hardware_profile()
467
+ print(format_hardware_report(hw))
468
+ print(f"Install Mode: {install_mode}")
469
+ print(f"Target Backend: {args.backend}")
470
+ print("=============================================\n")
471
+ if getattr(args, "diagnostics_only", False):
472
+ return ExitCode.SUCCESS
473
+
474
+ try:
475
+ provision_engine(force=args.force, backend=args.backend, install_mode=install_mode)
476
+ except TermuxDiffusionError as err:
477
+ print(f"[ERROR] [{err.code}]: {err}", file=sys.stderr)
478
+ if err.code == ErrorCode.MANIFEST_DOWNLOAD or "E_PREBUILT_FAILED" in str(err) or err.code == "E_PREBUILT_UNAVAILABLE":
479
+ sys.exit(ExitCode.INTEGRITY_ERROR)
480
+ elif err.code == ErrorCode.INSTALL_LOCKED:
481
+ sys.exit(ExitCode.CLI_ERROR)
482
+ else:
483
+ sys.exit(ExitCode.BUILD_ERROR)
484
+
485
+
486
+ def run_doctor_cli():
487
+ """Entry point for termux-diffusion-doctor."""
488
+ run_doctor()
489
+
490
+
491
+ if __name__ == "__main__":
492
+ sys.exit(main())