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.
- amv_diffusion-2.1.0.dist-info/METADATA +180 -0
- amv_diffusion-2.1.0.dist-info/RECORD +39 -0
- amv_diffusion-2.1.0.dist-info/WHEEL +5 -0
- amv_diffusion-2.1.0.dist-info/entry_points.txt +7 -0
- amv_diffusion-2.1.0.dist-info/licenses/LICENSE +21 -0
- amv_diffusion-2.1.0.dist-info/top_level.txt +1 -0
- termux_diffusion/__init__.py +185 -0
- termux_diffusion/__main__.py +7 -0
- termux_diffusion/_version.py +3 -0
- termux_diffusion/adapter.py +41 -0
- termux_diffusion/canvas.py +364 -0
- termux_diffusion/cli.py +492 -0
- termux_diffusion/cluster.py +340 -0
- termux_diffusion/control/__init__.py +5 -0
- termux_diffusion/control/component.py +309 -0
- termux_diffusion/control/status.py +15 -0
- termux_diffusion/core.py +2936 -0
- termux_diffusion/data/bootstrap-manifest.json +15 -0
- termux_diffusion/data/prompt_cache/108a7149c4e70550.json +10 -0
- termux_diffusion/data/release-public-keys.json +10 -0
- termux_diffusion/data/validated-vulkan-profiles.json +298 -0
- termux_diffusion/detailer.py +283 -0
- termux_diffusion/doctor.py +128 -0
- termux_diffusion/downloader.py +111 -0
- termux_diffusion/engine.py +226 -0
- termux_diffusion/exceptions.py +158 -0
- termux_diffusion/hardware.py +985 -0
- termux_diffusion/hub.py +686 -0
- termux_diffusion/installer.py +499 -0
- termux_diffusion/locking.py +134 -0
- termux_diffusion/manifest.py +128 -0
- termux_diffusion/npu.py +267 -0
- termux_diffusion/platform.py +533 -0
- termux_diffusion/purger.py +145 -0
- termux_diffusion/py.typed +1 -0
- termux_diffusion/safety.py +150 -0
- termux_diffusion/selftest.py +178 -0
- termux_diffusion/web.py +3370 -0
- termux_diffusion/workflow.py +1345 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""
|
|
4
|
+
termux_diffusion.doctor
|
|
5
|
+
=======================
|
|
6
|
+
Pre-flight diagnostics, hardware topology inspection, and system verification for termux-diffusion.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import shutil
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from .platform import (
|
|
17
|
+
is_android_termux,
|
|
18
|
+
is_arm64,
|
|
19
|
+
get_memory_info,
|
|
20
|
+
check_memory_safety,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("termux_diffusion.doctor")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_doctor() -> bool:
|
|
27
|
+
"""Run comprehensive 8-tier pre-flight diagnostic checks for Samsung Galaxy Termux setup."""
|
|
28
|
+
from .engine import get_binary_path
|
|
29
|
+
|
|
30
|
+
print("=" * 65)
|
|
31
|
+
print("[Doctor] [termux-diffusion] Pre-flight Diagnostic Doctor")
|
|
32
|
+
print("=" * 65)
|
|
33
|
+
|
|
34
|
+
all_passed = True
|
|
35
|
+
|
|
36
|
+
# 1. Platform Check
|
|
37
|
+
is_termux = is_android_termux()
|
|
38
|
+
arm_arch = is_arm64()
|
|
39
|
+
print(f"1. Platform: {'Android Termux [OK]' if is_termux else 'Non-Termux Host (Emulation Mode) [INFO]'}")
|
|
40
|
+
print(f"2. Architecture: {'ARM64 / aarch64 [OK]' if arm_arch else f'Host {sys.platform} ({os.name}) [INFO]'}")
|
|
41
|
+
|
|
42
|
+
# 3. Memory & RAM Plus
|
|
43
|
+
mem = get_memory_info()
|
|
44
|
+
safe, msg = check_memory_safety(required_mb=1200)
|
|
45
|
+
print(f"3. System Memory: {mem['mem_total_mb']}MB RAM + {mem['swap_total_mb']}MB Swap ({'Safe [OK]' if safe else 'Warning [WARN]'})")
|
|
46
|
+
if not safe:
|
|
47
|
+
print(f" -> {msg}")
|
|
48
|
+
|
|
49
|
+
# 4. Storage & Samsung Gallery
|
|
50
|
+
storage_ok = os.path.exists(os.path.expanduser("~/storage"))
|
|
51
|
+
print(f"4. Android Storage Permission: {'Configured [OK]' if storage_ok else 'Missing [WARN] (Run termux-setup-storage)'}")
|
|
52
|
+
|
|
53
|
+
# 5. Compiler Toolchain
|
|
54
|
+
clang_ok = bool(shutil.which("clang") or shutil.which("gcc") or shutil.which("clang++"))
|
|
55
|
+
cmake_ok = bool(shutil.which("cmake"))
|
|
56
|
+
git_ok = bool(shutil.which("git"))
|
|
57
|
+
print(f"5. Build Tools: clang ({'[OK]' if clang_ok else '[FAIL]'}), cmake ({'[OK]' if cmake_ok else '[FAIL]'}), git ({'[OK]' if git_ok else '[FAIL]'})")
|
|
58
|
+
if not (clang_ok and cmake_ok and git_ok) and is_termux:
|
|
59
|
+
all_passed = False
|
|
60
|
+
print(" -> Run: pkg install clang cmake git termux-api -y")
|
|
61
|
+
|
|
62
|
+
# 6. Engine Binary
|
|
63
|
+
engine = get_binary_path()
|
|
64
|
+
print(f"6. Native C++ Engine (sd-cli): {str(engine) + ' [OK]' if engine else 'Not Provisioned [FAIL] (Run termux-diffusion install)'}")
|
|
65
|
+
if not engine:
|
|
66
|
+
all_passed = False
|
|
67
|
+
|
|
68
|
+
# 7. Model Cache Status
|
|
69
|
+
from .hub import list_cached_models
|
|
70
|
+
cached = list_cached_models()
|
|
71
|
+
print(f"7. Cached GGUF Models: {len(cached)} model(s) available locally.")
|
|
72
|
+
for m in cached:
|
|
73
|
+
valid_tag = " [GGUF Valid [OK]]" if m.get("is_valid_gguf") else " [Header [WARN]]"
|
|
74
|
+
print(f" -> {m['name']} ({m['size_mb']} MB){valid_tag}")
|
|
75
|
+
|
|
76
|
+
# 8. Hardware Acceleration (GPU / NPU / TPU / Vulkan / OpenCL)
|
|
77
|
+
try:
|
|
78
|
+
from ameva_runtime.vulkan.adapters import find_system_vulkan_driver_dir
|
|
79
|
+
discovered_vulkan = find_system_vulkan_driver_dir()
|
|
80
|
+
except ImportError:
|
|
81
|
+
discovered_vulkan = None
|
|
82
|
+
|
|
83
|
+
from .hardware import detect_hardware_profile
|
|
84
|
+
hw = detect_hardware_profile()
|
|
85
|
+
print("8. Hardware Acceleration Profile:")
|
|
86
|
+
print(f" SoC: {hw.soc_name}, GPU Architecture: {hw.gpu_name}")
|
|
87
|
+
print(f" GPU Vulkan: {'Available [OK]' if (hw.vulkan_available or discovered_vulkan) else 'Not Found [WARN]'}")
|
|
88
|
+
if discovered_vulkan:
|
|
89
|
+
print(f" -> Vulkan Driver (ameva-runtime): {discovered_vulkan}")
|
|
90
|
+
elif hw.vulkan_driver:
|
|
91
|
+
print(f" -> Vulkan Driver: {hw.vulkan_driver.library_path}")
|
|
92
|
+
print(f" GPU OpenCL: {'Available [OK]' if hw.opencl_available else 'Not Found [WARN]'}")
|
|
93
|
+
if hw.opencl_driver:
|
|
94
|
+
print(f" -> OpenCL Driver: {hw.opencl_driver.library_path}")
|
|
95
|
+
if hw.npu_profile and hw.npu_profile.available:
|
|
96
|
+
print(" NPU / TPU Hardware: Detected [INFO]")
|
|
97
|
+
print(f" -> Architecture: {hw.npu_profile.dsp_architecture} ({hw.npu_profile.tops_rating} TOPS)")
|
|
98
|
+
print(f" -> Driver: {hw.npu_profile.driver_library}")
|
|
99
|
+
print(" -> Runtime: Native QNN C++ execution scheduled for v2.0")
|
|
100
|
+
else:
|
|
101
|
+
print(" NPU / TPU Hardware: Not Detected [INFO] (GPU Vulkan & CPU NEON Active)")
|
|
102
|
+
print(f" CPU ISA SIMD: DotProd={'[OK]' if hw.has_dotprod else '[FAIL]'} "
|
|
103
|
+
f"FP16={'[OK]' if hw.has_fp16 else '[FAIL]'} "
|
|
104
|
+
f"I8MM={'[OK]' if hw.has_i8mm else '[FAIL]'} "
|
|
105
|
+
f"SVE={'[OK]' if hw.has_sve else '[FAIL]'}")
|
|
106
|
+
print(f" Active Compute Pipeline: {hw.recommended_backend.value.upper()} "
|
|
107
|
+
f"(Offload Layers: {hw.recommended_ngl})")
|
|
108
|
+
|
|
109
|
+
# 9. Prompt Cache & Auxiliary Assets Status
|
|
110
|
+
from .platform import get_default_cache_dir
|
|
111
|
+
cache_dir = get_default_cache_dir() / "prompt_cache"
|
|
112
|
+
cache_count = len(list(cache_dir.glob("*"))) if cache_dir.is_dir() else 0
|
|
113
|
+
print(f"9. Auxiliary Assets & Prompt Cache: {cache_count} cached entries at {cache_dir} [OK]")
|
|
114
|
+
|
|
115
|
+
# 10. Android 12+ Background Stability Guard (Phantom Process Killer)
|
|
116
|
+
if is_termux:
|
|
117
|
+
print("10. Android 12+ Background Guard:")
|
|
118
|
+
print(" -> Tip: If generation crashes when Termux is in background, enable")
|
|
119
|
+
print(" 'Developer Options > Disable child process restrictions'")
|
|
120
|
+
print(" or run: adb shell \"/system/bin/device_config put activity_manager max_phantom_processes 2147483647\"")
|
|
121
|
+
|
|
122
|
+
print("=" * 65)
|
|
123
|
+
if all_passed:
|
|
124
|
+
print("All core diagnostics passed. System is ready for AI image generation.")
|
|
125
|
+
else:
|
|
126
|
+
print("Some diagnostics need attention. Run 'termux-diffusion install' to resolve.")
|
|
127
|
+
print("=" * 65)
|
|
128
|
+
return all_passed
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""Atomic download manager with SHA-256 integrity verification, concurrency lock, and rollback safety."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import time
|
|
8
|
+
import urllib.request
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .exceptions import DownloadError, InstallLockError, TermuxDiffusionError
|
|
14
|
+
from .locking import InstallLock
|
|
15
|
+
from .manifest import verify_file_sha256
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("termux_diffusion.downloader")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def atomic_download_file(
|
|
21
|
+
url: str,
|
|
22
|
+
dest_path: Path,
|
|
23
|
+
expected_sha256: Optional[str] = None,
|
|
24
|
+
expected_size: Optional[int] = None,
|
|
25
|
+
timeout_sec: float = 120.0,
|
|
26
|
+
user_agent: str = "termux-diffusion-downloader/1.2.0"
|
|
27
|
+
) -> Path:
|
|
28
|
+
"""Download url to dest_path atomically using .part -> SHA256 check -> atomic rename -> rollback."""
|
|
29
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
part_path = dest_path.with_suffix(dest_path.suffix + ".part")
|
|
31
|
+
bak_path = dest_path.with_suffix(dest_path.suffix + ".bak")
|
|
32
|
+
|
|
33
|
+
part_path.unlink(missing_ok=True)
|
|
34
|
+
bak_path.unlink(missing_ok=True)
|
|
35
|
+
|
|
36
|
+
print(f"[termux-diffusion] Downloading artifact from {url}...")
|
|
37
|
+
try:
|
|
38
|
+
req = urllib.request.Request(url, headers={"User-Agent": user_agent})
|
|
39
|
+
with urllib.request.urlopen(req, timeout=timeout_sec) as response:
|
|
40
|
+
if response.status != 200:
|
|
41
|
+
raise DownloadError(f"E_ARTIFACT_DOWNLOAD: HTTP server returned status code {response.status}")
|
|
42
|
+
|
|
43
|
+
with open(part_path, "wb") as out_f:
|
|
44
|
+
shutil.copyfileobj(response, out_f)
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
part_path.unlink(missing_ok=True)
|
|
47
|
+
raise DownloadError(f"E_ARTIFACT_DOWNLOAD: Network error downloading {url}: {exc}") from exc
|
|
48
|
+
|
|
49
|
+
# Size Verification
|
|
50
|
+
actual_size = part_path.stat().st_size
|
|
51
|
+
if expected_size is not None and expected_size > 0 and actual_size != expected_size:
|
|
52
|
+
part_path.unlink(missing_ok=True)
|
|
53
|
+
raise DownloadError(
|
|
54
|
+
f"E_ARTIFACT_SIZE: Downloaded artifact size mismatch. "
|
|
55
|
+
f"Expected {expected_size} bytes, got {actual_size} bytes."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# SHA-256 Verification
|
|
59
|
+
if expected_sha256 and not verify_file_sha256(part_path, expected_sha256):
|
|
60
|
+
part_path.unlink(missing_ok=True)
|
|
61
|
+
raise DownloadError(
|
|
62
|
+
f"E_ARTIFACT_SHA256: SHA-256 checksum verification failed for {dest_path.name}. "
|
|
63
|
+
f"Artifact hash mismatch!"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Atomic Rename with Backup Rollback Safety
|
|
67
|
+
try:
|
|
68
|
+
if dest_path.exists():
|
|
69
|
+
dest_path.rename(bak_path)
|
|
70
|
+
part_path.rename(dest_path)
|
|
71
|
+
os.chmod(dest_path, 0o755)
|
|
72
|
+
bak_path.unlink(missing_ok=True)
|
|
73
|
+
print(f"[termux-diffusion] Artifact {dest_path.name} downloaded and verified (SHA-256 match).")
|
|
74
|
+
return dest_path
|
|
75
|
+
except Exception as e:
|
|
76
|
+
# Rollback
|
|
77
|
+
part_path.unlink(missing_ok=True)
|
|
78
|
+
if bak_path.exists() and not dest_path.exists():
|
|
79
|
+
bak_path.rename(dest_path)
|
|
80
|
+
raise DownloadError(f"E_ATOMIC_RENAME_FAIL: Failed installing verified artifact {dest_path.name}: {e}") from e
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
from typing import List, Dict, Any, Union
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def list_models() -> List[Dict[str, Any]]:
|
|
87
|
+
"""List standard verified diffusion models from hub presets."""
|
|
88
|
+
from .hub import list_presets
|
|
89
|
+
presets = list_presets()
|
|
90
|
+
return [{"id": k, **v} for k, v in presets.items()]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def download_model(
|
|
94
|
+
model_name_or_url: str = "sdxs",
|
|
95
|
+
cache_dir: Optional[Union[str, Path]] = None,
|
|
96
|
+
force: bool = False,
|
|
97
|
+
**kwargs: Any,
|
|
98
|
+
) -> Path:
|
|
99
|
+
"""Download diffusion model weights using termux_diffusion.hub."""
|
|
100
|
+
from .hub import download_model as _hub_download
|
|
101
|
+
return _hub_download(model_name_or_url=model_name_or_url, cache_dir=cache_dir, force=force, **kwargs)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def resolve_model_path(
|
|
105
|
+
model_name_or_path: str = "sdxs",
|
|
106
|
+
cache_dir: Optional[Union[str, Path]] = None,
|
|
107
|
+
) -> Path:
|
|
108
|
+
"""Resolve diffusion model path to local Path."""
|
|
109
|
+
from .hub import resolve_model_path as _hub_resolve
|
|
110
|
+
return _hub_resolve(model_name_or_path=model_name_or_path, cache_dir=cache_dir)
|
|
111
|
+
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""
|
|
4
|
+
termux_diffusion.engine
|
|
5
|
+
=======================
|
|
6
|
+
High-performance C++ core engine locator, backend dispatcher, and high-level DiffusionEngine wrapper.
|
|
7
|
+
Strict Zero-Silent-Fallback and Anti-Deception compliant architecture matching AMEVA standards.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import shutil
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, List, Dict, Any, Union
|
|
16
|
+
|
|
17
|
+
from .platform import (
|
|
18
|
+
TERMUX_PREFIX,
|
|
19
|
+
is_android_termux,
|
|
20
|
+
get_default_cache_dir,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("termux_diffusion.engine")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_engine_bin_dir() -> Path:
|
|
27
|
+
"""Return canonical directory where termux-diffusion binaries reside ($PREFIX/bin SSOT)."""
|
|
28
|
+
prefix = os.environ.get("PREFIX") or TERMUX_PREFIX
|
|
29
|
+
if is_android_termux() or os.path.exists(prefix):
|
|
30
|
+
bin_dir = Path(prefix) / "bin"
|
|
31
|
+
else:
|
|
32
|
+
bin_dir = get_default_cache_dir() / "bin"
|
|
33
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
return bin_dir
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_engine_lib_dir() -> Path:
|
|
38
|
+
"""Return canonical directory where native shared libraries reside ($PREFIX/lib SSOT)."""
|
|
39
|
+
prefix = os.environ.get("PREFIX") or TERMUX_PREFIX
|
|
40
|
+
if is_android_termux() or os.path.exists(prefix):
|
|
41
|
+
lib_dir = Path(prefix) / "lib"
|
|
42
|
+
else:
|
|
43
|
+
lib_dir = get_default_cache_dir() / "lib"
|
|
44
|
+
lib_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
return lib_dir
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_binary_path(backend: str = "auto") -> Optional[Path]:
|
|
49
|
+
"""Find authoritative absolute path of sd-cli executable strictly isolated by compute backend.
|
|
50
|
+
|
|
51
|
+
Zero-Silent-Fallback Rules:
|
|
52
|
+
- 'gpu' / 'vulkan': Exclusively search for native Vulkan binary (sd-cli-vulkan).
|
|
53
|
+
Never fallback to CPU binary. Return None if unprovisioned.
|
|
54
|
+
- 'cpu' / default: Exclusively search for pure CPU NEON binary (sd-cli-cpu, sd-cli).
|
|
55
|
+
"""
|
|
56
|
+
home = Path(os.environ.get("HOME", os.path.expanduser("~")))
|
|
57
|
+
prefix_bin = Path(TERMUX_PREFIX) / "bin"
|
|
58
|
+
bin_dir = get_engine_bin_dir()
|
|
59
|
+
ameva_diff_dir = home / ".local" / "share" / "ameva" / "current" / "diffusion"
|
|
60
|
+
|
|
61
|
+
req_backend = str(backend or "auto").strip().lower()
|
|
62
|
+
|
|
63
|
+
# --------------------------------------------------------------------------
|
|
64
|
+
# 0. RPC / Distributed Multi-Node Route (sd-cli-rpc, sd-cli)
|
|
65
|
+
# --------------------------------------------------------------------------
|
|
66
|
+
if req_backend in ("rpc", "remote"):
|
|
67
|
+
rpc_candidates = [
|
|
68
|
+
ameva_diff_dir / "sd-cli-rpc",
|
|
69
|
+
home / ".local" / "share" / "ameva" / "rpc_llama" / "bin" / "sd-cli",
|
|
70
|
+
home / ".local" / "share" / "ameva" / "rpc_llama" / "bin" / "sd-cli-rpc",
|
|
71
|
+
bin_dir / "sd-cli-rpc",
|
|
72
|
+
prefix_bin / "sd-cli-rpc",
|
|
73
|
+
home / ".local" / "bin" / "sd-cli-rpc",
|
|
74
|
+
bin_dir / "sd-cli",
|
|
75
|
+
prefix_bin / "sd-cli",
|
|
76
|
+
]
|
|
77
|
+
for p in rpc_candidates:
|
|
78
|
+
if p.is_file() and (os.access(p, os.X_OK) or os.name == "nt"):
|
|
79
|
+
return p.resolve()
|
|
80
|
+
|
|
81
|
+
which_rpc = shutil.which("sd-cli-rpc")
|
|
82
|
+
if which_rpc and (os.access(which_rpc, os.X_OK) or os.name == "nt"):
|
|
83
|
+
return Path(which_rpc).resolve()
|
|
84
|
+
|
|
85
|
+
# --------------------------------------------------------------------------
|
|
86
|
+
# 1. GPU / Vulkan Route: Pure Vulkan GPU Binary (sd-cli-vulkan)
|
|
87
|
+
# --------------------------------------------------------------------------
|
|
88
|
+
if req_backend in ("vulkan", "gpu"):
|
|
89
|
+
vulkan_candidates = [
|
|
90
|
+
# AMEVA 2-Tier isolated current deployment
|
|
91
|
+
ameva_diff_dir / "sd-cli-vulkan",
|
|
92
|
+
# Termux prefix binary
|
|
93
|
+
prefix_bin / "sd-cli-vulkan",
|
|
94
|
+
bin_dir / "sd-cli-vulkan",
|
|
95
|
+
# User local binary
|
|
96
|
+
home / ".local" / "bin" / "sd-cli-vulkan",
|
|
97
|
+
]
|
|
98
|
+
for p in vulkan_candidates:
|
|
99
|
+
if p.is_file() and (os.access(p, os.X_OK) or os.name == "nt"):
|
|
100
|
+
return p.resolve()
|
|
101
|
+
|
|
102
|
+
# System PATH search strictly for 'sd-cli-vulkan'
|
|
103
|
+
which_vk = shutil.which("sd-cli-vulkan")
|
|
104
|
+
if which_vk and (os.access(which_vk, os.X_OK) or os.name == "nt"):
|
|
105
|
+
return Path(which_vk).resolve()
|
|
106
|
+
|
|
107
|
+
# Zero-Silent-Fallback: If Vulkan binary is absent, NEVER return CPU binary!
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------------
|
|
111
|
+
# 1.1 OpenCL Route: OpenCL GPU Binary (sd-cli with OpenCL)
|
|
112
|
+
# --------------------------------------------------------------------------
|
|
113
|
+
if req_backend in ("opencl", "cl"):
|
|
114
|
+
opencl_candidates = [
|
|
115
|
+
home / "sd-opencl-build" / "build" / "bin" / "sd-cli",
|
|
116
|
+
bin_dir / "sd-cli-opencl",
|
|
117
|
+
prefix_bin / "sd-cli-opencl",
|
|
118
|
+
prefix_bin / "sd-cli",
|
|
119
|
+
]
|
|
120
|
+
for p in opencl_candidates:
|
|
121
|
+
if p.is_file() and (os.access(p, os.X_OK) or os.name == "nt"):
|
|
122
|
+
return p.resolve()
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# --------------------------------------------------------------------------
|
|
126
|
+
# 2. CPU Baseline Route: Pure CPU NEON Binary (sd-cli-cpu, sd-cli)
|
|
127
|
+
# --------------------------------------------------------------------------
|
|
128
|
+
cpu_candidates = [
|
|
129
|
+
home / ".local" / "share" / "ameva" / "rpc_llama" / "bin" / "sd-cli",
|
|
130
|
+
bin_dir / "sd-cli-cpu",
|
|
131
|
+
prefix_bin / "sd-cli-cpu",
|
|
132
|
+
bin_dir / "sd-cli-source-cpu",
|
|
133
|
+
bin_dir / "sd-cli",
|
|
134
|
+
prefix_bin / "sd-cli",
|
|
135
|
+
home / ".local" / "bin" / "sd-cli-cpu",
|
|
136
|
+
home / ".local" / "bin" / "sd-cli",
|
|
137
|
+
]
|
|
138
|
+
for p in cpu_candidates:
|
|
139
|
+
if p.is_file() and (os.access(p, os.X_OK) or os.name == "nt"):
|
|
140
|
+
return p.resolve()
|
|
141
|
+
|
|
142
|
+
# System PATH search for CPU binary
|
|
143
|
+
for name in ("sd-cli", "sd-cli.exe", "sd", "sd.exe"):
|
|
144
|
+
which_path = shutil.which(name)
|
|
145
|
+
if which_path and (os.access(which_path, os.X_OK) or os.name == "nt"):
|
|
146
|
+
return Path(which_path).resolve()
|
|
147
|
+
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def locate_sd_cli(backend: Optional[str] = None) -> Optional[Path]:
|
|
152
|
+
"""Backward-compatible alias for get_binary_path."""
|
|
153
|
+
return get_binary_path(backend=backend or "auto")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class DiffusionEngine:
|
|
157
|
+
"""High-level Python SDK Engine for On-Device Stable Diffusion inference.
|
|
158
|
+
Follows canonical AMEVA component architecture matching LlamaRuntime and BitNetEngine.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
model: str = "realistic",
|
|
164
|
+
backend: str = "auto",
|
|
165
|
+
device: Optional[str] = None,
|
|
166
|
+
rpc: Optional[str] = None,
|
|
167
|
+
tensor_split: Optional[str] = None,
|
|
168
|
+
cluster_rpc_servers: Optional[Union[str, List[str]]] = None,
|
|
169
|
+
cluster_split_mode: Optional[str] = None,
|
|
170
|
+
cluster_tensor_split: Optional[str] = None,
|
|
171
|
+
cluster_vram_budget: Optional[str] = None,
|
|
172
|
+
**kwargs: Any,
|
|
173
|
+
):
|
|
174
|
+
self.model = model
|
|
175
|
+
self.device = device or backend or "auto"
|
|
176
|
+
self.requested_backend = self.device.strip().lower()
|
|
177
|
+
self.cluster_rpc_servers = cluster_rpc_servers or rpc
|
|
178
|
+
self.rpc = self.cluster_rpc_servers if isinstance(self.cluster_rpc_servers, str) else (",".join(self.cluster_rpc_servers) if self.cluster_rpc_servers else None)
|
|
179
|
+
self.cluster_tensor_split = cluster_tensor_split or tensor_split
|
|
180
|
+
self.tensor_split = self.cluster_tensor_split
|
|
181
|
+
self.cluster_split_mode = cluster_split_mode
|
|
182
|
+
self.cluster_vram_budget = cluster_vram_budget
|
|
183
|
+
self.bin_dir = get_engine_bin_dir()
|
|
184
|
+
self.lib_dir = get_engine_lib_dir()
|
|
185
|
+
self.kwargs = kwargs
|
|
186
|
+
|
|
187
|
+
def get_binary_path(self, backend: Optional[str] = None) -> Optional[Path]:
|
|
188
|
+
"""Resolve authoritative binary path for active or specified backend."""
|
|
189
|
+
target_backend = backend if backend is not None else self.requested_backend
|
|
190
|
+
return get_binary_path(backend=target_backend)
|
|
191
|
+
|
|
192
|
+
def generate(self, prompt: str, **kwargs: Any) -> Any:
|
|
193
|
+
"""Execute text-to-image generation delegating to core pipeline."""
|
|
194
|
+
from .core import generate
|
|
195
|
+
merged = {**self.kwargs, **kwargs}
|
|
196
|
+
merged.setdefault("device", self.requested_backend)
|
|
197
|
+
merged.setdefault("model", self.model)
|
|
198
|
+
if self.rpc and "rpc" not in merged:
|
|
199
|
+
merged["rpc"] = self.rpc
|
|
200
|
+
if self.cluster_rpc_servers and "cluster_rpc_servers" not in merged:
|
|
201
|
+
merged["cluster_rpc_servers"] = self.cluster_rpc_servers
|
|
202
|
+
if self.tensor_split and "tensor_split" not in merged:
|
|
203
|
+
merged["tensor_split"] = self.tensor_split
|
|
204
|
+
if self.cluster_split_mode and "cluster_split_mode" not in merged:
|
|
205
|
+
merged["cluster_split_mode"] = self.cluster_split_mode
|
|
206
|
+
if self.cluster_tensor_split and "cluster_tensor_split" not in merged:
|
|
207
|
+
merged["cluster_tensor_split"] = self.cluster_tensor_split
|
|
208
|
+
if self.cluster_vram_budget and "cluster_vram_budget" not in merged and "max_vram" not in merged:
|
|
209
|
+
merged["cluster_vram_budget"] = self.cluster_vram_budget
|
|
210
|
+
return generate(prompt=prompt, **merged)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def doctor() -> bool:
|
|
214
|
+
"""Run comprehensive pre-flight diagnostic checks."""
|
|
215
|
+
from .doctor import run_doctor
|
|
216
|
+
return run_doctor()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def load(model: str = "realistic", device: str = "auto", **kwargs: Any) -> DiffusionEngine:
|
|
220
|
+
"""Standard Unified Engine Factory for termux-diffusion matching AMEVA multi-modal standards."""
|
|
221
|
+
return DiffusionEngine(model=model, device=device, **kwargs)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Backward-compatible alias matching LlamaRuntime
|
|
225
|
+
DiffusionRuntime = DiffusionEngine
|
|
226
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""
|
|
4
|
+
AMEVA Unified Exception Hierarchy for Termux AI Engines.
|
|
5
|
+
Component: [DIFFUSION]
|
|
6
|
+
"""
|
|
7
|
+
from typing import Optional, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AmevaTermuxError(Exception):
|
|
11
|
+
"""Root exception for all Termux On-Device AI Engines."""
|
|
12
|
+
COMPONENT_TAG = "[DIFFUSION]"
|
|
13
|
+
DEFAULT_CODE = "E_UNKNOWN"
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, code: Optional[Any] = None, details: Optional[Any] = None):
|
|
16
|
+
self.code = code or self.DEFAULT_CODE
|
|
17
|
+
self.details = details
|
|
18
|
+
self.raw_message = message
|
|
19
|
+
super().__init__(message if message.startswith("[DIFFUSION]") else f"{self.COMPONENT_TAG} [{self.code}] {message}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
TermuxDiffusionError = AmevaTermuxError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ErrorCode:
|
|
26
|
+
CLI_EXCLUSIVE = "E_CLI_EXCLUSIVE_MUTEX"
|
|
27
|
+
PLATFORM_UNSUPPORTED = "E_PLATFORM_UNSUPPORTED"
|
|
28
|
+
MANIFEST_DOWNLOAD = "E_MANIFEST_DOWNLOAD"
|
|
29
|
+
MANIFEST_SIGNATURE = "E_MANIFEST_SIGNATURE"
|
|
30
|
+
MANIFEST_SCHEMA = "E_MANIFEST_SCHEMA"
|
|
31
|
+
MANIFEST_EXPIRED = "E_MANIFEST_EXPIRED"
|
|
32
|
+
ARTIFACT_DOWNLOAD = "E_ARTIFACT_DOWNLOAD"
|
|
33
|
+
ARTIFACT_SIZE = "E_ARTIFACT_SIZE"
|
|
34
|
+
ARTIFACT_SHA256 = "E_ARTIFACT_SHA256"
|
|
35
|
+
ARTIFACT_FORMAT = "E_ARTIFACT_FORMAT"
|
|
36
|
+
DYNAMIC_LINKER = "E_DYNAMIC_LINKER"
|
|
37
|
+
ILLEGAL_INSTRUCTION = "E_ILLEGAL_INSTRUCTION"
|
|
38
|
+
PROCESS_TIMEOUT = "E_PROCESS_TIMEOUT"
|
|
39
|
+
VULKAN_LOADER = "E_VULKAN_LOADER"
|
|
40
|
+
VULKAN_DEVICE = "E_VULKAN_DEVICE"
|
|
41
|
+
VULKAN_PIPELINE = "E_VULKAN_PIPELINE"
|
|
42
|
+
CPU_SELFTEST = "E_CPU_SELFTEST"
|
|
43
|
+
SOURCE_CLONE = "E_SOURCE_CLONE"
|
|
44
|
+
SOURCE_PATCH = "E_SOURCE_PATCH"
|
|
45
|
+
SOURCE_CONFIGURE = "E_SOURCE_CONFIGURE"
|
|
46
|
+
SOURCE_BUILD = "E_SOURCE_BUILD"
|
|
47
|
+
SOURCE_ARTIFACT = "E_SOURCE_ARTIFACT"
|
|
48
|
+
INSTALL_LOCKED = "E_INSTALL_LOCKED"
|
|
49
|
+
ROLLBACK_FAILED = "E_ROLLBACK_FAILED"
|
|
50
|
+
CLUSTER_CONNECTION = "E_CLUSTER_CONNECTION"
|
|
51
|
+
CLUSTER_CONFIG = "E_CLUSTER_CONFIG"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ExitCode:
|
|
55
|
+
SUCCESS = 0
|
|
56
|
+
CLI_ERROR = 2
|
|
57
|
+
PLATFORM_ERROR = 10
|
|
58
|
+
INTEGRITY_ERROR = 20
|
|
59
|
+
EXECUTION_ERROR = 30
|
|
60
|
+
SELFTEST_ERROR = 40
|
|
61
|
+
RUNTIME_REQUIRED = 69
|
|
62
|
+
BUILD_ERROR = 50
|
|
63
|
+
ROLLBACK_ERROR = 60
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PlatformNotSupportedError(TermuxDiffusionError):
|
|
67
|
+
def __init__(self, message: str):
|
|
68
|
+
super().__init__(message, code=ErrorCode.PLATFORM_UNSUPPORTED)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RuntimeRequiredError(PlatformNotSupportedError):
|
|
72
|
+
"""A GPU request needs ameva-runtime (AMUDA), which is missing or too old.
|
|
73
|
+
|
|
74
|
+
The message is the shared AMU_ERR_RUNTIME_REQUIRED explanation. The CLI exits with
|
|
75
|
+
ExitCode.RUNTIME_REQUIRED; nothing is run and there is no CPU fallback.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
runtime_required = True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ModelNotFoundError(TermuxDiffusionError):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ModelDownloadError(TermuxDiffusionError):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DownloadError(TermuxDiffusionError):
|
|
90
|
+
def __init__(self, message: str, code: str = ErrorCode.ARTIFACT_DOWNLOAD):
|
|
91
|
+
super().__init__(message, code=code)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ProvisioningError(TermuxDiffusionError):
|
|
95
|
+
def __init__(self, message: str, code: str = ErrorCode.SOURCE_BUILD):
|
|
96
|
+
super().__init__(message, code=code)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class InstallLockError(TermuxDiffusionError):
|
|
100
|
+
def __init__(self, message: str):
|
|
101
|
+
super().__init__(message, code=ErrorCode.INSTALL_LOCKED)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class OOMRiskError(TermuxDiffusionError):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class InferenceTimeoutError(TermuxDiffusionError):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class HardwareCompatibilityError(TermuxDiffusionError):
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RuntimeNotFoundError(TermuxDiffusionError):
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class InferenceExecutionError(TermuxDiffusionError):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ModelCorruptedError(TermuxDiffusionError):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ClusterConnectionError(TermuxDiffusionError):
|
|
129
|
+
"""Raised when one or more distributed RPC worker nodes cannot be reached or fail handshake."""
|
|
130
|
+
def __init__(self, message: str):
|
|
131
|
+
super().__init__(message, code=ErrorCode.CLUSTER_CONNECTION)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ClusterConfigurationError(TermuxDiffusionError):
|
|
135
|
+
"""Raised when cluster parameters (tensor split, backend, vram budget) are invalid."""
|
|
136
|
+
def __init__(self, message: str):
|
|
137
|
+
super().__init__(message, code=ErrorCode.CLUSTER_CONFIG)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ClusterLicenseRequiredError(TermuxDiffusionError):
|
|
141
|
+
"""Raised when distributed clustering is invoked without required AMEVA Cluster license."""
|
|
142
|
+
def __init__(self, message: str = ""):
|
|
143
|
+
if not message:
|
|
144
|
+
message = (
|
|
145
|
+
"\n================================================================================\n"
|
|
146
|
+
"[AMEVA-CLUSTER] CLUSTER LICENSE REQUIRED (E403)\n"
|
|
147
|
+
"================================================================================\n"
|
|
148
|
+
"Multi-device distributed clustering is an exclusive capability of 'ameva-cluster'.\n"
|
|
149
|
+
"Standalone distributed execution without the official AMEVA-Cluster package is prohibited.\n\n"
|
|
150
|
+
"Resolution:\n"
|
|
151
|
+
" 1. Install official AMEVA-Cluster runtime:\n"
|
|
152
|
+
" pip install ameva-cluster (or npm install @ameva/cluster)\n"
|
|
153
|
+
" 2. Launch worker/master through 'ameva-cluster' control plane:\n"
|
|
154
|
+
" ameva-cluster worker\n"
|
|
155
|
+
" ameva-cluster master -m model.gguf\n"
|
|
156
|
+
"================================================================================"
|
|
157
|
+
)
|
|
158
|
+
super().__init__(message, code=ErrorCode.CLUSTER_CONFIG)
|