termux-diffusion 1.6.8__tar.gz → 1.7.0__tar.gz

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 (48) hide show
  1. {termux_diffusion-1.6.8/termux_diffusion.egg-info → termux_diffusion-1.7.0}/PKG-INFO +1 -1
  2. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/pyproject.toml +1 -1
  3. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/setup.py +1 -1
  4. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/__init__.py +13 -18
  5. termux_diffusion-1.7.0/termux_diffusion/_version.py +1 -0
  6. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/cli.py +3 -1
  7. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/core.py +11 -2
  8. termux_diffusion-1.7.0/termux_diffusion/doctor.py +120 -0
  9. termux_diffusion-1.7.0/termux_diffusion/engine.py +162 -0
  10. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/hub.py +47 -0
  11. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/installer.py +16 -153
  12. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/platform.py +29 -7
  13. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/selftest.py +1 -1
  14. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0/termux_diffusion.egg-info}/PKG-INFO +1 -1
  15. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion.egg-info/SOURCES.txt +4 -0
  16. termux_diffusion-1.7.0/tests/test_doctor.py +13 -0
  17. termux_diffusion-1.7.0/tests/test_engine.py +98 -0
  18. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/tests/test_hardware.py +19 -0
  19. termux_diffusion-1.6.8/termux_diffusion/_version.py +0 -1
  20. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/LICENSE +0 -0
  21. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/README.md +0 -0
  22. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/README.pypi.md +0 -0
  23. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/setup.cfg +0 -0
  24. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/__main__.py +0 -0
  25. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/adapter.py +0 -0
  26. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/control/__init__.py +0 -0
  27. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/control/component.py +0 -0
  28. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/control/status.py +0 -0
  29. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/data/bootstrap-manifest.json +0 -0
  30. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/data/release-public-keys.json +0 -0
  31. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/data/validated-vulkan-profiles.json +0 -0
  32. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/downloader.py +0 -0
  33. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/exceptions.py +0 -0
  34. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/hardware.py +0 -0
  35. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/locking.py +0 -0
  36. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/manifest.py +0 -0
  37. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/npu.py +0 -0
  38. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion/py.typed +0 -0
  39. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion.egg-info/dependency_links.txt +0 -0
  40. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion.egg-info/entry_points.txt +0 -0
  41. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion.egg-info/requires.txt +0 -0
  42. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/termux_diffusion.egg-info/top_level.txt +0 -0
  43. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/tests/test_core.py +0 -0
  44. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/tests/test_hub.py +0 -0
  45. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/tests/test_installer.py +0 -0
  46. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/tests/test_npu.py +0 -0
  47. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/tests/test_platform.py +0 -0
  48. {termux_diffusion-1.6.8 → termux_diffusion-1.7.0}/tests/test_presets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: termux-diffusion
3
- Version: 1.6.8
3
+ Version: 1.7.0
4
4
  Summary: Pure CPU On-Device Stable Diffusion Runtime for Android Termux (ARM64 Bionic NEON SIMD)
5
5
  Home-page: https://github.com/uno-km/termux-diffusion
6
6
  Author: uno-km (AMEVA Foundation)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "termux-diffusion"
7
- version = "1.6.8"
7
+ version = "1.7.0"
8
8
  description = "Pure CPU On-Device Stable Diffusion Runtime for Android Termux (ARM64 Bionic NEON SIMD)"
9
9
  readme = "README.pypi.md"
10
10
  requires-python = ">=3.8"
@@ -11,7 +11,7 @@ if os.path.exists(readme_path):
11
11
 
12
12
  setup(
13
13
  name="termux-diffusion",
14
- version="1.5.2",
14
+ version="1.6.9",
15
15
  description="On-device Stable Diffusion runtime utilizing device resources for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)",
16
16
  long_description=long_description,
17
17
  long_description_content_type="text/markdown",
@@ -31,7 +31,15 @@ from .hub import (
31
31
  resolve_model_path,
32
32
  set_cache_dir,
33
33
  )
34
- from .installer import locate_sd_cli, provision_engine, run_doctor
34
+ from .engine import (
35
+ DiffusionEngine,
36
+ DiffusionRuntime,
37
+ get_binary_path,
38
+ locate_sd_cli,
39
+ load,
40
+ )
41
+ from .doctor import run_doctor
42
+ from .installer import provision_engine
35
43
  from .platform import (
36
44
  TermuxWakeLock,
37
45
  check_memory_safety,
@@ -78,6 +86,10 @@ __all__ = [
78
86
  "clear_cache",
79
87
  "list_presets",
80
88
  "DEFAULT_PRESETS",
89
+ "DiffusionEngine",
90
+ "DiffusionRuntime",
91
+ "load",
92
+ "get_binary_path",
81
93
  "locate_sd_cli",
82
94
  "provision_engine",
83
95
  "run_doctor",
@@ -103,20 +115,3 @@ __all__ = [
103
115
  "InferenceTimeoutError",
104
116
  ]
105
117
 
106
-
107
-
108
- class DiffusionEngine:
109
- """Standardized Engine wrapper for termux-diffusion."""
110
- def __init__(self, model: str = "default", device: str = "auto", **kwargs):
111
- self.model = model
112
- self.device = device
113
- self.kwargs = kwargs
114
-
115
- def generate(self, prompt: str, **kwargs):
116
- merged = {**self.kwargs, **kwargs}
117
- return generate(prompt, model=self.model, device=self.device, **merged)
118
-
119
-
120
- def load(model: str = "default", device: str = "auto", **kwargs) -> DiffusionEngine:
121
- """Standard Unified Engine Factory for termux-diffusion."""
122
- return DiffusionEngine(model=model, device=device, **kwargs)
@@ -0,0 +1 @@
1
+ __version__ = "1.7.0"
@@ -20,7 +20,9 @@ if hasattr(sys.stderr, "reconfigure"):
20
20
  from .core import generate
21
21
  from .hub import clear_cache, download_model, list_cached_models, list_presets
22
22
  from .exceptions import TermuxDiffusionError, ErrorCode, ExitCode
23
- from .installer import provision_engine, run_doctor
23
+ from .engine import DiffusionEngine, get_binary_path
24
+ from .doctor import run_doctor
25
+ from .installer import provision_engine
24
26
 
25
27
 
26
28
  def main(argv: Optional[List[str]] = None) -> int:
@@ -14,7 +14,8 @@ from typing import Optional, Union
14
14
 
15
15
  from .exceptions import InferenceTimeoutError, OOMRiskError, ProvisioningError, TermuxDiffusionError
16
16
  from .hub import DEFAULT_PRESETS, list_presets, resolve_model_path
17
- from .installer import locate_sd_cli, provision_engine
17
+ from .engine import get_binary_path, locate_sd_cli
18
+ from .installer import provision_engine
18
19
  from .platform import (
19
20
  TermuxWakeLock,
20
21
  check_memory_safety,
@@ -486,7 +487,7 @@ def generate(
486
487
 
487
488
  # 5.1 Configure Environment with companion library search paths (Termux native isolation)
488
489
  from .platform import get_clean_execution_env
489
- env = get_clean_execution_env()
490
+ env = get_clean_execution_env(backend=effective_device)
490
491
  # Bypass debug CPU shadow checks on mobile Vulkan to unlock 10x throughput
491
492
  env.setdefault("GGML_VULKAN_SKIP_CHECKS", "999999999")
492
493
 
@@ -513,6 +514,14 @@ def generate(
513
514
 
514
515
  # Auto-inject mobile Vulkan HAL shim (Samsung GOS & BDA sanitizer) into process environment
515
516
  env = DiffusionAdapter.get_execution_env(env)
517
+ # Re-sanitize LD_LIBRARY_PATH to eliminate Termux /usr/lib symbol collisions on Android Bionic
518
+ cur_ld = env.get("LD_LIBRARY_PATH", "")
519
+ prefix_str = str(Path(os.environ.get("PREFIX", "/data/data/com.termux/files/usr")) / "lib")
520
+ parts = [p for p in cur_ld.split(":") if p and not p.startswith(prefix_str)]
521
+ if parts:
522
+ env["LD_LIBRARY_PATH"] = ":".join(parts)
523
+ else:
524
+ env.pop("LD_LIBRARY_PATH", None)
516
525
 
517
526
  logger.info(
518
527
  "[termux-diffusion] DiffusionAdapter bound: backend=%s, status=%s, LD_PRELOAD=%s",
@@ -0,0 +1,120 @@
1
+ """
2
+ termux_diffusion.doctor
3
+ =======================
4
+ Pre-flight diagnostics, hardware topology inspection, and system verification for termux-diffusion.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import shutil
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from .platform import (
15
+ is_android_termux,
16
+ is_arm64,
17
+ get_memory_info,
18
+ check_memory_safety,
19
+ )
20
+
21
+ logger = logging.getLogger("termux_diffusion.doctor")
22
+
23
+
24
+ def run_doctor() -> bool:
25
+ """Run comprehensive 8-tier pre-flight diagnostic checks for Samsung Galaxy Termux setup."""
26
+ from .engine import get_binary_path
27
+
28
+ print("=" * 65)
29
+ print("[Doctor] [termux-diffusion] Pre-flight Diagnostic Doctor")
30
+ print("=" * 65)
31
+
32
+ all_passed = True
33
+
34
+ # 1. Platform Check
35
+ is_termux = is_android_termux()
36
+ arm_arch = is_arm64()
37
+ print(f"1. Platform: {'Android Termux [OK]' if is_termux else 'Non-Termux Host (Emulation Mode) [INFO]'}")
38
+ print(f"2. Architecture: {'ARM64 / aarch64 [OK]' if arm_arch else f'Host {sys.platform} ({os.name}) [INFO]'}")
39
+
40
+ # 3. Memory & RAM Plus
41
+ mem = get_memory_info()
42
+ safe, msg = check_memory_safety(required_mb=1200)
43
+ print(f"3. System Memory: {mem['mem_total_mb']}MB RAM + {mem['swap_total_mb']}MB Swap ({'Safe [OK]' if safe else 'Warning [WARN]'})")
44
+ if not safe:
45
+ print(f" -> {msg}")
46
+
47
+ # 4. Storage & Samsung Gallery
48
+ storage_ok = os.path.exists(os.path.expanduser("~/storage"))
49
+ print(f"4. Android Storage Permission: {'Configured [OK]' if storage_ok else 'Missing [WARN] (Run termux-setup-storage)'}")
50
+
51
+ # 5. Compiler Toolchain
52
+ clang_ok = bool(shutil.which("clang") or shutil.which("gcc") or shutil.which("clang++"))
53
+ cmake_ok = bool(shutil.which("cmake"))
54
+ git_ok = bool(shutil.which("git"))
55
+ 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]'})")
56
+ if not (clang_ok and cmake_ok and git_ok) and is_termux:
57
+ all_passed = False
58
+ print(" -> Run: pkg install clang cmake git termux-api -y")
59
+
60
+ # 6. Engine Binary
61
+ engine = get_binary_path()
62
+ print(f"6. Native C++ Engine (sd-cli): {str(engine) + ' [OK]' if engine else 'Not Provisioned [FAIL] (Run termux-diffusion install)'}")
63
+ if not engine:
64
+ all_passed = False
65
+
66
+ # 7. Model Cache Status
67
+ from .hub import list_cached_models
68
+ cached = list_cached_models()
69
+ print(f"7. Cached GGUF Models: {len(cached)} model(s) available locally.")
70
+ for m in cached:
71
+ valid_tag = " [GGUF Valid [OK]]" if m.get("is_valid_gguf") else " [Header [WARN]]"
72
+ print(f" -> {m['name']} ({m['size_mb']} MB){valid_tag}")
73
+
74
+ # 8. Hardware Acceleration (GPU / NPU / TPU / Vulkan / OpenCL)
75
+ try:
76
+ from ameva_runtime.vulkan.adapters import find_system_vulkan_driver_dir
77
+ discovered_vulkan = find_system_vulkan_driver_dir()
78
+ except ImportError:
79
+ discovered_vulkan = None
80
+
81
+ from .hardware import detect_hardware_profile
82
+ hw = detect_hardware_profile()
83
+ print("8. Hardware Acceleration Profile:")
84
+ print(f" SoC: {hw.soc_name}, GPU Architecture: {hw.gpu_name}")
85
+ print(f" GPU Vulkan: {'Available [OK]' if (hw.vulkan_available or discovered_vulkan) else 'Not Found [WARN]'}")
86
+ if discovered_vulkan:
87
+ print(f" -> Vulkan Driver (ameva-runtime): {discovered_vulkan}")
88
+ elif hw.vulkan_driver:
89
+ print(f" -> Vulkan Driver: {hw.vulkan_driver.library_path}")
90
+ print(f" GPU OpenCL: {'Available [OK]' if hw.opencl_available else 'Not Found [WARN]'}")
91
+ if hw.opencl_driver:
92
+ print(f" -> OpenCL Driver: {hw.opencl_driver.library_path}")
93
+ if hw.npu_profile and hw.npu_profile.available:
94
+ print(" NPU / TPU Hardware: Detected [INFO]")
95
+ print(f" -> Architecture: {hw.npu_profile.dsp_architecture} ({hw.npu_profile.tops_rating} TOPS)")
96
+ print(f" -> Driver: {hw.npu_profile.driver_library}")
97
+ print(" -> Runtime: Native QNN C++ execution scheduled for v2.0")
98
+ else:
99
+ print(" NPU / TPU Hardware: Not Detected [INFO] (GPU Vulkan & CPU NEON Active)")
100
+ print(f" CPU ISA SIMD: DotProd={'[OK]' if hw.has_dotprod else '[FAIL]'} "
101
+ f"FP16={'[OK]' if hw.has_fp16 else '[FAIL]'} "
102
+ f"I8MM={'[OK]' if hw.has_i8mm else '[FAIL]'} "
103
+ f"SVE={'[OK]' if hw.has_sve else '[FAIL]'}")
104
+ print(f" Active Compute Pipeline: {hw.recommended_backend.value.upper()} "
105
+ f"(Offload Layers: {hw.recommended_ngl})")
106
+
107
+ # 9. Android 12+ Background Stability Guard (Phantom Process Killer)
108
+ if is_termux:
109
+ print("9. Android 12+ Background Guard:")
110
+ print(" -> Tip: If generation crashes when Termux is in background, enable")
111
+ print(" 'Developer Options > Disable child process restrictions'")
112
+ print(" or run: adb shell \"/system/bin/device_config put activity_manager max_phantom_processes 2147483647\"")
113
+
114
+ print("=" * 65)
115
+ if all_passed:
116
+ print("All core diagnostics passed. System is ready for AI image generation.")
117
+ else:
118
+ print("Some diagnostics need attention. Run 'termux-diffusion install' to resolve.")
119
+ print("=" * 65)
120
+ return all_passed
@@ -0,0 +1,162 @@
1
+ """
2
+ termux_diffusion.engine
3
+ =======================
4
+ High-performance C++ core engine locator, backend dispatcher, and high-level DiffusionEngine wrapper.
5
+ Strict Zero-Silent-Fallback and Anti-Deception compliant architecture matching AMEVA standards.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import shutil
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Optional, List, Dict, Any
14
+
15
+ from .platform import (
16
+ TERMUX_PREFIX,
17
+ is_android_termux,
18
+ get_default_cache_dir,
19
+ )
20
+
21
+ logger = logging.getLogger("termux_diffusion.engine")
22
+
23
+
24
+ def get_engine_bin_dir() -> Path:
25
+ """Return canonical directory where termux-diffusion binaries reside ($PREFIX/bin SSOT)."""
26
+ prefix = os.environ.get("PREFIX") or TERMUX_PREFIX
27
+ if is_android_termux() or os.path.exists(prefix):
28
+ bin_dir = Path(prefix) / "bin"
29
+ else:
30
+ bin_dir = get_default_cache_dir() / "bin"
31
+ bin_dir.mkdir(parents=True, exist_ok=True)
32
+ return bin_dir
33
+
34
+
35
+ def get_engine_lib_dir() -> Path:
36
+ """Return canonical directory where native shared libraries reside ($PREFIX/lib SSOT)."""
37
+ prefix = os.environ.get("PREFIX") or TERMUX_PREFIX
38
+ if is_android_termux() or os.path.exists(prefix):
39
+ lib_dir = Path(prefix) / "lib"
40
+ else:
41
+ lib_dir = get_default_cache_dir() / "lib"
42
+ lib_dir.mkdir(parents=True, exist_ok=True)
43
+ return lib_dir
44
+
45
+
46
+ def get_binary_path(backend: str = "auto") -> Optional[Path]:
47
+ """Find authoritative absolute path of sd-cli executable strictly isolated by compute backend.
48
+
49
+ Zero-Silent-Fallback Rules:
50
+ - 'gpu' / 'vulkan': Exclusively search for native Vulkan binary (sd-cli-vulkan).
51
+ Never fallback to CPU binary. Return None if unprovisioned.
52
+ - 'cpu' / default: Exclusively search for pure CPU NEON binary (sd-cli-cpu, sd-cli).
53
+ """
54
+ home = Path(os.environ.get("HOME", os.path.expanduser("~")))
55
+ prefix_bin = Path(TERMUX_PREFIX) / "bin"
56
+ bin_dir = get_engine_bin_dir()
57
+ ameva_diff_dir = home / ".local" / "share" / "ameva" / "current" / "diffusion"
58
+
59
+ req_backend = str(backend or "auto").strip().lower()
60
+
61
+ # --------------------------------------------------------------------------
62
+ # 1. GPU / Vulkan Route: Pure Vulkan GPU Binary (sd-cli-vulkan)
63
+ # --------------------------------------------------------------------------
64
+ if req_backend in ("vulkan", "gpu"):
65
+ vulkan_candidates = [
66
+ # AMEVA 2-Tier isolated current deployment
67
+ ameva_diff_dir / "sd-cli-vulkan",
68
+ # Termux prefix binary
69
+ prefix_bin / "sd-cli-vulkan",
70
+ bin_dir / "sd-cli-vulkan",
71
+ # User local binary
72
+ home / ".local" / "bin" / "sd-cli-vulkan",
73
+ ]
74
+ for p in vulkan_candidates:
75
+ if p.is_file() and (os.access(p, os.X_OK) or os.name == "nt"):
76
+ return p.resolve()
77
+
78
+ # System PATH search strictly for 'sd-cli-vulkan'
79
+ which_vk = shutil.which("sd-cli-vulkan")
80
+ if which_vk and (os.access(which_vk, os.X_OK) or os.name == "nt"):
81
+ return Path(which_vk).resolve()
82
+
83
+ # Zero-Silent-Fallback: If Vulkan binary is absent, NEVER return CPU binary!
84
+ return None
85
+
86
+ # --------------------------------------------------------------------------
87
+ # 2. CPU Baseline Route: Pure CPU NEON Binary (sd-cli-cpu, sd-cli)
88
+ # --------------------------------------------------------------------------
89
+ cpu_candidates = [
90
+ bin_dir / "sd-cli-cpu",
91
+ prefix_bin / "sd-cli-cpu",
92
+ bin_dir / "sd-cli-source-cpu",
93
+ bin_dir / "sd-cli",
94
+ prefix_bin / "sd-cli",
95
+ home / ".local" / "bin" / "sd-cli-cpu",
96
+ home / ".local" / "bin" / "sd-cli",
97
+ ]
98
+ for p in cpu_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 for CPU binary
103
+ for name in ("sd-cli", "sd-cli.exe", "sd", "sd.exe"):
104
+ which_path = shutil.which(name)
105
+ if which_path and (os.access(which_path, os.X_OK) or os.name == "nt"):
106
+ return Path(which_path).resolve()
107
+
108
+ return None
109
+
110
+
111
+ def locate_sd_cli(backend: Optional[str] = None) -> Optional[Path]:
112
+ """Backward-compatible alias for get_binary_path."""
113
+ return get_binary_path(backend=backend or "auto")
114
+
115
+
116
+ class DiffusionEngine:
117
+ """High-level Python SDK Engine for On-Device Stable Diffusion inference.
118
+ Follows canonical AMEVA component architecture matching LlamaRuntime and BitNetEngine.
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ model: str = "realistic",
124
+ backend: str = "auto",
125
+ device: Optional[str] = None,
126
+ **kwargs: Any,
127
+ ):
128
+ self.model = model
129
+ self.device = device or backend or "auto"
130
+ self.requested_backend = self.device.strip().lower()
131
+ self.bin_dir = get_engine_bin_dir()
132
+ self.lib_dir = get_engine_lib_dir()
133
+ self.kwargs = kwargs
134
+
135
+ def get_binary_path(self, backend: Optional[str] = None) -> Optional[Path]:
136
+ """Resolve authoritative binary path for active or specified backend."""
137
+ target_backend = backend if backend is not None else self.requested_backend
138
+ return get_binary_path(backend=target_backend)
139
+
140
+ def generate(self, prompt: str, **kwargs: Any) -> Any:
141
+ """Execute text-to-image generation delegating to core pipeline."""
142
+ from .core import generate
143
+ merged = {**self.kwargs, **kwargs}
144
+ merged.setdefault("device", self.requested_backend)
145
+ merged.setdefault("model", self.model)
146
+ return generate(prompt=prompt, **merged)
147
+
148
+ @staticmethod
149
+ def doctor() -> bool:
150
+ """Run comprehensive pre-flight diagnostic checks."""
151
+ from .doctor import run_doctor
152
+ return run_doctor()
153
+
154
+
155
+ def load(model: str = "realistic", device: str = "auto", **kwargs: Any) -> DiffusionEngine:
156
+ """Standard Unified Engine Factory for termux-diffusion matching AMEVA multi-modal standards."""
157
+ return DiffusionEngine(model=model, device=device, **kwargs)
158
+
159
+
160
+ # Backward-compatible alias matching LlamaRuntime
161
+ DiffusionRuntime = DiffusionEngine
162
+
@@ -114,6 +114,53 @@ DEFAULT_PRESETS = MappingProxyType({
114
114
  "default_steps": 20,
115
115
  "default_cfg": 6.0,
116
116
  },
117
+ "counterfeit-v3-lcm": {
118
+ "repo_id": "mzwing/Anything-LCM-GGUF",
119
+ "filename": "anything_v30-LCM.q4_0.gguf",
120
+ "alias": "counterfeit-v3-lcm.gguf",
121
+ "description": "Counterfeit / Anime LCM (Q4_0) - High-quality 4~6 step Anime & Illustration generation",
122
+ "size_mb": 1566,
123
+ "default_steps": 6,
124
+ "default_cfg": 1.5,
125
+ "default_sampler": "lcm",
126
+ },
127
+ "anything-v5-lcm": {
128
+ "repo_id": "mzwing/Anything-LCM-GGUF",
129
+ "filename": "anything_v30-LCM.q4_0.gguf",
130
+ "alias": "anything-v5-lcm.gguf",
131
+ "description": "Anything V5 / V3 LCM (Q4_0) - 4~6 step Stylized Anime art",
132
+ "size_mb": 1566,
133
+ "default_steps": 6,
134
+ "default_cfg": 1.5,
135
+ "default_sampler": "lcm",
136
+ },
137
+ "ghostmix-lcm": {
138
+ "repo_id": "void-gryph/ghostmix-GGUF",
139
+ "filename": "ghostmix.Q4_K_M.gguf",
140
+ "alias": "ghostmix.gguf",
141
+ "description": "GhostMix (Q4_K_M) - 2.5D Semi-realistic Anime illustration",
142
+ "size_mb": 484,
143
+ "default_steps": 15,
144
+ "default_cfg": 6.0,
145
+ },
146
+ "cyberrealistic-lcm": {
147
+ "repo_id": "void-gryph/cyberrealistic-GGUF",
148
+ "filename": "cyberrealistic.Q4_K_M.gguf",
149
+ "alias": "cyberrealistic.gguf",
150
+ "description": "CyberRealistic (Q4_K_M) - High-detail photorealistic portraits",
151
+ "size_mb": 484,
152
+ "default_steps": 15,
153
+ "default_cfg": 6.0,
154
+ },
155
+ "bk-sdm-tiny": {
156
+ "repo_id": "concedo/sdxs-512-tinySDdistilled-GGUF",
157
+ "filename": "sdxs-512-tinySDdistilled_Q8_0.gguf",
158
+ "alias": "sdxs.gguf",
159
+ "description": "BK-SDM Tiny Fallback (SDXS Distilled Q8_0) - Ultra-lightweight on-device diffusion",
160
+ "size_mb": 651,
161
+ "default_steps": 1,
162
+ "default_cfg": 1.0,
163
+ },
117
164
  })
118
165
 
119
166
  _registry_lock = threading.Lock()
@@ -33,6 +33,13 @@ from .platform import (
33
33
  is_arm64,
34
34
  )
35
35
 
36
+ from .engine import (
37
+ get_engine_bin_dir,
38
+ get_engine_lib_dir,
39
+ get_binary_path,
40
+ locate_sd_cli,
41
+ )
42
+
36
43
  from ._version import __version__
37
44
 
38
45
  logger = logging.getLogger("termux_diffusion.installer")
@@ -60,11 +67,11 @@ def get_candidate_prebuilt_urls(filename: str = "sd-cli-cpu-android-arm64.tar.gz
60
67
  urls: List[str] = []
61
68
  if custom_base := os.environ.get("TERMUX_DIFFUSION_RELEASE_BASE") or os.environ.get("AMEVA_RELEASE_BASE"):
62
69
  urls.append(f"{custom_base.rstrip('/')}/{filename}")
63
- if custom_tag := os.environ.get("TERMUX_DIFFUSION_RELEASE_TAG") or os.environ.get("AMEVA_RELEASE_TAG"):
64
- tag = custom_tag if custom_tag.startswith("v") else f"v{custom_tag}"
65
- urls.append(f"https://github.com/uno-km/termux-diffusion/releases/download/{tag}/{filename}")
66
- urls.append(f"https://github.com/uno-km/termux-diffusion/releases/download/v{__version__}/{filename}")
70
+ # Tier 2: GitHub Releases latest canonical endpoint (Zero-Hardcoding SSOT)
67
71
  urls.append(f"{TERMUX_DIFFUSION_RELEASE_LATEST}/{filename}")
72
+
73
+ # Tier 3: Installed package dynamic version matching
74
+ urls.append(f"https://github.com/uno-km/termux-diffusion/releases/download/v{__version__}/{filename}")
68
75
  return urls
69
76
 
70
77
 
@@ -79,7 +86,8 @@ def activate_binary(bin_dir: Path, target_name: str) -> Path:
79
86
  allowed = {
80
87
  "sd-cli-cpu",
81
88
  "sd-cli-source-cpu",
82
- "sd-cli"
89
+ "sd-cli",
90
+ "sd-cli-vulkan",
83
91
  }
84
92
  if target_name not in allowed:
85
93
  raise ValueError(f"Unauthorized activation target: {target_name}")
@@ -201,59 +209,6 @@ def fetch_prebuilt_binary(install_mode: str = "prebuilt-first") -> Optional[Path
201
209
  return None
202
210
 
203
211
 
204
- def get_engine_bin_dir() -> Path:
205
- """Return directory where compiled termux-diffusion binaries reside ($PREFIX/bin SSOT)."""
206
- prefix = os.environ.get("PREFIX") or "/data/data/com.termux/files/usr"
207
- if is_android_termux() or os.path.exists(prefix):
208
- bin_dir = Path(prefix) / "bin"
209
- else:
210
- bin_dir = get_default_cache_dir() / "bin"
211
- bin_dir.mkdir(parents=True, exist_ok=True)
212
- return bin_dir
213
-
214
-
215
- def get_engine_lib_dir() -> Path:
216
- """Return directory where native shared libraries reside ($PREFIX/lib SSOT)."""
217
- prefix = os.environ.get("PREFIX") or "/data/data/com.termux/files/usr"
218
- if is_android_termux() or os.path.exists(prefix):
219
- lib_dir = Path(prefix) / "lib"
220
- else:
221
- lib_dir = get_default_cache_dir() / "lib"
222
- lib_dir.mkdir(parents=True, exist_ok=True)
223
- return lib_dir
224
-
225
-
226
- def locate_sd_cli(backend: Optional[str] = None) -> Optional[Path]:
227
- """Locate the compiled sd-cli executable binary across standard locations."""
228
- bin_dir = get_engine_bin_dir()
229
- candidates = ("sd-cli-cpu", "sd-cli-source-cpu", "sd-cli", "sd-cli.exe", "sd", "sd.exe")
230
-
231
- for fname in candidates:
232
- cached_bin = bin_dir / fname
233
- if cached_bin.is_file() and (os.access(cached_bin, os.X_OK) or os.name == "nt"):
234
- return cached_bin.resolve()
235
-
236
- # 2. System PATH
237
- for name in ("sd-cli", "sd-cli.exe", "sd", "sd.exe"):
238
- which_path = shutil.which(name)
239
- if which_path and (os.access(which_path, os.X_OK) or os.name == "nt"):
240
- return Path(which_path).resolve()
241
-
242
- # 3. Termux prefix bin
243
- for name in ("sd-cli", "sd"):
244
- termux_bin = Path(TERMUX_PREFIX) / "bin" / name
245
- if termux_bin.is_file() and (os.access(termux_bin, os.X_OK) or os.name == "nt"):
246
- return termux_bin.resolve()
247
-
248
- # 4. Standard user local binary paths
249
- for name in ("sd-cli", "sd-cli.exe"):
250
- local_bin = Path(os.path.expanduser(f"~/.local/bin/{name}"))
251
- if local_bin.is_file() and (os.access(local_bin, os.X_OK) or os.name == "nt"):
252
- return local_bin.resolve()
253
-
254
- return None
255
-
256
-
257
212
  def provision_engine(
258
213
  force: bool = False,
259
214
  install_mode: str = "prebuilt-first",
@@ -474,98 +429,6 @@ def provision_engine(
474
429
  return target_bin.resolve()
475
430
 
476
431
 
477
- def run_doctor() -> bool:
478
- """Run comprehensive 8-tier pre-flight diagnostic checks for Samsung Galaxy Termux setup."""
479
- print("=" * 65)
480
- print("[Doctor] [termux-diffusion] Pre-flight Diagnostic Doctor")
481
- print("=" * 65)
482
-
483
- all_passed = True
484
-
485
- # 1. Platform Check
486
- is_termux = is_android_termux()
487
- arm_arch = is_arm64()
488
- print(f"1. Platform: {'Android Termux [OK]' if is_termux else 'Non-Termux Host (Emulation Mode) [INFO]'}")
489
- print(f"2. Architecture: {'ARM64 / aarch64 [OK]' if arm_arch else f'Host {sys.platform} ({os.name}) [INFO]'}")
490
-
491
- # 3. Memory & RAM Plus
492
- mem = get_memory_info()
493
- safe, msg = check_memory_safety(required_mb=1200)
494
- print(f"3. System Memory: {mem['mem_total_mb']}MB RAM + {mem['swap_total_mb']}MB Swap ({'Safe [OK]' if safe else 'Warning [WARN]'})")
495
- if not safe:
496
- print(f" -> {msg}")
497
-
498
- # 4. Storage & Samsung Gallery
499
- storage_ok = os.path.exists(os.path.expanduser("~/storage"))
500
- print(f"4. Android Storage Permission: {'Configured [OK]' if storage_ok else 'Missing [WARN] (Run termux-setup-storage)'}")
501
-
502
- # 5. Compiler Toolchain
503
- clang_ok = bool(shutil.which("clang") or shutil.which("gcc") or shutil.which("clang++"))
504
- cmake_ok = bool(shutil.which("cmake"))
505
- git_ok = bool(shutil.which("git"))
506
- 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]'})")
507
- if not (clang_ok and cmake_ok and git_ok) and is_termux:
508
- all_passed = False
509
- print(" -> Run: pkg install clang cmake git termux-api -y")
510
-
511
- # 6. Engine Binary
512
- engine = locate_sd_cli()
513
- print(f"6. Native C++ Engine (sd-cli): {str(engine) + ' [OK]' if engine else 'Not Provisioned [FAIL] (Run termux-diffusion-install)'}")
514
- if not engine:
515
- all_passed = False
516
-
517
- # 7. Model Cache Status
518
- from .hub import list_cached_models
519
- cached = list_cached_models()
520
- print(f"7. Cached GGUF Models: {len(cached)} model(s) available locally.")
521
- for m in cached:
522
- valid_tag = " [GGUF Valid [OK]]" if m.get("is_valid_gguf") else " [Header [WARN]]"
523
- print(f" -> {m['name']} ({m['size_mb']} MB){valid_tag}")
524
-
525
- # 8. Hardware Acceleration (GPU / NPU / TPU / Vulkan / OpenCL)
526
- try:
527
- from ameva_runtime.vulkan.adapters import find_system_vulkan_driver_dir, DiffusionAdapter
528
- discovered_vulkan = find_system_vulkan_driver_dir()
529
- except ImportError:
530
- discovered_vulkan = None
531
-
532
- from .hardware import detect_hardware_profile, format_hardware_report
533
- hw = detect_hardware_profile()
534
- print(f"8. Hardware Acceleration Profile:")
535
- print(f" SoC: {hw.soc_name}, GPU Architecture: {hw.gpu_name}")
536
- print(f" GPU Vulkan: {'Available [OK]' if (hw.vulkan_available or discovered_vulkan) else 'Not Found [WARN]'}")
537
- if discovered_vulkan:
538
- print(f" -> Vulkan Driver (ameva-runtime): {discovered_vulkan}")
539
- elif hw.vulkan_driver:
540
- print(f" -> Vulkan Driver: {hw.vulkan_driver.library_path}")
541
- print(f" GPU OpenCL: {'Available [OK]' if hw.opencl_available else 'Not Found [WARN]'}")
542
- if hw.opencl_driver:
543
- print(f" -> OpenCL Driver: {hw.opencl_driver.library_path}")
544
- if hw.npu_profile and hw.npu_profile.available:
545
- print(f" NPU / TPU Hardware: Detected [INFO]")
546
- print(f" -> Architecture: {hw.npu_profile.dsp_architecture} ({hw.npu_profile.tops_rating} TOPS)")
547
- print(f" -> Driver: {hw.npu_profile.driver_library}")
548
- print(f" -> Runtime: Native QNN C++ execution scheduled for v2.0")
549
- else:
550
- print(f" NPU / TPU Hardware: Not Detected [INFO] (GPU Vulkan & CPU NEON Active)")
551
- print(f" CPU ISA SIMD: DotProd={'[OK]' if hw.has_dotprod else '[FAIL]'} "
552
- f"FP16={'[OK]' if hw.has_fp16 else '[FAIL]'} "
553
- f"I8MM={'[OK]' if hw.has_i8mm else '[FAIL]'} "
554
- f"SVE={'[OK]' if hw.has_sve else '[FAIL]'}")
555
- print(f" Active Compute Pipeline: {hw.recommended_backend.value.upper()} "
556
- f"(Offload Layers: {hw.recommended_ngl})")
557
-
558
- # 9. Android 12+ Background Stability Guard (Phantom Process Killer)
559
- if is_termux:
560
- print("9. Android 12+ Background Guard:")
561
- print(" -> Tip: If generation crashes when Termux is in background, enable")
562
- print(" 'Developer Options > Disable child process restrictions'")
563
- print(" or run: adb shell \"/system/bin/device_config put activity_manager max_phantom_processes 2147483647\"")
564
-
565
- print("=" * 65)
566
- if all_passed:
567
- print("All core diagnostics passed. System is ready for AI image generation.")
568
- else:
569
- print("Some diagnostics need attention. Run 'termux-diffusion-install' to resolve.")
570
- print("=" * 65)
571
- return all_passed
432
+ # Re-export diagnostic utilities from .doctor for backward compatibility
433
+ from .doctor import run_doctor
434
+
@@ -19,11 +19,19 @@ TERMUX_PREFIX = os.environ.get("PREFIX") or "/data/data/com.termux/files/usr"
19
19
  TERMUX_HOME = os.environ.get("HOME") or "/data/data/com.termux/files/home"
20
20
 
21
21
 
22
- def get_clean_execution_env(base_env: Optional[Dict[str, str]] = None) -> Dict[str, str]:
22
+ def get_clean_execution_env(
23
+ base_env: Optional[Dict[str, str]] = None,
24
+ backend: str = "cpu",
25
+ ) -> Dict[str, str]:
23
26
  """Construct clean execution environment for Termux binaries with proper library search paths.
24
27
 
25
28
  Guarantees $PREFIX, $PREFIX/lib, and companion directories are correctly populated
26
29
  while preventing Android system library collision (/system/, /vendor/, etc.).
30
+
31
+ Zero-Collision Rule:
32
+ For native Bionic Vulkan binaries (backend in ('vulkan', 'gpu')), Termux $PREFIX/lib is
33
+ strictly excluded from LD_LIBRARY_PATH to eliminate symbol collisions with Android Bionic
34
+ system libraries (/system/lib64/libunwindstack.so, etc.).
27
35
  """
28
36
  env = (base_env or os.environ).copy()
29
37
  prefix = os.environ.get("PREFIX") or "/data/data/com.termux/files/usr"
@@ -36,16 +44,28 @@ def get_clean_execution_env(base_env: Optional[Dict[str, str]] = None) -> Dict[s
36
44
  if bin_dir not in cur_path.split(":"):
37
45
  env["PATH"] = f"{bin_dir}:{cur_path}" if cur_path else bin_dir
38
46
 
47
+ req_backend = str(backend or "cpu").strip().lower()
48
+ is_vulkan_route = req_backend in ("vulkan", "gpu")
49
+
39
50
  forbidden_prefixes = ("/system/", "/vendor/", "/apex/", "/system_ext/", "/odm/", "/product/")
40
51
  cur_ld = env.get("LD_LIBRARY_PATH", "")
41
52
  existing_parts = [p for p in cur_ld.split(":") if p and not any(p.startswith(fp) for fp in forbidden_prefixes)]
42
53
 
43
- lib_candidates = [
44
- str(Path(prefix) / "lib"),
45
- str(Path.home() / ".cache" / "termux-diffusion" / "lib"),
46
- str(Path.home() / ".cache" / "termux-diffusion" / "staging" / "lib"),
47
- str(Path.home() / ".local" / "lib"),
48
- ]
54
+ if is_vulkan_route:
55
+ # Strictly isolate Vulkan binaries from Termux /usr/lib to prevent libc++ ABI crash
56
+ lib_candidates = [
57
+ str(Path.home() / ".local" / "share" / "ameva" / "current" / "diffusion"),
58
+ str(Path.home() / ".cache" / "termux-diffusion" / "lib"),
59
+ ]
60
+ # Filter out prefix/lib from existing parts for Vulkan
61
+ existing_parts = [p for p in existing_parts if not p.startswith(str(Path(prefix) / "lib"))]
62
+ else:
63
+ lib_candidates = [
64
+ str(Path(prefix) / "lib"),
65
+ str(Path.home() / ".cache" / "termux-diffusion" / "lib"),
66
+ str(Path.home() / ".cache" / "termux-diffusion" / "staging" / "lib"),
67
+ str(Path.home() / ".local" / "lib"),
68
+ ]
49
69
 
50
70
  merged: List[str] = []
51
71
  for d in lib_candidates + existing_parts:
@@ -54,6 +74,8 @@ def get_clean_execution_env(base_env: Optional[Dict[str, str]] = None) -> Dict[s
54
74
 
55
75
  if merged:
56
76
  env["LD_LIBRARY_PATH"] = ":".join(merged)
77
+ elif is_vulkan_route and "LD_LIBRARY_PATH" in env:
78
+ env.pop("LD_LIBRARY_PATH", None)
57
79
 
58
80
  # Detect libegl_shim.so for Android Mali/Samsung GOS Vulkan HAL compatibility
59
81
  shim_candidates = [
@@ -72,7 +72,7 @@ def run_binary_self_test(
72
72
 
73
73
  # Build environment with clean Termux companion library search paths
74
74
  from .platform import get_clean_execution_env
75
- env = get_clean_execution_env()
75
+ env = get_clean_execution_env(backend=expected_backend)
76
76
 
77
77
  # --------------------------------------------------------------------------
78
78
  # Stage 1: Load Test (sd-cli --help)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: termux-diffusion
3
- Version: 1.6.8
3
+ Version: 1.7.0
4
4
  Summary: Pure CPU On-Device Stable Diffusion Runtime for Android Termux (ARM64 Bionic NEON SIMD)
5
5
  Home-page: https://github.com/uno-km/termux-diffusion
6
6
  Author: uno-km (AMEVA Foundation)
@@ -9,7 +9,9 @@ termux_diffusion/_version.py
9
9
  termux_diffusion/adapter.py
10
10
  termux_diffusion/cli.py
11
11
  termux_diffusion/core.py
12
+ termux_diffusion/doctor.py
12
13
  termux_diffusion/downloader.py
14
+ termux_diffusion/engine.py
13
15
  termux_diffusion/exceptions.py
14
16
  termux_diffusion/hardware.py
15
17
  termux_diffusion/hub.py
@@ -33,6 +35,8 @@ termux_diffusion/data/bootstrap-manifest.json
33
35
  termux_diffusion/data/release-public-keys.json
34
36
  termux_diffusion/data/validated-vulkan-profiles.json
35
37
  tests/test_core.py
38
+ tests/test_doctor.py
39
+ tests/test_engine.py
36
40
  tests/test_hardware.py
37
41
  tests/test_hub.py
38
42
  tests/test_installer.py
@@ -0,0 +1,13 @@
1
+ """Unit tests for pre-flight doctor diagnostics in termux-diffusion."""
2
+
3
+ from unittest.mock import patch
4
+ from termux_diffusion.doctor import run_doctor
5
+
6
+
7
+ def test_doctor_execution(capsys):
8
+ res = run_doctor()
9
+ captured = capsys.readouterr()
10
+ assert "Pre-flight Diagnostic Doctor" in captured.out
11
+ assert "Platform:" in captured.out
12
+ assert "Native C++ Engine" in captured.out
13
+ assert isinstance(res, bool)
@@ -0,0 +1,98 @@
1
+ """Unit tests for DiffusionEngine, backend-specific binary locator, and Zero-Silent-Fallback rules."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from unittest.mock import patch, MagicMock
6
+ import pytest
7
+
8
+ from termux_diffusion.engine import (
9
+ DiffusionEngine,
10
+ DiffusionRuntime,
11
+ get_binary_path,
12
+ locate_sd_cli,
13
+ get_engine_bin_dir,
14
+ get_engine_lib_dir,
15
+ load,
16
+ )
17
+
18
+
19
+ def test_engine_directories():
20
+ bin_dir = get_engine_bin_dir()
21
+ lib_dir = get_engine_lib_dir()
22
+ assert isinstance(bin_dir, Path)
23
+ assert isinstance(lib_dir, Path)
24
+ assert bin_dir.is_dir()
25
+ assert lib_dir.is_dir()
26
+
27
+
28
+ def test_get_binary_path_cpu_returns_path_or_none():
29
+ res = get_binary_path(backend="cpu")
30
+ assert res is None or isinstance(res, Path)
31
+
32
+
33
+ def test_zero_silent_fallback_vulkan_never_returns_cpu_binary(tmp_path):
34
+ """Zero-Silent-Fallback Core Test:
35
+ When backend is 'vulkan' or 'gpu', but only CPU binary (sd-cli) exists,
36
+ get_binary_path MUST return None and NEVER fall back to the CPU binary."""
37
+ mock_bin_dir = tmp_path / "bin"
38
+ mock_bin_dir.mkdir(parents=True)
39
+ cpu_bin = mock_bin_dir / "sd-cli"
40
+ cpu_bin.touch(mode=0o755)
41
+
42
+ with patch("termux_diffusion.engine.get_engine_bin_dir", return_value=mock_bin_dir), \
43
+ patch("shutil.which", return_value=None):
44
+ vk_result = get_binary_path(backend="vulkan")
45
+ assert vk_result is None, "Vulkan backend must return None when sd-cli-vulkan is missing, never fallback to sd-cli"
46
+
47
+ gpu_result = get_binary_path(backend="gpu")
48
+ assert gpu_result is None, "GPU backend must return None when sd-cli-vulkan is missing"
49
+
50
+
51
+ def test_vulkan_binary_located_when_present(tmp_path):
52
+ """Verify that when sd-cli-vulkan is present, it is resolved correctly."""
53
+ mock_bin_dir = tmp_path / "bin"
54
+ mock_bin_dir.mkdir(parents=True)
55
+ vk_bin = mock_bin_dir / "sd-cli-vulkan"
56
+ vk_bin.touch(mode=0o755)
57
+
58
+ with patch("termux_diffusion.engine.get_engine_bin_dir", return_value=mock_bin_dir), \
59
+ patch("termux_diffusion.engine.TERMUX_PREFIX", str(tmp_path)):
60
+ res = get_binary_path(backend="vulkan")
61
+ assert res is not None
62
+ assert res.name == "sd-cli-vulkan"
63
+
64
+
65
+ def test_locate_sd_cli_backward_compatibility():
66
+ res = locate_sd_cli("cpu")
67
+ assert res is None or isinstance(res, Path)
68
+
69
+
70
+ def test_diffusion_engine_initialization_and_methods():
71
+ engine = DiffusionEngine(model="realistic", device="cpu", width=256, height=256)
72
+ assert engine.model == "realistic"
73
+ assert engine.device == "cpu"
74
+ assert engine.requested_backend == "cpu"
75
+ assert engine.kwargs == {"width": 256, "height": 256}
76
+
77
+ # Factory function load()
78
+ loaded_engine = load(model="anime", device="vulkan")
79
+ assert isinstance(loaded_engine, DiffusionEngine)
80
+ assert loaded_engine.model == "anime"
81
+ assert loaded_engine.device == "vulkan"
82
+
83
+ # Alias DiffusionRuntime
84
+ assert DiffusionRuntime is DiffusionEngine
85
+
86
+
87
+ def test_diffusion_engine_generate_delegation():
88
+ engine = DiffusionEngine(model="sdxs", device="cpu")
89
+ with patch("termux_diffusion.core.generate") as mock_gen:
90
+ mock_gen.return_value = Path("/tmp/output.png")
91
+ res = engine.generate("test prompt", steps=4)
92
+ assert res == Path("/tmp/output.png")
93
+ mock_gen.assert_called_once_with(
94
+ prompt="test prompt",
95
+ model="sdxs",
96
+ device="cpu",
97
+ steps=4,
98
+ )
@@ -162,3 +162,22 @@ def test_adreno_650_gpu_failfast_e003():
162
162
  assert "AMEVA-DIFFUSION-E003" in str(exc_info.value)
163
163
  assert "storageBuffer8BitAccess" in str(exc_info.value)
164
164
 
165
+
166
+ def test_adreno_830_gpu_supported_with_vulkan_driver():
167
+ """Verify that Adreno 830 with Vulkan driver resolves to vulkan backend after v2.7.2 shader repair."""
168
+ mock_ameva = MagicMock()
169
+ with patch("termux_diffusion.hardware._resolve_ameva_runtime", return_value=mock_ameva):
170
+ with patch("termux_diffusion.hardware._detect_gpu_name", return_value="Adreno (TM) 830"):
171
+ with patch("termux_diffusion.hardware._detect_soc_name", return_value="SM8750"):
172
+ with patch("termux_diffusion.hardware.detect_hardware_profile") as mock_prof:
173
+ prof = MagicMock()
174
+ prof.vulkan_available = True
175
+ prof.recommended_backend = MagicMock(value="vulkan")
176
+ prof.recommended_ngl = 99
177
+ mock_prof.return_value = prof
178
+ backend, ngl = resolve_device_backend("gpu")
179
+ assert backend == "vulkan"
180
+ assert ngl == 99
181
+
182
+
183
+
@@ -1 +0,0 @@
1
- __version__ = "1.6.8"