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.
- termux_diffusion/__init__.py +74 -0
- termux_diffusion/cli.py +112 -0
- termux_diffusion/core.py +240 -0
- termux_diffusion/exceptions.py +35 -0
- termux_diffusion/hub.py +332 -0
- termux_diffusion/installer.py +206 -0
- termux_diffusion/platform.py +220 -0
- termux_diffusion/py.typed +1 -0
- termux_diffusion-1.0.0.dist-info/LICENSE +21 -0
- termux_diffusion-1.0.0.dist-info/METADATA +199 -0
- termux_diffusion-1.0.0.dist-info/RECORD +14 -0
- termux_diffusion-1.0.0.dist-info/WHEEL +5 -0
- termux_diffusion-1.0.0.dist-info/entry_points.txt +4 -0
- termux_diffusion-1.0.0.dist-info/top_level.txt +1 -0
termux_diffusion/hub.py
ADDED
|
@@ -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
|