termux-diffusion 1.0.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.
@@ -0,0 +1,332 @@
1
+ """Smart Model Hub, preset management, Hugging Face streaming downloader, and local caching."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ import urllib.error
11
+ import urllib.request
12
+ from pathlib import Path
13
+ from typing import Callable, Dict, List, Optional, Union
14
+
15
+ from .exceptions import ModelDownloadError, ModelNotFoundError
16
+ from .platform import get_default_cache_dir
17
+
18
+ logger = logging.getLogger("termux_diffusion.hub")
19
+
20
+ # Built-in Samsung Galaxy & Mobile optimized GGUF presets
21
+ DEFAULT_PRESETS: Dict[str, Dict] = {
22
+ "realistic": {
23
+ "repo_id": "second-state/Realistic_Vision_V6.0_B1-GGUF",
24
+ "filename": "realisticVisionV60B1_v51HyperVAE-Q4_k.gguf",
25
+ "alias": "realistic.gguf",
26
+ "description": "Realistic Vision V6.0 B1 (Q4_K) — Ultra-detailed photorealistic portraits & scenes",
27
+ "size_mb": 1620,
28
+ "default_steps": 10,
29
+ "default_cfg": 4.0,
30
+ },
31
+ "speed": {
32
+ "repo_id": "gpustack/stable-diffusion-v1-5-GGUF",
33
+ "filename": "stable-diffusion-v1-5-Q4_1.gguf",
34
+ "alias": "lightning.gguf",
35
+ "description": "Stable Diffusion 1.5 (Q4_1) — Fast general-purpose base model",
36
+ "size_mb": 1590,
37
+ "default_steps": 10,
38
+ "default_cfg": 4.0,
39
+ },
40
+ "sdxs": {
41
+ "repo_id": "gpustack/SDXS-512-0.9-GGUF",
42
+ "filename": "sdxs-512-0.9-Q4_0.gguf",
43
+ "alias": "sdxs.gguf",
44
+ "description": "SDXS 512-0.9 (Q4_0) — Ultra-lightweight mobile-optimized 2-3 step model (~450MB)",
45
+ "size_mb": 450,
46
+ "default_steps": 2,
47
+ "default_cfg": 2.0,
48
+ },
49
+ "turbo": {
50
+ "repo_id": "second-state/SD-Turbo-GGUF",
51
+ "filename": "sd-turbo-Q4_0.gguf",
52
+ "alias": "turbo.gguf",
53
+ "description": "SD Turbo (Q4_0) — Real-time 1-step inference model",
54
+ "size_mb": 1200,
55
+ "default_steps": 1,
56
+ "default_cfg": 1.5,
57
+ },
58
+ "anime": {
59
+ "repo_id": "second-state/DreamShaper-8-GGUF",
60
+ "filename": "dreamshaper-8-Q4_k.gguf",
61
+ "alias": "anime.gguf",
62
+ "description": "DreamShaper 8 (Q4_K) — Stylized anime & 2.5D illustration art",
63
+ "size_mb": 1650,
64
+ "default_steps": 10,
65
+ "default_cfg": 4.5,
66
+ },
67
+ }
68
+
69
+ _custom_registry: Dict[str, Dict] = {}
70
+ _active_cache_dir: Optional[Path] = None
71
+
72
+
73
+ def set_cache_dir(path: Union[str, Path]) -> Path:
74
+ """Explicitly set custom storage directory for model weights and caches."""
75
+ global _active_cache_dir
76
+ resolved = Path(os.path.expanduser(str(path))).resolve()
77
+ resolved.mkdir(parents=True, exist_ok=True)
78
+ _active_cache_dir = resolved
79
+ logger.info("termux-diffusion cache directory set to: %s", _active_cache_dir)
80
+ return _active_cache_dir
81
+
82
+
83
+ def get_cache_dir() -> Path:
84
+ """Get active model cache directory."""
85
+ if _active_cache_dir is not None:
86
+ return _active_cache_dir
87
+ return get_default_cache_dir() / "models"
88
+
89
+
90
+ def register_model(
91
+ name: str,
92
+ repo_id: str,
93
+ filename: str,
94
+ alias: Optional[str] = None,
95
+ description: Optional[str] = None,
96
+ default_steps: int = 10,
97
+ default_cfg: float = 4.0,
98
+ ) -> None:
99
+ """Register a custom Hugging Face GGUF model into the hub catalog."""
100
+ alias_name = alias or f"{name}.gguf"
101
+ _custom_registry[name] = {
102
+ "repo_id": repo_id,
103
+ "filename": filename,
104
+ "alias": alias_name,
105
+ "description": description or f"Custom model '{name}' from {repo_id}",
106
+ "default_steps": default_steps,
107
+ "default_cfg": default_cfg,
108
+ }
109
+ logger.info("Registered custom model '%s' -> %s/%s", name, repo_id, filename)
110
+
111
+
112
+ def list_presets() -> Dict[str, Dict]:
113
+ """Return dictionary of all available presets (built-in + custom)."""
114
+ combined = dict(DEFAULT_PRESETS)
115
+ combined.update(_custom_registry)
116
+ return combined
117
+
118
+
119
+ def is_model_cached(model_name_or_path: str, cache_dir: Optional[Union[str, Path]] = None) -> bool:
120
+ """Check if a model exists locally as an exact file or cached preset alias."""
121
+ target_dir = Path(cache_dir).resolve() if cache_dir else get_cache_dir()
122
+
123
+ # 1. Direct file path check
124
+ direct_path = Path(os.path.expanduser(model_name_or_path))
125
+ if direct_path.is_file():
126
+ return True
127
+
128
+ # 2. In cache directory direct filename check
129
+ in_cache_path = target_dir / model_name_or_path
130
+ if in_cache_path.is_file():
131
+ return True
132
+
133
+ # 3. Preset alias check
134
+ presets = list_presets()
135
+ if model_name_or_path in presets:
136
+ alias = presets[model_name_or_path]["alias"]
137
+ if (target_dir / alias).is_file():
138
+ return True
139
+
140
+ return False
141
+
142
+
143
+ def resolve_model_path(model_name_or_path: str, cache_dir: Optional[Union[str, Path]] = None) -> Path:
144
+ """Resolve model path to an absolute Path, downloading it if not yet cached."""
145
+ target_dir = Path(cache_dir).resolve() if cache_dir else get_cache_dir()
146
+
147
+ # Direct file exists
148
+ direct_path = Path(os.path.expanduser(model_name_or_path))
149
+ if direct_path.is_file():
150
+ return direct_path.resolve()
151
+
152
+ # Direct filename in cache
153
+ in_cache_path = target_dir / model_name_or_path
154
+ if in_cache_path.is_file():
155
+ return in_cache_path.resolve()
156
+
157
+ # Preset alias
158
+ presets = list_presets()
159
+ if model_name_or_path in presets:
160
+ alias = presets[model_name_or_path]["alias"]
161
+ alias_path = target_dir / alias
162
+ if alias_path.is_file():
163
+ return alias_path.resolve()
164
+ # Automatically download preset if not cached
165
+ return download_model(model_name_or_path, cache_dir=target_dir)
166
+
167
+ # Direct URL or custom HuggingFace repo identifier
168
+ if model_name_or_path.startswith("http://") or model_name_or_path.startswith("https://") or ("/" in model_name_or_path and not Path(model_name_or_path).is_file()):
169
+ return download_model(model_name_or_path, cache_dir=target_dir)
170
+
171
+ raise ModelNotFoundError(
172
+ f"Model '{model_name_or_path}' could not be resolved. "
173
+ f"Available presets: {list(presets.keys())}, or specify a custom repo ('org/repo/file.gguf'), "
174
+ f"a direct URL ('https://.../model.gguf'), or an existing local .gguf file path."
175
+ )
176
+
177
+
178
+ def download_model(
179
+ model_name_or_url: str,
180
+ cache_dir: Optional[Union[str, Path]] = None,
181
+ force: bool = False,
182
+ progress_callback: Optional[Callable[[int, int], None]] = None,
183
+ ) -> Path:
184
+ """Download GGUF model weights from Hugging Face or direct HTTP URL with progress display and resume capability."""
185
+ target_dir = Path(cache_dir).resolve() if cache_dir else get_cache_dir()
186
+ target_dir.mkdir(parents=True, exist_ok=True)
187
+
188
+ presets = list_presets()
189
+ if model_name_or_url in presets:
190
+ info = presets[model_name_or_url]
191
+ repo_id = info["repo_id"]
192
+ filename = info["filename"]
193
+ target_filename = info.get("alias", filename)
194
+ download_url = f"https://huggingface.co/{repo_id}/resolve/main/{filename}"
195
+ elif model_name_or_url.startswith("http://") or model_name_or_url.startswith("https://"):
196
+ # Direct URL download
197
+ download_url = model_name_or_url
198
+ target_filename = model_name_or_url.split("?")[0].rstrip("/").split("/")[-1]
199
+ if not target_filename.endswith(".gguf"):
200
+ target_filename += ".gguf"
201
+ elif "/" in model_name_or_url:
202
+ # Custom HuggingFace repo identifier (e.g. org/repo/file.gguf)
203
+ parts = model_name_or_url.split("/")
204
+ if len(parts) == 3:
205
+ repo_id = f"{parts[0]}/{parts[1]}"
206
+ filename = parts[2]
207
+ target_filename = filename
208
+ download_url = f"https://huggingface.co/{repo_id}/resolve/main/{filename}"
209
+ elif len(parts) == 2:
210
+ repo_id = f"{parts[0]}/{parts[1]}"
211
+ # Query Hugging Face API to find the main .gguf file in the repository
212
+ try:
213
+ api_url = f"https://huggingface.co/api/models/{repo_id}"
214
+ req = urllib.request.Request(api_url, headers={"User-Agent": "termux-diffusion/1.0.0"})
215
+ with urllib.request.urlopen(req, timeout=10.0) as resp:
216
+ data = json.loads(resp.read().decode("utf-8"))
217
+ siblings = [s.get("rfilename", "") for s in data.get("siblings", [])]
218
+ gguf_files = [f for f in siblings if f.endswith(".gguf")]
219
+ if gguf_files:
220
+ filename = gguf_files[0]
221
+ target_filename = filename
222
+ download_url = f"https://huggingface.co/{repo_id}/resolve/main/{filename}"
223
+ else:
224
+ raise ModelNotFoundError(f"No .gguf file found in repository '{repo_id}'")
225
+ except Exception as e:
226
+ raise ModelNotFoundError(f"Could not inspect Hugging Face repo '{repo_id}': {e}") from e
227
+ else:
228
+ raise ModelNotFoundError(f"Invalid model reference: '{model_name_or_url}'")
229
+ else:
230
+ raise ModelNotFoundError(f"Unknown model preset: '{model_name_or_url}'. Available: {list(presets.keys())}")
231
+
232
+ final_path = target_dir / target_filename
233
+ if final_path.is_file() and not force:
234
+ logger.info("Model '%s' already cached at: %s", model_name_or_url, final_path)
235
+ return final_path
236
+
237
+ temp_path = target_dir / f"{target_filename}.part"
238
+ logger.info("Downloading '%s' from %s -> %s", model_name_or_url, download_url, final_path)
239
+ print(f"[termux-diffusion] Fetching model '{target_filename}' ({download_url})...")
240
+
241
+ # Attempt download with streaming chunk writer and resume support
242
+ try:
243
+ _stream_download(download_url, temp_path, progress_callback)
244
+ temp_path.rename(final_path)
245
+ print(f"[termux-diffusion] Successfully cached model at: {final_path}")
246
+ return final_path
247
+ except Exception as exc:
248
+ if temp_path.exists():
249
+ try:
250
+ temp_path.unlink()
251
+ except OSError:
252
+ pass
253
+ raise ModelDownloadError(f"Failed downloading model '{model_name_or_url}' from {download_url}: {exc}") from exc
254
+
255
+
256
+ def _stream_download(
257
+ url: str,
258
+ temp_path: Path,
259
+ progress_callback: Optional[Callable[[int, int], None]] = None,
260
+ ) -> None:
261
+ """Download large binary file via HTTP streaming with real-time terminal progress."""
262
+ headers = {"User-Agent": "termux-diffusion/1.0.0"}
263
+ req = urllib.request.Request(url, headers=headers)
264
+
265
+ with urllib.request.urlopen(req, timeout=30.0) as resp:
266
+ total_size = int(resp.headers.get("content-length", 0))
267
+ downloaded = 0
268
+ chunk_size = 1024 * 1024 # 1MB chunks
269
+
270
+ with open(temp_path, "wb") as f:
271
+ last_print = time.time()
272
+ while True:
273
+ chunk = resp.read(chunk_size)
274
+ if not chunk:
275
+ break
276
+ f.write(chunk)
277
+ downloaded += len(chunk)
278
+
279
+ if progress_callback:
280
+ progress_callback(downloaded, total_size)
281
+ elif total_size > 0 and (time.time() - last_print > 0.5):
282
+ pct = (downloaded / total_size) * 100
283
+ mb_done = downloaded / (1024 * 1024)
284
+ mb_total = total_size / (1024 * 1024)
285
+ print(f" > Progress: {mb_done:.1f}MB / {mb_total:.1f}MB ({pct:.1f}%)", end="\r", flush=True)
286
+ last_print = time.time()
287
+
288
+ if total_size > 0:
289
+ print(f" > Progress: {total_size / (1024*1024):.1f}MB / {total_size / (1024*1024):.1f}MB (100.0%)")
290
+
291
+ print() # newline after completion
292
+
293
+
294
+ def list_cached_models(cache_dir: Optional[Union[str, Path]] = None) -> List[Dict]:
295
+ """List all currently downloaded and cached GGUF models on device."""
296
+ target_dir = Path(cache_dir).resolve() if cache_dir else get_cache_dir()
297
+ results = []
298
+ if not target_dir.is_dir():
299
+ return results
300
+
301
+ for item in target_dir.glob("*.gguf"):
302
+ size_mb = item.stat().st_size / (1024 * 1024)
303
+ results.append({
304
+ "name": item.name,
305
+ "path": str(item.resolve()),
306
+ "size_mb": round(size_mb, 2),
307
+ "last_modified": item.stat().st_mtime,
308
+ })
309
+ return results
310
+
311
+
312
+ def clear_cache(cache_dir: Optional[Union[str, Path]] = None, model_name: Optional[str] = None) -> int:
313
+ """Clear cached models to reclaim storage space. Returns number of files removed."""
314
+ target_dir = Path(cache_dir).resolve() if cache_dir else get_cache_dir()
315
+ if not target_dir.is_dir():
316
+ return 0
317
+
318
+ removed = 0
319
+ if model_name:
320
+ target_file = target_dir / model_name
321
+ if not target_file.name.endswith(".gguf"):
322
+ target_file = target_dir / f"{model_name}.gguf"
323
+ if target_file.is_file():
324
+ target_file.unlink()
325
+ removed += 1
326
+ else:
327
+ for item in target_dir.glob("*.gguf"):
328
+ item.unlink()
329
+ removed += 1
330
+
331
+ logger.info("Cleared %d cached model file(s) from %s", removed, target_dir)
332
+ return removed
@@ -0,0 +1,206 @@
1
+ """Automated C++ core engine provisioning, binary locator, build healer, and doctor diagnostics."""
2
+
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Tuple
10
+
11
+ from .exceptions import ProvisioningError
12
+ from .platform import (
13
+ TERMUX_PREFIX,
14
+ check_memory_safety,
15
+ get_default_cache_dir,
16
+ get_memory_info,
17
+ is_android_termux,
18
+ is_arm64,
19
+ )
20
+
21
+ logger = logging.getLogger("termux_diffusion.installer")
22
+
23
+ SD_CPP_REPO = "https://github.com/leejet/stable-diffusion.cpp"
24
+
25
+
26
+ def get_engine_bin_dir() -> Path:
27
+ """Return directory where compiled termux-diffusion binaries reside."""
28
+ bin_dir = get_default_cache_dir() / "bin"
29
+ bin_dir.mkdir(parents=True, exist_ok=True)
30
+ return bin_dir
31
+
32
+
33
+ def locate_sd_cli() -> Optional[Path]:
34
+ """Locate the compiled sd-cli executable binary across standard locations."""
35
+ # 1. Custom cached engine binary
36
+ cached_bin = get_engine_bin_dir() / "sd-cli"
37
+ if cached_bin.is_file() and os.access(cached_bin, os.X_OK):
38
+ return cached_bin.resolve()
39
+
40
+ # 2. System PATH
41
+ for name in ("sd-cli", "sd"):
42
+ which_path = shutil.which(name)
43
+ if which_path and os.access(which_path, os.X_OK):
44
+ return Path(which_path).resolve()
45
+
46
+ # 3. Termux prefix bin
47
+ termux_bin = Path(TERMUX_PREFIX) / "bin" / "sd-cli"
48
+ if termux_bin.is_file() and os.access(termux_bin, os.X_OK):
49
+ return termux_bin.resolve()
50
+
51
+ # 4. Standard local build workspace
52
+ home_workspace = Path(os.path.expanduser("~/projects/ai-workspace/stable-diffusion.cpp/build/bin"))
53
+ for candidate in ("sd-cli", "sd"):
54
+ p = home_workspace / candidate
55
+ if p.is_file() and os.access(p, os.X_OK):
56
+ return p.resolve()
57
+
58
+ return None
59
+
60
+
61
+ def provision_engine(force: bool = False) -> Path:
62
+ """Download, update submodules, and compile stable-diffusion.cpp into ~/.cache/termux-diffusion/bin/sd-cli."""
63
+ existing = locate_sd_cli()
64
+ if existing and not force:
65
+ logger.info("Found existing native engine binary at: %s", existing)
66
+ return existing
67
+
68
+ print("[termux-diffusion] Initializing native ARM64 Bionic engine provisioning...")
69
+
70
+ # Step 1: Ensure required system packages
71
+ if is_android_termux() and shutil.which("pkg"):
72
+ print("[termux-diffusion] Checking build toolchains (git, cmake, clang, termux-api)...")
73
+ try:
74
+ subprocess.run(
75
+ ["pkg", "install", "-y", "git", "cmake", "clang", "termux-api", "wget"],
76
+ capture_output=False,
77
+ check=False,
78
+ timeout=180.0
79
+ )
80
+ except Exception as exc:
81
+ logger.warning("pkg install check note: %s", exc)
82
+
83
+ # Step 2: Set up build directory
84
+ build_root = get_default_cache_dir() / "build_src"
85
+ build_root.mkdir(parents=True, exist_ok=True)
86
+ repo_dir = build_root / "stable-diffusion.cpp"
87
+
88
+ if not repo_dir.exists():
89
+ print(f"[termux-diffusion] Cloning {SD_CPP_REPO}...")
90
+ res = subprocess.run(["git", "clone", SD_CPP_REPO, str(repo_dir)], capture_output=True, text=True)
91
+ if res.returncode != 0:
92
+ raise ProvisioningError(f"Failed cloning stable-diffusion.cpp repository: {res.stderr}")
93
+
94
+ # Step 3: Crucial Submodule Update (Ensures ggml is present)
95
+ print("[termux-diffusion] Synchronizing tensor submodules (ggml)...")
96
+ subprocess.run(
97
+ ["git", "submodule", "update", "--init", "--recursive"],
98
+ cwd=str(repo_dir),
99
+ capture_output=True,
100
+ check=False
101
+ )
102
+
103
+ # Step 4: CMake & Compilation
104
+ build_dir = repo_dir / "build"
105
+ build_dir.mkdir(parents=True, exist_ok=True)
106
+
107
+ print("[termux-diffusion] Configuring CMake build with ARM64 optimizations...")
108
+ cmake_cmd = [
109
+ "cmake", "..",
110
+ "-DCMAKE_BUILD_TYPE=Release",
111
+ "-DSD_BUILD_EXAMPLES=ON",
112
+ "-DGGML_OPENMP=OFF",
113
+ "-DCMAKE_C_FLAGS=-O3 -D_GNU_SOURCE",
114
+ "-DCMAKE_CXX_FLAGS=-O3 -D_GNU_SOURCE"
115
+ ]
116
+ cmake_res = subprocess.run(
117
+ cmake_cmd,
118
+ cwd=str(build_dir),
119
+ capture_output=True,
120
+ text=True
121
+ )
122
+ if cmake_res.returncode != 0:
123
+ raise ProvisioningError(f"CMake configuration failed: {cmake_res.stderr}")
124
+
125
+ print("[termux-diffusion] Compiling native Bionic binary with clang (make -j4)...")
126
+ make_res = subprocess.run(
127
+ ["make", "-j4"],
128
+ cwd=str(build_dir),
129
+ capture_output=False
130
+ )
131
+ if make_res.returncode != 0:
132
+ raise ProvisioningError("Compilation failed. Please run 'termux-diffusion doctor' to diagnose missing headers.")
133
+
134
+ # Locate compiled binary
135
+ compiled_bin = None
136
+ for candidate in (build_dir / "bin" / "sd-cli", build_dir / "bin" / "sd", build_dir / "sd-cli", build_dir / "sd"):
137
+ if candidate.is_file():
138
+ compiled_bin = candidate
139
+ break
140
+
141
+ if not compiled_bin:
142
+ raise ProvisioningError("Could not locate compiled binary in build directory.")
143
+
144
+ # Install into cache bin directory
145
+ target_bin = get_engine_bin_dir() / "sd-cli"
146
+ shutil.copy2(compiled_bin, target_bin)
147
+ target_bin.chmod(0o755)
148
+
149
+ print(f"[termux-diffusion] Engine provisioned successfully at: {target_bin}")
150
+ return target_bin.resolve()
151
+
152
+
153
+ def run_doctor() -> bool:
154
+ """Run comprehensive 7-tier pre-flight diagnostic checks for Samsung Galaxy Termux setup."""
155
+ print("=" * 65)
156
+ print("🩺 [termux-diffusion] Pre-flight Diagnostic Doctor")
157
+ print("=" * 65)
158
+
159
+ all_passed = True
160
+
161
+ # 1. Platform Check
162
+ is_termux = is_android_termux()
163
+ arm_arch = is_arm64()
164
+ print(f"1. Platform: {'Android Termux ✅' if is_termux else 'Non-Termux Host (Emulation Mode) ℹ️'}")
165
+ print(f"2. Architecture: {'ARM64 / aarch64 ✅' if arm_arch else f'Host {sys.platform} ({os.name}) ℹ️'}")
166
+
167
+ # 3. Memory & RAM Plus
168
+ mem = get_memory_info()
169
+ safe, msg = check_memory_safety(required_mb=1200)
170
+ print(f"3. System Memory: {mem['mem_total_mb']}MB RAM + {mem['swap_total_mb']}MB Swap ({'Safe ✅' if safe else 'Warning ⚠️'})")
171
+ if not safe:
172
+ print(f" ↳ {msg}")
173
+
174
+ # 4. Storage & Samsung Gallery
175
+ storage_ok = os.path.exists(os.path.expanduser("~/storage"))
176
+ print(f"4. Android Storage Permission: {'Configured ✅' if storage_ok else 'Missing ⚠️ (Run termux-setup-storage)'}")
177
+
178
+ # 5. Compiler Toolchain
179
+ clang_ok = bool(shutil.which("clang") or shutil.which("gcc"))
180
+ cmake_ok = bool(shutil.which("cmake"))
181
+ git_ok = bool(shutil.which("git"))
182
+ print(f"5. Build Tools: clang ({'✅' if clang_ok else '❌'}), cmake ({'✅' if cmake_ok else '❌'}), git ({'✅' if git_ok else '❌'})")
183
+ if not (clang_ok and cmake_ok and git_ok) and is_termux:
184
+ all_passed = False
185
+ print(" ↳ Run: pkg install clang cmake git termux-api -y")
186
+
187
+ # 6. Engine Binary
188
+ engine = locate_sd_cli()
189
+ print(f"6. Native C++ Engine (sd-cli): {str(engine) + ' ✅' if engine else 'Not Provisioned ❌ (Run termux-diffusion-install)'}")
190
+ if not engine:
191
+ all_passed = False
192
+
193
+ # 7. Model Cache Status
194
+ from .hub import list_cached_models
195
+ cached = list_cached_models()
196
+ print(f"7. Cached GGUF Models: {len(cached)} model(s) available locally.")
197
+ for m in cached:
198
+ print(f" ↳ {m['name']} ({m['size_mb']} MB)")
199
+
200
+ print("=" * 65)
201
+ if all_passed:
202
+ print("🎉 All core diagnostics passed! You are ready to generate AI images.")
203
+ else:
204
+ print("⚠️ Some diagnostics need attention. Run 'termux-diffusion-install' to resolve.")
205
+ print("=" * 65)
206
+ return all_passed