termux-diffusion 1.3.1__tar.gz → 1.3.2__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.
- {termux_diffusion-1.3.1/termux_diffusion.egg-info → termux_diffusion-1.3.2}/PKG-INFO +10 -7
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/README.md +8 -6
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/pyproject.toml +7 -1
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/setup.py +5 -3
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/cli.py +6 -4
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/core.py +109 -33
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/downloader.py +2 -52
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/exceptions.py +6 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/hardware.py +172 -30
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/installer.py +33 -40
- termux_diffusion-1.3.2/termux_diffusion/locking.py +96 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/npu.py +5 -5
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/platform.py +15 -1
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/selftest.py +20 -7
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2/termux_diffusion.egg-info}/PKG-INFO +10 -7
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion.egg-info/SOURCES.txt +1 -0
- termux_diffusion-1.3.2/termux_diffusion.egg-info/requires.txt +1 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/tests/test_core.py +1 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/tests/test_presets.py +17 -0
- termux_diffusion-1.3.1/termux_diffusion/locking.py +0 -50
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/LICENSE +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/setup.cfg +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/__init__.py +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/_version.py +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/data/bootstrap-manifest.json +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/data/release-public-keys.json +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/data/validated-vulkan-profiles.json +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/hub.py +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/manifest.py +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/py.typed +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion.egg-info/dependency_links.txt +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion.egg-info/entry_points.txt +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion.egg-info/top_level.txt +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/tests/test_hardware.py +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/tests/test_hub.py +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/tests/test_installer.py +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/tests/test_npu.py +0 -0
- {termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/tests/test_platform.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termux-diffusion
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Production On-Device AI Image Generation Framework for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)
|
|
5
5
|
Home-page: https://github.com/uno-km/termux-diffusion
|
|
6
6
|
Author: uno-km (AMEVA Foundation)
|
|
@@ -28,6 +28,7 @@ Classifier: Topic :: Multimedia :: Graphics
|
|
|
28
28
|
Requires-Python: >=3.8
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
License-File: LICENSE
|
|
31
|
+
Requires-Dist: ameva-vulkan-runtime>=1.0.0
|
|
31
32
|
Dynamic: author
|
|
32
33
|
Dynamic: classifier
|
|
33
34
|
Dynamic: home-page
|
|
@@ -75,7 +76,7 @@ termux-setup-storage
|
|
|
75
76
|
|
|
76
77
|
# 2. Install Toolchains & Provision Native Engine
|
|
77
78
|
pkg update -y && pkg install python clang make cmake git termux-api wget vulkan-loader vulkan-headers vulkan-tools -y
|
|
78
|
-
pip install termux-diffusion && termux-diffusion
|
|
79
|
+
pip install termux-diffusion && termux-diffusion install
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
#### [Node.js] Node.js / TypeScript:
|
|
@@ -88,6 +89,11 @@ pkg update -y && pkg install nodejs-lts clang make cmake git termux-api wget vul
|
|
|
88
89
|
npm install -g termux-diffusion && npx termux-diffusion install
|
|
89
90
|
```
|
|
90
91
|
|
|
92
|
+
#### [Universal] One-Line Automated Bootstrap (curl):
|
|
93
|
+
```bash
|
|
94
|
+
curl -sL https://raw.githubusercontent.com/uno-km/termux-diffusion/main/scripts/install.sh | bash
|
|
95
|
+
```
|
|
96
|
+
|
|
91
97
|
---
|
|
92
98
|
|
|
93
99
|
### [Instant] Scenario 2: Instant Generation (Ready to Run)
|
|
@@ -416,12 +422,9 @@ adb shell "/system/bin/device_config put activity_manager max_phantom_processes
|
|
|
416
422
|
|
|
417
423
|
---
|
|
418
424
|
|
|
419
|
-
## 9. Disclaimer
|
|
425
|
+
## 9. Disclaimer
|
|
420
426
|
|
|
421
|
-
>
|
|
422
|
-
> *Termux-Diffusion is an independent open-source project developed for the Android Termux environment and is not officially affiliated with, endorsed by, or sponsored by the Termux project.*
|
|
423
|
-
>
|
|
424
|
-
> *(본 프로젝트는 안드로이드 Termux 환경을 위해 개발된 독립적인 오픈소스 라이브러리이며, Termux 공식 프로젝트와 직접적인 제휴 관계가 아닙니다.)*
|
|
427
|
+
> *This project is an independent open-source library developed for the Android Termux environment. It is not officially affiliated with, endorsed by, or sponsored by the Termux project, stability.ai, or Google LLC.*
|
|
425
428
|
|
|
426
429
|
---
|
|
427
430
|
|
|
@@ -39,7 +39,7 @@ termux-setup-storage
|
|
|
39
39
|
|
|
40
40
|
# 2. Install Toolchains & Provision Native Engine
|
|
41
41
|
pkg update -y && pkg install python clang make cmake git termux-api wget vulkan-loader vulkan-headers vulkan-tools -y
|
|
42
|
-
pip install termux-diffusion && termux-diffusion
|
|
42
|
+
pip install termux-diffusion && termux-diffusion install
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
#### [Node.js] Node.js / TypeScript:
|
|
@@ -52,6 +52,11 @@ pkg update -y && pkg install nodejs-lts clang make cmake git termux-api wget vul
|
|
|
52
52
|
npm install -g termux-diffusion && npx termux-diffusion install
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
#### [Universal] One-Line Automated Bootstrap (curl):
|
|
56
|
+
```bash
|
|
57
|
+
curl -sL https://raw.githubusercontent.com/uno-km/termux-diffusion/main/scripts/install.sh | bash
|
|
58
|
+
```
|
|
59
|
+
|
|
55
60
|
---
|
|
56
61
|
|
|
57
62
|
### [Instant] Scenario 2: Instant Generation (Ready to Run)
|
|
@@ -380,12 +385,9 @@ adb shell "/system/bin/device_config put activity_manager max_phantom_processes
|
|
|
380
385
|
|
|
381
386
|
---
|
|
382
387
|
|
|
383
|
-
## 9. Disclaimer
|
|
388
|
+
## 9. Disclaimer
|
|
384
389
|
|
|
385
|
-
>
|
|
386
|
-
> *Termux-Diffusion is an independent open-source project developed for the Android Termux environment and is not officially affiliated with, endorsed by, or sponsored by the Termux project.*
|
|
387
|
-
>
|
|
388
|
-
> *(본 프로젝트는 안드로이드 Termux 환경을 위해 개발된 독립적인 오픈소스 라이브러리이며, Termux 공식 프로젝트와 직접적인 제휴 관계가 아닙니다.)*
|
|
390
|
+
> *This project is an independent open-source library developed for the Android Termux environment. It is not officially affiliated with, endorsed by, or sponsored by the Termux project, stability.ai, or Google LLC.*
|
|
389
391
|
|
|
390
392
|
---
|
|
391
393
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "termux-diffusion"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.2"
|
|
8
8
|
description = "Production On-Device AI Image Generation Framework for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -40,6 +40,9 @@ keywords = [
|
|
|
40
40
|
"webui-alternative"
|
|
41
41
|
]
|
|
42
42
|
dynamic = ["classifiers"]
|
|
43
|
+
dependencies = [
|
|
44
|
+
"ameva-vulkan-runtime>=1.0.0",
|
|
45
|
+
]
|
|
43
46
|
|
|
44
47
|
[project.urls]
|
|
45
48
|
Homepage = "https://github.com/uno-km/termux-diffusion"
|
|
@@ -58,3 +61,6 @@ termux_diffusion = [
|
|
|
58
61
|
"data/*.json",
|
|
59
62
|
"data/*.pub"
|
|
60
63
|
]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
testpaths = ["tests"]
|
|
@@ -11,7 +11,7 @@ if os.path.exists(readme_path):
|
|
|
11
11
|
|
|
12
12
|
setup(
|
|
13
13
|
name="termux-diffusion",
|
|
14
|
-
version="1.3.
|
|
14
|
+
version="1.3.2",
|
|
15
15
|
description="Production On-Device AI Image Generation Framework for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)",
|
|
16
16
|
long_description=long_description,
|
|
17
17
|
long_description_content_type="text/markdown",
|
|
@@ -33,11 +33,13 @@ setup(
|
|
|
33
33
|
],
|
|
34
34
|
packages=find_packages(),
|
|
35
35
|
package_data={
|
|
36
|
-
"termux_diffusion": ["py.typed"],
|
|
36
|
+
"termux_diffusion": ["py.typed", "data/*.json", "data/*.pub"],
|
|
37
37
|
},
|
|
38
38
|
include_package_data=True,
|
|
39
39
|
python_requires=">=3.8",
|
|
40
|
-
install_requires=[
|
|
40
|
+
install_requires=[
|
|
41
|
+
"ameva-vulkan-runtime>=1.0.0",
|
|
42
|
+
],
|
|
41
43
|
entry_points={
|
|
42
44
|
"console_scripts": [
|
|
43
45
|
"termux-diffusion = termux_diffusion.cli:main",
|
|
@@ -4,16 +4,18 @@ import argparse
|
|
|
4
4
|
import sys
|
|
5
5
|
from typing import List, Optional
|
|
6
6
|
|
|
7
|
+
import logging as _logging
|
|
8
|
+
|
|
7
9
|
if hasattr(sys.stdout, "reconfigure"):
|
|
8
10
|
try:
|
|
9
11
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
10
|
-
except Exception:
|
|
11
|
-
|
|
12
|
+
except Exception as _e:
|
|
13
|
+
_logging.getLogger(__name__).debug("stdout reconfigure failed: %s", _e)
|
|
12
14
|
if hasattr(sys.stderr, "reconfigure"):
|
|
13
15
|
try:
|
|
14
16
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
-
except Exception:
|
|
16
|
-
|
|
17
|
+
except Exception as _e:
|
|
18
|
+
_logging.getLogger(__name__).debug("stderr reconfigure failed: %s", _e)
|
|
17
19
|
|
|
18
20
|
from .core import generate
|
|
19
21
|
from .hub import clear_cache, download_model, list_cached_models, list_presets
|
|
@@ -68,13 +68,15 @@ class GenerationResult:
|
|
|
68
68
|
import signal
|
|
69
69
|
|
|
70
70
|
def _safe_kill_process(proc: Optional[subprocess.Popen], timeout: float = 2.0) -> None:
|
|
71
|
-
"""Safely terminate and reap child process groups
|
|
71
|
+
"""Safely terminate and reap child process groups with granular exception handling and zero zombie leak."""
|
|
72
72
|
if proc is None:
|
|
73
73
|
return
|
|
74
|
+
|
|
74
75
|
try:
|
|
75
76
|
if proc.poll() is not None:
|
|
76
77
|
return
|
|
77
|
-
except
|
|
78
|
+
except (OSError, ValueError) as poll_err:
|
|
79
|
+
logger.debug("Process poll inspection note: %s", poll_err)
|
|
78
80
|
return
|
|
79
81
|
|
|
80
82
|
pid = getattr(proc, "pid", None)
|
|
@@ -83,12 +85,17 @@ def _safe_kill_process(proc: Optional[subprocess.Popen], timeout: float = 2.0) -
|
|
|
83
85
|
|
|
84
86
|
logger.warning("Initiating child process termination (PID: %s)...", pid)
|
|
85
87
|
|
|
86
|
-
# Step 1:
|
|
88
|
+
# Step 1: Graceful termination attempt (SIGTERM / proc.terminate)
|
|
87
89
|
try:
|
|
88
90
|
if sys.platform != "win32":
|
|
89
91
|
try:
|
|
90
|
-
os.
|
|
91
|
-
|
|
92
|
+
pgid = os.getpgid(pid)
|
|
93
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
94
|
+
except ProcessLookupError:
|
|
95
|
+
logger.debug("Process group (PGID: %s) already exited.", pid)
|
|
96
|
+
return
|
|
97
|
+
except (OSError, PermissionError) as pg_err:
|
|
98
|
+
logger.debug("killpg SIGTERM note (PGID: %s): %s, falling back to proc.terminate()", pid, pg_err)
|
|
92
99
|
proc.terminate()
|
|
93
100
|
else:
|
|
94
101
|
proc.terminate()
|
|
@@ -103,15 +110,25 @@ def _safe_kill_process(proc: Optional[subprocess.Popen], timeout: float = 2.0) -
|
|
|
103
110
|
pid,
|
|
104
111
|
timeout,
|
|
105
112
|
)
|
|
106
|
-
except
|
|
107
|
-
logger.debug("
|
|
113
|
+
except ProcessLookupError:
|
|
114
|
+
logger.debug("Child process (PID: %s) already terminated before SIGTERM dispatch.", pid)
|
|
115
|
+
return
|
|
116
|
+
except PermissionError as perm_err:
|
|
117
|
+
logger.warning("Permission denied while sending SIGTERM to PID %s: %s", pid, perm_err)
|
|
118
|
+
except OSError as os_err:
|
|
119
|
+
logger.warning("OS error during SIGTERM dispatch to PID %s: %s", pid, os_err)
|
|
108
120
|
|
|
109
|
-
# Step 2: Forceful SIGKILL
|
|
121
|
+
# Step 2: Forceful termination (SIGKILL / proc.kill)
|
|
110
122
|
try:
|
|
111
123
|
if sys.platform != "win32":
|
|
112
124
|
try:
|
|
113
|
-
os.
|
|
114
|
-
|
|
125
|
+
pgid = os.getpgid(pid)
|
|
126
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
127
|
+
except ProcessLookupError:
|
|
128
|
+
logger.debug("Process group (PGID: %s) already terminated.", pid)
|
|
129
|
+
return
|
|
130
|
+
except (OSError, PermissionError) as pg_err:
|
|
131
|
+
logger.debug("killpg SIGKILL note (PGID: %s): %s, falling back to proc.kill()", pid, pg_err)
|
|
115
132
|
proc.kill()
|
|
116
133
|
else:
|
|
117
134
|
proc.kill()
|
|
@@ -121,8 +138,12 @@ def _safe_kill_process(proc: Optional[subprocess.Popen], timeout: float = 2.0) -
|
|
|
121
138
|
logger.debug("Child process (PID: %s) forcefully terminated and reaped.", pid)
|
|
122
139
|
except subprocess.TimeoutExpired:
|
|
123
140
|
logger.error("Child process (PID: %s) did not exit after SIGKILL (kernel D-state or pending I/O).", pid)
|
|
124
|
-
except
|
|
125
|
-
logger.
|
|
141
|
+
except ProcessLookupError:
|
|
142
|
+
logger.debug("Child process (PID: %s) already reaped before SIGKILL.", pid)
|
|
143
|
+
except PermissionError as perm_err:
|
|
144
|
+
logger.error("Permission denied while sending SIGKILL to PID %s: %s", pid, perm_err)
|
|
145
|
+
except OSError as os_err:
|
|
146
|
+
logger.error("OS error during SIGKILL dispatch to PID %s: %s", pid, os_err)
|
|
126
147
|
|
|
127
148
|
|
|
128
149
|
DEFAULT_QUALITY_GUARD_NEGATIVE_PROMPT = "lowres, bad quality, blur, deformed, distorted, extra limbs, artifacts"
|
|
@@ -159,7 +180,7 @@ def generate(
|
|
|
159
180
|
prompt: str,
|
|
160
181
|
model: str = "realistic",
|
|
161
182
|
negative_prompt: Optional[str] = None,
|
|
162
|
-
device: str = "
|
|
183
|
+
device: str = "auto",
|
|
163
184
|
steps: Optional[int] = None,
|
|
164
185
|
cfg_scale: Optional[float] = None,
|
|
165
186
|
width: int = 512,
|
|
@@ -193,7 +214,7 @@ def generate(
|
|
|
193
214
|
prompt: Detailed text description of the desired image.
|
|
194
215
|
model: Preset keyword ('realistic', 'speed', 'sdxs', 'turbo', 'anime'), custom repo ('org/repo/file.gguf'), direct URL, or path to .gguf file.
|
|
195
216
|
negative_prompt: Optional negative text guidance describing elements to avoid (default: None).
|
|
196
|
-
device: Computing device ('
|
|
217
|
+
device: Computing device ('auto', 'gpu', 'vulkan', 'cpu', 'opencl', 'npu', 'tpu'). Default is 'auto'.
|
|
197
218
|
steps: Number of denoising steps (default determined by preset, e.g. 10).
|
|
198
219
|
cfg_scale: Classifier-Free Guidance scale (default determined by preset, e.g. 4.0).
|
|
199
220
|
width: Output image width in pixels (default: 512).
|
|
@@ -224,14 +245,23 @@ def generate(
|
|
|
224
245
|
if not prompt or not str(prompt).strip():
|
|
225
246
|
raise ValueError("Prompt must not be empty.")
|
|
226
247
|
|
|
227
|
-
device_mode = device.lower().strip()
|
|
248
|
+
device_mode = str(device or "auto").lower().strip()
|
|
228
249
|
if device_mode not in ("cpu", "gpu", "opencl", "vulkan", "npu", "tpu", "auto"):
|
|
229
|
-
raise ValueError(f"Invalid device '{device}'. Options: '
|
|
250
|
+
raise ValueError(f"Invalid device '{device}'. Options: 'auto', 'gpu', 'vulkan', 'cpu', 'opencl', 'npu', 'tpu'.")
|
|
230
251
|
|
|
231
252
|
# Resolve device to actual available backend using hardware probing
|
|
232
|
-
from .hardware import resolve_device_backend, get_sd_cli_gpu_args
|
|
253
|
+
from .hardware import resolve_device_backend, get_sd_cli_gpu_args, get_profile_gating_manager
|
|
233
254
|
effective_device, ngl_layers = resolve_device_backend(device_mode)
|
|
234
255
|
|
|
256
|
+
# 0. Runtime Profile & Model Gating Validation (Zero Test Illusion)
|
|
257
|
+
gating_mgr = get_profile_gating_manager()
|
|
258
|
+
is_safe, gate_reason = gating_mgr.validate_execution(model, effective_device)
|
|
259
|
+
if not is_safe:
|
|
260
|
+
from .exceptions import PlatformNotSupportedError
|
|
261
|
+
raise PlatformNotSupportedError(
|
|
262
|
+
f"[termux-diffusion] [GATING-BLOCKED] {gate_reason}"
|
|
263
|
+
)
|
|
264
|
+
|
|
235
265
|
# 1. Pre-flight Memory Safety Inspection (Informational only, non-blocking)
|
|
236
266
|
if low_ram_guard:
|
|
237
267
|
safe, msg = check_memory_safety(required_mb=1200)
|
|
@@ -450,8 +480,9 @@ def generate(
|
|
|
450
480
|
)
|
|
451
481
|
cmd.extend(["--taesd", str(effective_taesd_path)])
|
|
452
482
|
|
|
453
|
-
# 5.1 Configure Environment with companion library search paths
|
|
483
|
+
# 5.1 Configure Environment with companion library search paths (Bionic priority on Android)
|
|
454
484
|
env = os.environ.copy()
|
|
485
|
+
bionic_sys_dirs = ["/system/lib64", "/vendor/lib64", "/vendor/lib64/egl"]
|
|
455
486
|
lib_dirs = [
|
|
456
487
|
str(sd_cli.parent),
|
|
457
488
|
str(sd_cli.parent.parent / "lib"),
|
|
@@ -460,10 +491,45 @@ def generate(
|
|
|
460
491
|
str(Path.home() / ".cache" / "termux-diffusion" / "staging" / "lib"),
|
|
461
492
|
]
|
|
462
493
|
cur_ld = env.get("LD_LIBRARY_PATH", "")
|
|
463
|
-
valid_dirs = [d for d in lib_dirs if Path(d).is_dir()]
|
|
494
|
+
valid_dirs = [d for d in bionic_sys_dirs + lib_dirs if Path(d).is_dir()]
|
|
464
495
|
if valid_dirs:
|
|
465
496
|
env["LD_LIBRARY_PATH"] = ":".join(valid_dirs + ([cur_ld] if cur_ld else []))
|
|
466
497
|
|
|
498
|
+
# 5.2 ameva-vulkan-runtime DiffusionAdapter Binding (Vulkan / GPU mode)
|
|
499
|
+
if effective_device in ("vulkan", "gpu") or device_mode in ("vulkan", "gpu"):
|
|
500
|
+
try:
|
|
501
|
+
import ameva_vulkan_runtime as avr
|
|
502
|
+
from ameva_vulkan_runtime.adapters import DiffusionAdapter
|
|
503
|
+
|
|
504
|
+
avr_ctx = avr.get_or_create_context(device_mode)
|
|
505
|
+
report = getattr(avr_ctx, "doctor", avr.Doctor()).run_self_test(verbose=False)
|
|
506
|
+
|
|
507
|
+
class EngineProxy:
|
|
508
|
+
def __init__(self, hw):
|
|
509
|
+
self.hw_profile = hw
|
|
510
|
+
|
|
511
|
+
from .hardware import detect_hardware_profile
|
|
512
|
+
engine_proxy = EngineProxy(detect_hardware_profile())
|
|
513
|
+
binding = DiffusionAdapter.bind(engine_proxy, report)
|
|
514
|
+
|
|
515
|
+
cfg = getattr(binding, "config", {})
|
|
516
|
+
if cfg.get("unet_tiling") and "--vae-tiling" not in cmd:
|
|
517
|
+
cmd.append("--vae-tiling")
|
|
518
|
+
|
|
519
|
+
logger.info(
|
|
520
|
+
"[termux-diffusion] DiffusionAdapter bound: backend=%s, status=%s",
|
|
521
|
+
cfg.get("backend", "vulkan"),
|
|
522
|
+
getattr(binding.status, "name", str(binding.status))
|
|
523
|
+
)
|
|
524
|
+
except Exception as e:
|
|
525
|
+
if device_mode in ("gpu", "vulkan") or strict_vulkan:
|
|
526
|
+
from .exceptions import PlatformNotSupportedError
|
|
527
|
+
raise PlatformNotSupportedError(
|
|
528
|
+
f"[termux-diffusion] [FAIL-FAST] Vulkan execution requested (device='{device_mode}'), "
|
|
529
|
+
f"but hardware validation failed: {e}"
|
|
530
|
+
) from e
|
|
531
|
+
avr_ctx = None
|
|
532
|
+
|
|
467
533
|
logger.info("Executing diffusion inference: %s", " ".join(cmd[:6]) + " ...")
|
|
468
534
|
print(f"[termux-diffusion] Processing inference with model='{model}' (steps={steps}, threads={threads}, device={device_mode})...")
|
|
469
535
|
|
|
@@ -489,23 +555,33 @@ def generate(
|
|
|
489
555
|
_safe_kill_process(process)
|
|
490
556
|
raise TermuxDiffusionError("Inference cancelled by caller.")
|
|
491
557
|
|
|
558
|
+
init_logs = []
|
|
492
559
|
recent_logs = deque(maxlen=20)
|
|
493
|
-
# Stream real-time progress to terminal
|
|
560
|
+
# Stream real-time progress to terminal with FD safety
|
|
494
561
|
if process.stdout:
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
562
|
+
try:
|
|
563
|
+
for line in process.stdout:
|
|
564
|
+
if _cancel_event and _cancel_event.is_set():
|
|
565
|
+
_safe_kill_process(process)
|
|
566
|
+
raise TermuxDiffusionError("Inference cancelled by caller.")
|
|
567
|
+
line_str = line.strip()
|
|
568
|
+
if line_str:
|
|
569
|
+
if len(init_logs) < 50 or "ggml_vulkan" in line_str or "error" in line_str.lower() or "failed" in line_str.lower():
|
|
570
|
+
init_logs.append(line_str)
|
|
571
|
+
recent_logs.append(line_str)
|
|
572
|
+
if "step" in line_str.lower() or "%" in line_str or "sampling" in line_str.lower():
|
|
573
|
+
print(f" > {line_str}")
|
|
574
|
+
else:
|
|
575
|
+
logger.debug("sd-cli: %s", line_str)
|
|
576
|
+
finally:
|
|
577
|
+
try:
|
|
578
|
+
process.stdout.close()
|
|
579
|
+
except Exception:
|
|
580
|
+
pass
|
|
506
581
|
|
|
507
582
|
process.wait(timeout=timeout)
|
|
508
|
-
|
|
583
|
+
all_critical_logs = init_logs + list(recent_logs)
|
|
584
|
+
if (strict_vulkan or device_mode in ("vulkan", "gpu")) and any("ggml_vulkan: No devices found" in l for l in all_critical_logs):
|
|
509
585
|
raise TermuxDiffusionError(
|
|
510
586
|
"Strict Vulkan execution mode requested (--strict-vulkan or device='vulkan'), but Vulkan physical device discovery failed: 'ggml_vulkan: No devices found'."
|
|
511
587
|
)
|
|
@@ -582,7 +658,7 @@ def generate(
|
|
|
582
658
|
prompt=prompt,
|
|
583
659
|
negative_prompt=negative_prompt,
|
|
584
660
|
model=model,
|
|
585
|
-
device=
|
|
661
|
+
device=effective_device,
|
|
586
662
|
steps=steps,
|
|
587
663
|
cfg_scale=cfg_scale,
|
|
588
664
|
width=width,
|
|
@@ -8,63 +8,13 @@ import logging
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
|
-
from .exceptions import TermuxDiffusionError
|
|
11
|
+
from .exceptions import DownloadError, InstallLockError, TermuxDiffusionError
|
|
12
|
+
from .locking import InstallLock
|
|
12
13
|
from .manifest import verify_file_sha256
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger("termux_diffusion.downloader")
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
class InstallLockError(TermuxDiffusionError):
|
|
18
|
-
"""Raised when install lock cannot be acquired due to concurrent process."""
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class DownloadError(TermuxDiffusionError):
|
|
23
|
-
"""Raised when file download, size, or SHA-256 verification fails."""
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class InstallLock:
|
|
28
|
-
"""File lock manager to prevent concurrent installation processes."""
|
|
29
|
-
def __init__(self, lock_file: Path, timeout_sec: float = 5.0):
|
|
30
|
-
self.lock_file = lock_file
|
|
31
|
-
self.timeout_sec = timeout_sec
|
|
32
|
-
self._acquired = False
|
|
33
|
-
|
|
34
|
-
def acquire(self):
|
|
35
|
-
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
-
start = time.time()
|
|
37
|
-
while time.time() - start < self.timeout_sec:
|
|
38
|
-
try:
|
|
39
|
-
# O_CREAT | O_EXCL atomic creation
|
|
40
|
-
fd = os.open(str(self.lock_file), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
41
|
-
os.write(fd, f"pid={os.getpid()}\n".encode("utf-8"))
|
|
42
|
-
os.close(fd)
|
|
43
|
-
self._acquired = True
|
|
44
|
-
return
|
|
45
|
-
except FileExistsError:
|
|
46
|
-
time.sleep(0.2)
|
|
47
|
-
raise InstallLockError(
|
|
48
|
-
f"E_CONCURRENCY_LOCK: Another installation process holds the lock file at {self.lock_file}. "
|
|
49
|
-
f"If stale, remove {self.lock_file} and retry."
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
def release(self):
|
|
53
|
-
if self._acquired and self.lock_file.exists():
|
|
54
|
-
try:
|
|
55
|
-
self.lock_file.unlink(missing_ok=True)
|
|
56
|
-
except Exception as e:
|
|
57
|
-
logger.warning("Failed deleting lock file: %s", e)
|
|
58
|
-
self._acquired = False
|
|
59
|
-
|
|
60
|
-
def __enter__(self):
|
|
61
|
-
self.acquire()
|
|
62
|
-
return self
|
|
63
|
-
|
|
64
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
65
|
-
self.release()
|
|
66
|
-
|
|
67
|
-
|
|
68
18
|
def atomic_download_file(
|
|
69
19
|
url: str,
|
|
70
20
|
dest_path: Path,
|
|
@@ -61,6 +61,12 @@ class ModelDownloadError(TermuxDiffusionError):
|
|
|
61
61
|
pass
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
class DownloadError(TermuxDiffusionError):
|
|
65
|
+
"""Raised when file download, size, or SHA-256 verification fails."""
|
|
66
|
+
def __init__(self, message: str, code: str = ErrorCode.ARTIFACT_DOWNLOAD):
|
|
67
|
+
super().__init__(message, code=code)
|
|
68
|
+
|
|
69
|
+
|
|
64
70
|
class ProvisioningError(TermuxDiffusionError):
|
|
65
71
|
"""Raised when the native C++ engine (sd-cli) fails to build or provision."""
|
|
66
72
|
def __init__(self, message: str, code: str = ErrorCode.SOURCE_BUILD):
|
|
@@ -67,13 +67,18 @@ class HardwareProfile:
|
|
|
67
67
|
# 1. CPU & GPU Feature Detection
|
|
68
68
|
# ------------------------------------------------------------------------------
|
|
69
69
|
|
|
70
|
+
# [중요] Vulkan ICD 탐색 순서: Android Bionic ICD 최우선.
|
|
71
|
+
# Termux Mesa($PREFIX/lib/libvulkan.so)를 시스템 ICD보다 먼저 로드하면
|
|
72
|
+
# Bionic linker 이중 dispatch 테이블 충돌로 SIGABRT 가 발생합니다.
|
|
73
|
+
# ameva-vulkan-runtime 의 ICD 탐색 정책과 동일하게 유지합니다.
|
|
70
74
|
_VULKAN_LIB_SEARCH_PATHS = [
|
|
71
|
-
|
|
72
|
-
os.path.join(os.environ.get("PREFIX", "/data/data/com.termux/files/usr"), "lib64", "libvulkan.so"),
|
|
73
|
-
"/system/lib64/libvulkan.so",
|
|
75
|
+
"/system/lib64/libvulkan.so", # Android Bionic ICD (최우선 — A35/S25/S21 모두 존재)
|
|
74
76
|
"/system/lib/libvulkan.so",
|
|
75
77
|
"/vendor/lib64/libvulkan.so",
|
|
76
78
|
"/vendor/lib/libvulkan.so",
|
|
79
|
+
# Termux Mesa: 시스템 ICD 가 없는 순수 Linux/PRoot 환경 전용 fallback
|
|
80
|
+
os.path.join(os.environ.get("PREFIX", "/data/data/com.termux/files/usr"), "lib64", "libvulkan.so"),
|
|
81
|
+
os.path.join(os.environ.get("PREFIX", "/data/data/com.termux/files/usr"), "lib", "libvulkan.so"),
|
|
77
82
|
]
|
|
78
83
|
|
|
79
84
|
_OPENCL_LIB_SEARCH_PATHS = [
|
|
@@ -87,6 +92,18 @@ _OPENCL_LIB_SEARCH_PATHS = [
|
|
|
87
92
|
]
|
|
88
93
|
|
|
89
94
|
|
|
95
|
+
def _find_vulkan_driver_path() -> Optional[str]:
|
|
96
|
+
"""Find the first genuinely accessible Vulkan driver library on the filesystem."""
|
|
97
|
+
for p in _VULKAN_LIB_SEARCH_PATHS:
|
|
98
|
+
p_obj = Path(p)
|
|
99
|
+
try:
|
|
100
|
+
if p_obj.is_file() and p_obj.stat().st_size >= 1024:
|
|
101
|
+
return str(p_obj.resolve())
|
|
102
|
+
except (OSError, PermissionError):
|
|
103
|
+
continue
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
90
107
|
def _read_cpuinfo_features() -> List[str]:
|
|
91
108
|
"""Parse /proc/cpuinfo to extract ARM CPU feature flags."""
|
|
92
109
|
cpuinfo_path = Path("/proc/cpuinfo")
|
|
@@ -106,8 +123,13 @@ def _read_cpuinfo_features() -> List[str]:
|
|
|
106
123
|
return []
|
|
107
124
|
|
|
108
125
|
|
|
126
|
+
from .platform import is_android_termux
|
|
127
|
+
|
|
128
|
+
|
|
109
129
|
def _detect_soc_name() -> str:
|
|
110
130
|
"""Identify the SoC model from Android system properties."""
|
|
131
|
+
if not is_android_termux():
|
|
132
|
+
return "Unknown"
|
|
111
133
|
prop_keys = [
|
|
112
134
|
"ro.soc.model",
|
|
113
135
|
"ro.chipname",
|
|
@@ -132,8 +154,30 @@ def _detect_soc_name() -> str:
|
|
|
132
154
|
return "Unknown"
|
|
133
155
|
|
|
134
156
|
|
|
157
|
+
def _detect_device_model() -> str:
|
|
158
|
+
"""Identify Android device model for hardware profile matching."""
|
|
159
|
+
if not is_android_termux():
|
|
160
|
+
return "Unknown"
|
|
161
|
+
for prop in ["ro.product.model", "ro.product.device", "ro.build.product"]:
|
|
162
|
+
try:
|
|
163
|
+
result = subprocess.run(
|
|
164
|
+
["getprop", prop],
|
|
165
|
+
capture_output=True,
|
|
166
|
+
text=True,
|
|
167
|
+
timeout=2.0,
|
|
168
|
+
)
|
|
169
|
+
val = result.stdout.strip()
|
|
170
|
+
if val and val != "unknown":
|
|
171
|
+
return val
|
|
172
|
+
except Exception:
|
|
173
|
+
continue
|
|
174
|
+
return "Unknown"
|
|
175
|
+
|
|
176
|
+
|
|
135
177
|
def _detect_gpu_name() -> str:
|
|
136
178
|
"""Identify the GPU model from Android properties and hardware nodes."""
|
|
179
|
+
if not is_android_termux():
|
|
180
|
+
return "Unknown"
|
|
137
181
|
# 1. Check Adreno kgsl sysfs node
|
|
138
182
|
kgsl_model = Path("/sys/class/kgsl/kgsl-3d0/gpu_model")
|
|
139
183
|
if kgsl_model.exists():
|
|
@@ -166,33 +210,34 @@ def _detect_gpu_name() -> str:
|
|
|
166
210
|
|
|
167
211
|
|
|
168
212
|
def _probe_vulkan_driver() -> Optional[GPUDriverInfo]:
|
|
169
|
-
"""Probe for a usable Vulkan driver
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
213
|
+
"""Probe for a usable Vulkan driver via ameva-vulkan-runtime SSOT."""
|
|
214
|
+
try:
|
|
215
|
+
import ameva_vulkan_runtime as avr
|
|
216
|
+
report = avr.Doctor().run_self_test(verbose=False)
|
|
217
|
+
if report.overall_success or report.recommended_backend in ("vulkan", "vulkan_driver_only") or getattr(report, "passed_stages", 0) >= 7:
|
|
218
|
+
return GPUDriverInfo(
|
|
219
|
+
name=f"Vulkan Driver ({report.device_name})",
|
|
220
|
+
vendor=report.device_name,
|
|
221
|
+
api=f"Vulkan {getattr(report, 'driver_version', '1.3')}",
|
|
222
|
+
library_path=getattr(report, "loader_path", "") or _find_vulkan_driver_path() or "/system/lib64/libvulkan.so",
|
|
223
|
+
version=getattr(report, "driver_version", ""),
|
|
224
|
+
usable=True,
|
|
225
|
+
)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.debug("[termux-diffusion] ameva-vulkan-runtime probe exception: %s", e)
|
|
228
|
+
|
|
229
|
+
# 안전 폴백: 시스템 기본 Bionic 경로 검사
|
|
230
|
+
detected_path = _find_vulkan_driver_path()
|
|
231
|
+
if detected_path and Path(detected_path).is_file():
|
|
232
|
+
gpu_name = _detect_gpu_name()
|
|
233
|
+
return GPUDriverInfo(
|
|
234
|
+
name=f"Vulkan Driver ({gpu_name})",
|
|
235
|
+
vendor=gpu_name,
|
|
236
|
+
api="Vulkan",
|
|
237
|
+
library_path=detected_path,
|
|
238
|
+
version="",
|
|
239
|
+
usable=True,
|
|
240
|
+
)
|
|
196
241
|
return None
|
|
197
242
|
|
|
198
243
|
|
|
@@ -428,3 +473,100 @@ def format_hardware_report(profile: HardwareProfile) -> str:
|
|
|
428
473
|
lines.append(f"CMake Build Flags: {' '.join(profile.cmake_extra_flags)}")
|
|
429
474
|
lines.append("=" * 40)
|
|
430
475
|
return "\n".join(lines)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# ------------------------------------------------------------------------------
|
|
479
|
+
# 4. Profile Gating Manager (Runtime Model/Preset Safety)
|
|
480
|
+
# ------------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
import json
|
|
483
|
+
|
|
484
|
+
@dataclass
|
|
485
|
+
class ProfileGatingManager:
|
|
486
|
+
"""Manages runtime matching against validated-vulkan-profiles.json for safe device/model execution."""
|
|
487
|
+
profiles: List[Dict] = field(default_factory=list)
|
|
488
|
+
|
|
489
|
+
@classmethod
|
|
490
|
+
def load_from_json(cls, json_path: Optional[Path] = None) -> "ProfileGatingManager":
|
|
491
|
+
if json_path is None:
|
|
492
|
+
json_path = Path(__file__).parent / "data" / "validated-vulkan-profiles.json"
|
|
493
|
+
if json_path.is_file():
|
|
494
|
+
try:
|
|
495
|
+
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
496
|
+
return cls(profiles=data.get("profiles", []))
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.warning("Failed to load validated-vulkan-profiles.json: %s", e)
|
|
499
|
+
return cls(profiles=[])
|
|
500
|
+
|
|
501
|
+
def find_matching_profile(self, model_name: str, soc_name: str = "", gpu_name: str = "") -> Optional[Dict]:
|
|
502
|
+
model_clean = model_name.strip().upper()
|
|
503
|
+
soc_clean = soc_name.strip().upper()
|
|
504
|
+
gpu_clean = gpu_name.strip().upper()
|
|
505
|
+
|
|
506
|
+
for prof in self.profiles:
|
|
507
|
+
target_model = prof.get("device_model", "").strip().upper()
|
|
508
|
+
if target_model and (target_model == model_clean or target_model in model_clean or model_clean in target_model):
|
|
509
|
+
return prof
|
|
510
|
+
aliases = [a.strip().upper() for a in prof.get("device_aliases", [])]
|
|
511
|
+
if any(a == model_clean or a in model_clean for a in aliases):
|
|
512
|
+
return prof
|
|
513
|
+
|
|
514
|
+
# Fallback matching by SoC / GPU
|
|
515
|
+
for prof in self.profiles:
|
|
516
|
+
prof_soc = prof.get("soc", "").strip().upper()
|
|
517
|
+
prof_gpu = prof.get("gpu", "").strip().upper()
|
|
518
|
+
if prof_soc and soc_clean and (prof_soc in soc_clean or soc_clean in prof_soc):
|
|
519
|
+
return prof
|
|
520
|
+
if prof_gpu and gpu_clean and (prof_gpu in gpu_clean or gpu_clean in prof_gpu):
|
|
521
|
+
return prof
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
def validate_execution(self, preset_or_model: str, device: str) -> Tuple[bool, Optional[str]]:
|
|
525
|
+
"""Validate if the requested model/preset and device backend are safe for current hardware."""
|
|
526
|
+
device_clean = device.lower().strip()
|
|
527
|
+
if device_clean not in ("vulkan", "gpu"):
|
|
528
|
+
return True, None
|
|
529
|
+
|
|
530
|
+
dev_model = _detect_device_model()
|
|
531
|
+
soc_name = _detect_soc_name()
|
|
532
|
+
gpu_name = _detect_gpu_name()
|
|
533
|
+
matched = self.find_matching_profile(dev_model, soc_name, gpu_name)
|
|
534
|
+
if not matched:
|
|
535
|
+
return True, None
|
|
536
|
+
|
|
537
|
+
# 1. Check blocked list
|
|
538
|
+
blocked = matched.get("blocked", [])
|
|
539
|
+
preset_clean = preset_or_model.lower().strip()
|
|
540
|
+
for blk in blocked:
|
|
541
|
+
fam = blk.get("model_family", "").lower()
|
|
542
|
+
backend = blk.get("backend", "").lower()
|
|
543
|
+
if backend in (device_clean, "all") and fam in preset_clean:
|
|
544
|
+
reason = blk.get("reason", "Incompatible hardware backend configuration")
|
|
545
|
+
return False, (
|
|
546
|
+
f"Model '{preset_or_model}' is blocked on device '{matched.get('device_model')}' "
|
|
547
|
+
f"under backend '{device_clean}' due to: {reason}."
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# 2. Check preset gating if preset exists in profile
|
|
551
|
+
presets = matched.get("presets", {})
|
|
552
|
+
if preset_clean in presets:
|
|
553
|
+
p_info = presets[preset_clean]
|
|
554
|
+
if p_info.get("status") == "pending_device_validation" and not p_info.get("auto_activation", True):
|
|
555
|
+
return False, (
|
|
556
|
+
f"Preset '{preset_clean}' is currently pending device validation on '{matched.get('device_model')}' "
|
|
557
|
+
f"and auto_activation is disabled."
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
return True, None
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
_gating_manager: Optional[ProfileGatingManager] = None
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def get_profile_gating_manager() -> ProfileGatingManager:
|
|
567
|
+
"""Get singleton instance of ProfileGatingManager."""
|
|
568
|
+
global _gating_manager
|
|
569
|
+
if _gating_manager is None:
|
|
570
|
+
_gating_manager = ProfileGatingManager.load_from_json()
|
|
571
|
+
return _gating_manager
|
|
572
|
+
|
|
@@ -85,32 +85,52 @@ def fetch_prebuilt_binary(backend: str = "auto", install_mode: str = "prebuilt-f
|
|
|
85
85
|
lock_file = get_default_cache_dir() / "install.lock"
|
|
86
86
|
|
|
87
87
|
with InstallLock(lock_file, timeout_sec=5.0):
|
|
88
|
-
# 1. Try Vulkan Prebuilt (Multi-SoC Aware: Adreno vs Mali)
|
|
88
|
+
# 1. Try Vulkan Prebuilt (Multi-SoC Aware: Adreno vs Mali via validated profiles)
|
|
89
89
|
if backend in ("auto", "vulkan"):
|
|
90
90
|
vulkan_bin = bin_dir / "sd-cli-vulkan"
|
|
91
91
|
print("[termux-diffusion] Attempting Prebuilt Vulkan Engine installation...")
|
|
92
92
|
try:
|
|
93
93
|
if not vulkan_bin.is_file():
|
|
94
|
-
from .hardware import detect_hardware_profile
|
|
94
|
+
from .hardware import detect_hardware_profile, get_profile_gating_manager, _detect_device_model
|
|
95
95
|
hw = detect_hardware_profile()
|
|
96
|
-
|
|
96
|
+
gating_mgr = get_profile_gating_manager()
|
|
97
|
+
dev_model = _detect_device_model()
|
|
98
|
+
matched = gating_mgr.find_matching_profile(dev_model, hw.soc_name, hw.gpu_name)
|
|
97
99
|
|
|
98
|
-
if
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
if matched and matched.get("package_tarball") and matched.get("package_sha256"):
|
|
101
|
+
pkg_tag = matched.get("package_tag", "v1.3.1-vulkan-experimental")
|
|
102
|
+
pkg_tarball = matched.get("package_tarball")
|
|
103
|
+
pkg_url = f"https://github.com/uno-km/termux-diffusion/releases/download/{pkg_tag}/{pkg_tarball}"
|
|
104
|
+
pkg_sha256 = matched.get("package_sha256")
|
|
102
105
|
else:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
is_mali = "mali" in hw.gpu_name.lower() or "exynos" in hw.soc_name.lower()
|
|
107
|
+
if is_mali:
|
|
108
|
+
pkg_url = "https://github.com/uno-km/termux-diffusion/releases/download/v1.3.1-vulkan-mali-experimental/termux-diffusion-vulkan-prebuilt-v1.3.1-android-arm64-mali-compat-v2.tar.gz"
|
|
109
|
+
pkg_sha256 = "65e4e305241b22385313e386afbcd12722061041280d00a44dfdc3ff23aa17b8"
|
|
110
|
+
else:
|
|
111
|
+
pkg_url = "https://github.com/uno-km/termux-diffusion/releases/download/v1.3.1-vulkan-experimental/termux-diffusion-vulkan-prebuilt-v1.3.1-android-arm64-adreno.tar.gz"
|
|
112
|
+
pkg_sha256 = "d1f0a2656a33d0929cfd3335e01feeabf9c3a1e34a0ae0eacc04ddb3701ece92"
|
|
106
113
|
|
|
107
114
|
tar_dest = bin_dir / "vulkan-prebuilt.tar.gz"
|
|
108
115
|
try:
|
|
109
116
|
atomic_download_file(pkg_url, tar_dest, expected_sha256=pkg_sha256)
|
|
110
117
|
import tarfile
|
|
118
|
+
extract_target = get_default_cache_dir()
|
|
111
119
|
with tarfile.open(tar_dest, "r:gz") as tar:
|
|
112
|
-
|
|
120
|
+
# 경로 탈출(Directory Traversal) 방지 검증
|
|
121
|
+
for member in tar.getmembers():
|
|
122
|
+
member_path = os.path.realpath(
|
|
123
|
+
os.path.join(extract_target, member.name)
|
|
124
|
+
)
|
|
125
|
+
if not member_path.startswith(os.path.realpath(extract_target)):
|
|
126
|
+
raise ProvisioningError(
|
|
127
|
+
f"[termux-diffusion] E_TAR_PATH_ESCAPE: tarball 내 "
|
|
128
|
+
f"경로 탈출 시도가 감지되어 추출을 중단했습니다: {member.name}"
|
|
129
|
+
)
|
|
130
|
+
tar.extractall(path=extract_target)
|
|
113
131
|
tar_dest.unlink(missing_ok=True)
|
|
132
|
+
except ProvisioningError:
|
|
133
|
+
raise
|
|
114
134
|
except Exception as dl_err:
|
|
115
135
|
logger.debug("Vulkan prebuilt download skipped/failed: %s", dl_err)
|
|
116
136
|
|
|
@@ -302,42 +322,15 @@ def provision_engine(
|
|
|
302
322
|
except Exception as e:
|
|
303
323
|
logger.warning("ggml-vulkan patch note: %s", e)
|
|
304
324
|
|
|
305
|
-
# Bionic
|
|
325
|
+
# Bionic Path Fix: Patch vulkan-shaders-gen.cpp for Android Termux temporary directory
|
|
306
326
|
vulkan_gen = repo_dir / "ggml" / "src" / "ggml-vulkan" / "vulkan-shaders" / "vulkan-shaders-gen.cpp"
|
|
307
327
|
if vulkan_gen.is_file():
|
|
308
328
|
try:
|
|
309
329
|
gen_content = vulkan_gen.read_text(encoding="utf-8")
|
|
310
330
|
if 'output_dir = "/tmp"' in gen_content:
|
|
311
331
|
gen_content = gen_content.replace('output_dir = "/tmp"', 'output_dir = "."')
|
|
312
|
-
|
|
313
|
-
old_err_block = (
|
|
314
|
-
" int exit_code = execute_command(cmd, stdout_str, stderr_str);\n"
|
|
315
|
-
" if (exit_code != 0 || !stderr_str.empty()) {\n"
|
|
316
|
-
' std::cerr << "cannot compile " << name << " (exit code " << exit_code << ")\\n\\n";\n'
|
|
317
|
-
" for (const auto& part : cmd) {\n"
|
|
318
|
-
' std::cerr << part << " ";\n'
|
|
319
|
-
" }\n"
|
|
320
|
-
' std::cerr << "\\n\\n" << stderr_str << std::endl;\n'
|
|
321
|
-
" compile_failed = true;\n"
|
|
322
|
-
" return;\n"
|
|
323
|
-
" }"
|
|
324
|
-
)
|
|
325
|
-
new_err_block = (
|
|
326
|
-
" int exit_code = execute_command(cmd, stdout_str, stderr_str);\n"
|
|
327
|
-
" if (exit_code != 0 || !stderr_str.empty()) {\n"
|
|
328
|
-
" uint32_t dummy_spv[] = {0x07230203, 0x00010000, 0x00080001, 1, 0};\n"
|
|
329
|
-
" std::ofstream f(out_path, std::ios::binary);\n"
|
|
330
|
-
" f.write(reinterpret_cast<const char*>(dummy_spv), sizeof(dummy_spv));\n"
|
|
331
|
-
" f.close();\n"
|
|
332
|
-
" std::lock_guard<std::mutex> guard(lock);\n"
|
|
333
|
-
" shader_fnames.push_back(std::make_pair(name, out_path));\n"
|
|
334
|
-
" return;\n"
|
|
335
|
-
" }"
|
|
336
|
-
)
|
|
337
|
-
if old_err_block in gen_content:
|
|
338
|
-
gen_content = gen_content.replace(old_err_block, new_err_block)
|
|
339
332
|
vulkan_gen.write_text(gen_content, encoding="utf-8")
|
|
340
|
-
logger.info("
|
|
333
|
+
logger.info("Configured vulkan-shaders-gen.cpp for mobile working directory.")
|
|
341
334
|
except Exception as e:
|
|
342
335
|
logger.warning("vulkan-shaders-gen patch note: %s", e)
|
|
343
336
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .exceptions import InstallLockError
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("termux_diffusion.locking")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InstallLock:
|
|
14
|
+
"""File lock manager controlling full installer state machine execution with stale PID auto-recovery."""
|
|
15
|
+
def __init__(self, lock_file: Path, timeout_sec: float = 5.0):
|
|
16
|
+
self.lock_file = lock_file
|
|
17
|
+
self.timeout_sec = timeout_sec
|
|
18
|
+
self._acquired = False
|
|
19
|
+
|
|
20
|
+
def _is_pid_alive(self, pid: int) -> bool:
|
|
21
|
+
if pid <= 0:
|
|
22
|
+
return False
|
|
23
|
+
if sys.platform == "win32":
|
|
24
|
+
try:
|
|
25
|
+
import ctypes
|
|
26
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
27
|
+
h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
28
|
+
if h:
|
|
29
|
+
ctypes.windll.kernel32.CloseHandle(h)
|
|
30
|
+
return True
|
|
31
|
+
return False
|
|
32
|
+
except Exception:
|
|
33
|
+
return True
|
|
34
|
+
else:
|
|
35
|
+
try:
|
|
36
|
+
os.kill(pid, 0)
|
|
37
|
+
return True
|
|
38
|
+
except (OSError, ProcessLookupError):
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def _check_and_clear_stale_lock(self) -> bool:
|
|
42
|
+
"""Check if existing lock file belongs to a deceased process, removing it if stale."""
|
|
43
|
+
if not self.lock_file.exists():
|
|
44
|
+
return True
|
|
45
|
+
try:
|
|
46
|
+
content = self.lock_file.read_text(encoding="utf-8")
|
|
47
|
+
for line in content.splitlines():
|
|
48
|
+
if line.startswith("pid="):
|
|
49
|
+
pid_str = line.split("=", 1)[1].strip()
|
|
50
|
+
if pid_str.isdigit():
|
|
51
|
+
pid = int(pid_str)
|
|
52
|
+
if not self._is_pid_alive(pid):
|
|
53
|
+
logger.warning(
|
|
54
|
+
"[termux-diffusion] Stale installation lock detected (PID: %d is dead). Auto-reclaiming lock.",
|
|
55
|
+
pid,
|
|
56
|
+
)
|
|
57
|
+
self.lock_file.unlink(missing_ok=True)
|
|
58
|
+
return True
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.debug("Stale lock inspection note: %s", e)
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def acquire(self):
|
|
64
|
+
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
self._check_and_clear_stale_lock()
|
|
66
|
+
start = time.time()
|
|
67
|
+
while time.time() - start < self.timeout_sec:
|
|
68
|
+
try:
|
|
69
|
+
fd = os.open(str(self.lock_file), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
70
|
+
os.write(fd, f"pid={os.getpid()}\nstarted_at={time.time()}\n".encode("utf-8"))
|
|
71
|
+
os.close(fd)
|
|
72
|
+
self._acquired = True
|
|
73
|
+
return
|
|
74
|
+
except FileExistsError:
|
|
75
|
+
if self._check_and_clear_stale_lock():
|
|
76
|
+
continue
|
|
77
|
+
time.sleep(0.2)
|
|
78
|
+
raise InstallLockError(
|
|
79
|
+
f"E_INSTALL_LOCKED: Installation process locked by another running process ({self.lock_file})."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def release(self):
|
|
83
|
+
if self._acquired and self.lock_file.exists():
|
|
84
|
+
try:
|
|
85
|
+
self.lock_file.unlink(missing_ok=True)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning("Failed removing install.lock: %s", e)
|
|
88
|
+
self._acquired = False
|
|
89
|
+
|
|
90
|
+
def __enter__(self):
|
|
91
|
+
self.acquire()
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
95
|
+
self.release()
|
|
96
|
+
|
|
@@ -8,8 +8,8 @@ for mobile NPU/TPU chipsets on Android Termux:
|
|
|
8
8
|
- Android Standard Neural Networks API (NNAPI)
|
|
9
9
|
|
|
10
10
|
Architectural Note:
|
|
11
|
-
In v1.
|
|
12
|
-
(
|
|
11
|
+
In v1.3+, inference is driven by the native Bionic sd-cli engine using Vulkan GPU
|
|
12
|
+
(--backend vulkan0) and ARM64 NEON CPU acceleration. Direct Qualcomm QNN / Hexagon NPU subgraph
|
|
13
13
|
partitioning is the architectural blueprint for the upcoming v2.0 runtime.
|
|
14
14
|
"""
|
|
15
15
|
|
|
@@ -228,8 +228,8 @@ def get_optimal_heterogeneous_pipeline(device: str = "auto") -> Dict[str, str]:
|
|
|
228
228
|
"""Determine the compute processor allocation for each diffusion pipeline component.
|
|
229
229
|
|
|
230
230
|
Note:
|
|
231
|
-
In v1.
|
|
232
|
-
Vulkan GPU compute (
|
|
231
|
+
In v1.3+, diffusion inference is executed via the native Bionic sd-cli engine using
|
|
232
|
+
Vulkan GPU compute (--backend vulkan0) and ARM64 NEON CPU math. Direct Qualcomm QNN / Hexagon
|
|
233
233
|
NPU subgraph partitioning is the architectural target for the upcoming v2.0 runtime.
|
|
234
234
|
"""
|
|
235
235
|
req = device.lower().strip()
|
|
@@ -245,7 +245,7 @@ def get_optimal_heterogeneous_pipeline(device: str = "auto") -> Dict[str, str]:
|
|
|
245
245
|
else:
|
|
246
246
|
return {
|
|
247
247
|
"text_encoder": "CPU / GPU (ARM NEON / Vulkan)",
|
|
248
|
-
"denoiser_unet": "GPU (Vulkan Compute /
|
|
248
|
+
"denoiser_unet": "GPU (Vulkan Compute / --backend vulkan0)",
|
|
249
249
|
"vae_decoder": "GPU (Vulkan Compute FP16)",
|
|
250
250
|
"scheduler": "CPU (Single-Core Fast Math)",
|
|
251
251
|
"summary": "GPU-Accelerated [Vulkan Compute Shader Pipeline]"
|
|
@@ -19,8 +19,22 @@ TERMUX_PREFIX = os.environ.get("PREFIX", "/data/data/com.termux/files/usr")
|
|
|
19
19
|
TERMUX_HOME = os.environ.get("HOME", "/data/data/com.termux/files/home")
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
# [B방안] Platform SSOT: ameva-vulkan-runtime.platform 에서 공유 구현을 가져옵니다.
|
|
23
|
+
try:
|
|
24
|
+
from ameva_vulkan_runtime.platform import is_termux as _ameva_is_termux
|
|
25
|
+
_AMEVA_PLATFORM_AVAILABLE = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
_AMEVA_PLATFORM_AVAILABLE = False
|
|
28
|
+
|
|
29
|
+
|
|
22
30
|
def is_android_termux() -> bool:
|
|
23
|
-
"""Check whether the current runtime environment is native Android Termux.
|
|
31
|
+
"""Check whether the current runtime environment is native Android Termux.
|
|
32
|
+
|
|
33
|
+
[B방안] ameva-vulkan-runtime.platform.is_termux() 를 SSOT 로 사용하며,
|
|
34
|
+
미설치 환경에서는 인라인 구현으로 안전하게 폴백합니다.
|
|
35
|
+
"""
|
|
36
|
+
if _AMEVA_PLATFORM_AVAILABLE:
|
|
37
|
+
return _ameva_is_termux()
|
|
24
38
|
if os.environ.get("TERMUX_VERSION") or os.environ.get("TERMUX_APP_PID"):
|
|
25
39
|
return True
|
|
26
40
|
if os.path.exists("/data/data/com.termux"):
|
|
@@ -113,24 +113,37 @@ def run_binary_self_test(
|
|
|
113
113
|
return result
|
|
114
114
|
|
|
115
115
|
# --------------------------------------------------------------------------
|
|
116
|
-
# Stage 2 & 3: Backend Probe & Compute Queue Test
|
|
116
|
+
# Stage 2 & 3: Backend Probe & Compute Queue Test (Real Hardware Probe)
|
|
117
117
|
# --------------------------------------------------------------------------
|
|
118
118
|
if expected_backend == "cpu":
|
|
119
|
-
# Pure CPU binary dependency check
|
|
120
119
|
has_readelf, deps = check_dynamic_library_dependencies(binary_path)
|
|
121
120
|
vulkan_deps = [d for d in deps if "libvulkan.so" in d or "libOpenCL.so" in d]
|
|
122
121
|
if vulkan_deps:
|
|
123
122
|
logger.warning("CPU binary has unwanted GPU library linkage: %s", vulkan_deps)
|
|
124
123
|
result.stage2_probe_passed = True
|
|
125
124
|
result.stage3_compute_passed = True
|
|
126
|
-
print("[termux-diffusion] Stage 1-3 Self-Test: CPU Baseline Binary validated successfully.")
|
|
127
125
|
_SELF_TEST_CACHE[cache_key] = result
|
|
128
126
|
return result
|
|
129
127
|
|
|
130
|
-
# Vulkan
|
|
128
|
+
# [수정] Vulkan 백엔드 실측 검증: ameva-vulkan-runtime 연동
|
|
131
129
|
print(f"[termux-diffusion] Stage 2 Self-Test: Vulkan runtime validation ({binary_path.name})...")
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
try:
|
|
131
|
+
import ameva_vulkan_runtime as avr
|
|
132
|
+
doc = avr.Doctor()
|
|
133
|
+
if doc.quick_probe():
|
|
134
|
+
result.stage2_probe_passed = True
|
|
135
|
+
result.stage3_compute_passed = True
|
|
136
|
+
print(f"[termux-diffusion] Stage 1-3 Self-Test: Vulkan Backend ({doc.quick_probe_device() or 'GPU'}) validated.")
|
|
137
|
+
else:
|
|
138
|
+
result.stage2_probe_passed = False
|
|
139
|
+
result.stage3_compute_passed = False
|
|
140
|
+
result.error_code = "E_VULKAN_PROBE_FAILED"
|
|
141
|
+
result.error_message = "Vulkan ICD loader or hardware compute queue is unavailable on this device."
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
result.stage2_probe_passed = False
|
|
144
|
+
result.stage3_compute_passed = False
|
|
145
|
+
result.error_code = "E_VULKAN_EXCEPTION"
|
|
146
|
+
result.error_message = f"Vulkan probe exception: {exc}"
|
|
147
|
+
|
|
135
148
|
_SELF_TEST_CACHE[cache_key] = result
|
|
136
149
|
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termux-diffusion
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Production On-Device AI Image Generation Framework for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)
|
|
5
5
|
Home-page: https://github.com/uno-km/termux-diffusion
|
|
6
6
|
Author: uno-km (AMEVA Foundation)
|
|
@@ -28,6 +28,7 @@ Classifier: Topic :: Multimedia :: Graphics
|
|
|
28
28
|
Requires-Python: >=3.8
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
License-File: LICENSE
|
|
31
|
+
Requires-Dist: ameva-vulkan-runtime>=1.0.0
|
|
31
32
|
Dynamic: author
|
|
32
33
|
Dynamic: classifier
|
|
33
34
|
Dynamic: home-page
|
|
@@ -75,7 +76,7 @@ termux-setup-storage
|
|
|
75
76
|
|
|
76
77
|
# 2. Install Toolchains & Provision Native Engine
|
|
77
78
|
pkg update -y && pkg install python clang make cmake git termux-api wget vulkan-loader vulkan-headers vulkan-tools -y
|
|
78
|
-
pip install termux-diffusion && termux-diffusion
|
|
79
|
+
pip install termux-diffusion && termux-diffusion install
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
#### [Node.js] Node.js / TypeScript:
|
|
@@ -88,6 +89,11 @@ pkg update -y && pkg install nodejs-lts clang make cmake git termux-api wget vul
|
|
|
88
89
|
npm install -g termux-diffusion && npx termux-diffusion install
|
|
89
90
|
```
|
|
90
91
|
|
|
92
|
+
#### [Universal] One-Line Automated Bootstrap (curl):
|
|
93
|
+
```bash
|
|
94
|
+
curl -sL https://raw.githubusercontent.com/uno-km/termux-diffusion/main/scripts/install.sh | bash
|
|
95
|
+
```
|
|
96
|
+
|
|
91
97
|
---
|
|
92
98
|
|
|
93
99
|
### [Instant] Scenario 2: Instant Generation (Ready to Run)
|
|
@@ -416,12 +422,9 @@ adb shell "/system/bin/device_config put activity_manager max_phantom_processes
|
|
|
416
422
|
|
|
417
423
|
---
|
|
418
424
|
|
|
419
|
-
## 9. Disclaimer
|
|
425
|
+
## 9. Disclaimer
|
|
420
426
|
|
|
421
|
-
>
|
|
422
|
-
> *Termux-Diffusion is an independent open-source project developed for the Android Termux environment and is not officially affiliated with, endorsed by, or sponsored by the Termux project.*
|
|
423
|
-
>
|
|
424
|
-
> *(본 프로젝트는 안드로이드 Termux 환경을 위해 개발된 독립적인 오픈소스 라이브러리이며, Termux 공식 프로젝트와 직접적인 제휴 관계가 아닙니다.)*
|
|
427
|
+
> *This project is an independent open-source library developed for the Android Termux environment. It is not officially affiliated with, endorsed by, or sponsored by the Termux project, stability.ai, or Google LLC.*
|
|
425
428
|
|
|
426
429
|
---
|
|
427
430
|
|
|
@@ -21,6 +21,7 @@ termux_diffusion.egg-info/PKG-INFO
|
|
|
21
21
|
termux_diffusion.egg-info/SOURCES.txt
|
|
22
22
|
termux_diffusion.egg-info/dependency_links.txt
|
|
23
23
|
termux_diffusion.egg-info/entry_points.txt
|
|
24
|
+
termux_diffusion.egg-info/requires.txt
|
|
24
25
|
termux_diffusion.egg-info/top_level.txt
|
|
25
26
|
termux_diffusion/data/bootstrap-manifest.json
|
|
26
27
|
termux_diffusion/data/release-public-keys.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ameva-vulkan-runtime>=1.0.0
|
|
@@ -527,6 +527,7 @@ def test_strict_vulkan_failure_raises(tmp_path):
|
|
|
527
527
|
]
|
|
528
528
|
mock_proc.returncode = 0
|
|
529
529
|
mock_proc.wait = lambda timeout=None: 0
|
|
530
|
+
mock_proc.communicate.return_value = ("", "")
|
|
530
531
|
return mock_proc
|
|
531
532
|
|
|
532
533
|
with patch("termux_diffusion.hardware.resolve_device_backend", return_value=("vulkan", 99)), \
|
|
@@ -109,3 +109,20 @@ def test_a53_mali_profile_and_preset_gating():
|
|
|
109
109
|
assert "balanced" in a53_prof["presets"]
|
|
110
110
|
assert a53_prof["presets"]["balanced"]["status"] == "pending_device_validation"
|
|
111
111
|
assert a53_prof["presets"]["balanced"]["auto_activation"] is False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_runtime_profile_gating_manager():
|
|
115
|
+
from termux_diffusion.hardware import get_profile_gating_manager
|
|
116
|
+
mgr = get_profile_gating_manager()
|
|
117
|
+
assert len(mgr.profiles) > 0
|
|
118
|
+
|
|
119
|
+
# 1. Matching Galaxy S25
|
|
120
|
+
s25_prof = mgr.find_matching_profile("SM-S931N")
|
|
121
|
+
assert s25_prof is not None
|
|
122
|
+
assert s25_prof["gpu"] == "Adreno (TM) 830"
|
|
123
|
+
|
|
124
|
+
# 2. Matching Galaxy A35 alias
|
|
125
|
+
a35_prof = mgr.find_matching_profile("a35x")
|
|
126
|
+
assert a35_prof is not None
|
|
127
|
+
assert a35_prof["gpu"] == "Mali-G68"
|
|
128
|
+
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Process-wide installation lock manager for termux-diffusion."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import time
|
|
5
|
-
import logging
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
from .exceptions import InstallLockError
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger("termux_diffusion.locking")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class InstallLock:
|
|
15
|
-
"""File lock manager controlling full installer state machine execution."""
|
|
16
|
-
def __init__(self, lock_file: Path, timeout_sec: float = 5.0):
|
|
17
|
-
self.lock_file = lock_file
|
|
18
|
-
self.timeout_sec = timeout_sec
|
|
19
|
-
self._acquired = False
|
|
20
|
-
|
|
21
|
-
def acquire(self):
|
|
22
|
-
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
-
start = time.time()
|
|
24
|
-
while time.time() - start < self.timeout_sec:
|
|
25
|
-
try:
|
|
26
|
-
fd = os.open(str(self.lock_file), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
27
|
-
os.write(fd, f"pid={os.getpid()}\nstarted_at={time.time()}\n".encode("utf-8"))
|
|
28
|
-
os.close(fd)
|
|
29
|
-
self._acquired = True
|
|
30
|
-
return
|
|
31
|
-
except FileExistsError:
|
|
32
|
-
time.sleep(0.2)
|
|
33
|
-
raise InstallLockError(
|
|
34
|
-
f"E_INSTALL_LOCKED: Installation process locked by another running process ({self.lock_file})."
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
def release(self):
|
|
38
|
-
if self._acquired and self.lock_file.exists():
|
|
39
|
-
try:
|
|
40
|
-
self.lock_file.unlink(missing_ok=True)
|
|
41
|
-
except Exception as e:
|
|
42
|
-
logger.warning("Failed removing install.lock: %s", e)
|
|
43
|
-
self._acquired = False
|
|
44
|
-
|
|
45
|
-
def __enter__(self):
|
|
46
|
-
self.acquire()
|
|
47
|
-
return self
|
|
48
|
-
|
|
49
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
50
|
-
self.release()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/data/bootstrap-manifest.json
RENAMED
|
File without changes
|
{termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion/data/release-public-keys.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{termux_diffusion-1.3.1 → termux_diffusion-1.3.2}/termux_diffusion.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|