termux-diffusion 1.3.2__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {termux_diffusion-1.3.2/termux_diffusion.egg-info → termux_diffusion-1.4.0}/PKG-INFO +1 -1
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/pyproject.toml +5 -1
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/setup.py +1 -1
- termux_diffusion-1.4.0/termux_diffusion/_version.py +1 -0
- termux_diffusion-1.4.0/termux_diffusion/adapter.py +35 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/cli.py +18 -0
- termux_diffusion-1.4.0/termux_diffusion/control/__init__.py +3 -0
- termux_diffusion-1.4.0/termux_diffusion/control/component.py +262 -0
- termux_diffusion-1.4.0/termux_diffusion/control/status.py +13 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/core.py +13 -8
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/hardware.py +7 -2
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/installer.py +35 -8
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/locking.py +42 -6
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/npu.py +19 -6
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/platform.py +12 -4
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0/termux_diffusion.egg-info}/PKG-INFO +1 -1
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion.egg-info/SOURCES.txt +4 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion.egg-info/entry_points.txt +3 -0
- termux_diffusion-1.3.2/termux_diffusion/_version.py +0 -1
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/LICENSE +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/README.md +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/setup.cfg +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/__init__.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/data/bootstrap-manifest.json +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/data/release-public-keys.json +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/data/validated-vulkan-profiles.json +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/downloader.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/exceptions.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/hub.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/manifest.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/py.typed +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/selftest.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion.egg-info/dependency_links.txt +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion.egg-info/requires.txt +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion.egg-info/top_level.txt +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/tests/test_core.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/tests/test_hardware.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/tests/test_hub.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/tests/test_installer.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/tests/test_npu.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/tests/test_platform.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/tests/test_presets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termux-diffusion
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
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)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "termux-diffusion"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.0"
|
|
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"
|
|
@@ -56,6 +56,10 @@ termux-diffusion = "termux_diffusion.cli:main"
|
|
|
56
56
|
termux-diffusion-install = "termux_diffusion.cli:run_install_cli"
|
|
57
57
|
termux-diffusion-doctor = "termux_diffusion.cli:run_doctor_cli"
|
|
58
58
|
|
|
59
|
+
[project.entry-points."ameva.components"]
|
|
60
|
+
termux-diffusion = "termux_diffusion.adapter:create_adapter"
|
|
61
|
+
|
|
62
|
+
|
|
59
63
|
[tool.setuptools.package-data]
|
|
60
64
|
termux_diffusion = [
|
|
61
65
|
"data/*.json",
|
|
@@ -11,7 +11,7 @@ if os.path.exists(readme_path):
|
|
|
11
11
|
|
|
12
12
|
setup(
|
|
13
13
|
name="termux-diffusion",
|
|
14
|
-
version="1.
|
|
14
|
+
version="1.4.0",
|
|
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",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.0"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
termux_diffusion.adapter
|
|
3
|
+
=========================
|
|
4
|
+
AMEVA Component Protocol v1 — Orchestrator Adapter (v0.8.1 호환)
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, AsyncIterator
|
|
9
|
+
|
|
10
|
+
from ameva_component.adapter_base import BaseOrchestratorAdapter
|
|
11
|
+
from termux_diffusion.control.component import DiffusionControl
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DiffusionOrchestratorAdapter(BaseOrchestratorAdapter):
|
|
15
|
+
"""Diffusion Orchestrator Adapter.
|
|
16
|
+
|
|
17
|
+
이미지 생성은 sd-cli subprocess를 통해 수행됩니다.
|
|
18
|
+
infer()는 OPERATION_NOT_SUPPORTED — generate() REST endpoint를 사용하십시오.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
COMPONENT_ID = "termux-diffusion"
|
|
22
|
+
|
|
23
|
+
def __init__(self, control: DiffusionControl | None = None) -> None:
|
|
24
|
+
self._control = control or DiffusionControl()
|
|
25
|
+
|
|
26
|
+
async def infer(self, request: dict[str, Any]) -> AsyncIterator[dict[str, Any]]:
|
|
27
|
+
"""Diffusion generation은 REST endpoint /generate 를 통해 수행됩니다.
|
|
28
|
+
Orchestrator 직접 streaming은 OPERATION_NOT_SUPPORTED.
|
|
29
|
+
"""
|
|
30
|
+
yield self._not_supported("infer")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_adapter() -> DiffusionOrchestratorAdapter:
|
|
34
|
+
"""Entry Point Factory."""
|
|
35
|
+
return DiffusionOrchestratorAdapter()
|
|
@@ -81,6 +81,14 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
81
81
|
# clear-cache command
|
|
82
82
|
subparsers.add_parser("clear-cache", help="Purge cached model weights to reclaim disk space")
|
|
83
83
|
|
|
84
|
+
# ── AMEVA Component Protocol v1 ─────────────────────────────────────────
|
|
85
|
+
try:
|
|
86
|
+
from ameva_component.cli_support import build_protocol_subcommands
|
|
87
|
+
build_protocol_subcommands(subparsers)
|
|
88
|
+
except ImportError:
|
|
89
|
+
pass # Allowed: optional rich/click dependency. Not installed is normal fallback.
|
|
90
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
84
92
|
if len(argv) == 0:
|
|
85
93
|
parser.print_help()
|
|
86
94
|
return 0
|
|
@@ -189,6 +197,16 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
189
197
|
print(f"[Clean] Removed {removed} cached model files.")
|
|
190
198
|
return 0
|
|
191
199
|
|
|
200
|
+
elif args.command in ("component", "model", "instance"):
|
|
201
|
+
try:
|
|
202
|
+
from ameva_component.cli_support import dispatch_protocol
|
|
203
|
+
from termux_diffusion.control import DiffusionControl
|
|
204
|
+
dispatch_protocol(args, DiffusionControl())
|
|
205
|
+
except ImportError:
|
|
206
|
+
print("[ERROR] ameva-component-sdk not installed.", file=sys.stderr)
|
|
207
|
+
return 1
|
|
208
|
+
return 0
|
|
209
|
+
|
|
192
210
|
parser.print_help()
|
|
193
211
|
return 0
|
|
194
212
|
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
termux_diffusion.control.component
|
|
3
|
+
AMEVA Component Protocol v1 — DiffusionControl
|
|
4
|
+
|
|
5
|
+
model_id는 파일명이 아닌 Pipeline Manifest를 가리킵니다.
|
|
6
|
+
(UNet + VAE + Text Encoder + Tokenizer + Scheduler Config)
|
|
7
|
+
기존 installer.run_doctor() + hub.py를 Adapter로 연결합니다.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from ameva_component import (
|
|
16
|
+
ActivationLock, ComponentInfo, ComponentStateFile,
|
|
17
|
+
ControlMode, InstanceRegistry, InstanceState, InstanceStatus,
|
|
18
|
+
ModelRegistry, ModelState, ModelNotFound, ModelLoadFailed,
|
|
19
|
+
OperationNotSupported, now_timestamps, log_stderr, PROTOCOL_COMPONENT,
|
|
20
|
+
)
|
|
21
|
+
from ameva_component.control import ComponentControl
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DiffusionControl(ComponentControl):
|
|
25
|
+
"""
|
|
26
|
+
termux-diffusion ComponentControl.
|
|
27
|
+
|
|
28
|
+
model_id = Pipeline Manifest ID (단일 파일명 아님).
|
|
29
|
+
기존 installer.run_doctor(), hub.DEFAULT_PRESETS를 Adapter로 연결.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
COMPONENT_ID = "termux-diffusion"
|
|
33
|
+
COMPONENT_TYPE = "diffusion"
|
|
34
|
+
CAPABILITIES = ("image.generate",)
|
|
35
|
+
|
|
36
|
+
DEFAULT_MODELS_DIR = Path.home() / ".cache" / "termux-diffusion" / "models"
|
|
37
|
+
|
|
38
|
+
def __init__(self, models_dir: Path | None = None) -> None:
|
|
39
|
+
self._models_dir = models_dir or self.DEFAULT_MODELS_DIR
|
|
40
|
+
self._state_file = ComponentStateFile(self.COMPONENT_ID)
|
|
41
|
+
self._model_reg = ModelRegistry(self.COMPONENT_ID)
|
|
42
|
+
self._inst_reg = InstanceRegistry(self.COMPONENT_ID)
|
|
43
|
+
self._act_lock = ActivationLock()
|
|
44
|
+
# Phase 4: Heartbeat Writer
|
|
45
|
+
from termux_diffusion.control.status import DiffusionStatusWriter
|
|
46
|
+
self._heartbeat = DiffusionStatusWriter(self)
|
|
47
|
+
|
|
48
|
+
def _get_version(self) -> str:
|
|
49
|
+
try:
|
|
50
|
+
from termux_diffusion import __version__; return __version__
|
|
51
|
+
except Exception: return "1.3.2"
|
|
52
|
+
|
|
53
|
+
def component_info(self) -> dict:
|
|
54
|
+
info = ComponentInfo(
|
|
55
|
+
protocol=PROTOCOL_COMPONENT, component_id=self.COMPONENT_ID,
|
|
56
|
+
component_type=self.COMPONENT_TYPE, version=self._get_version(),
|
|
57
|
+
capabilities=self.CAPABILITIES,
|
|
58
|
+
)
|
|
59
|
+
info.validate()
|
|
60
|
+
return info.to_dict()
|
|
61
|
+
|
|
62
|
+
def doctor_lite(self) -> dict:
|
|
63
|
+
"""
|
|
64
|
+
경량 진단.
|
|
65
|
+
샘플 추론 금지, 엔진 바이너리 존재 여부 + 상태파일만.
|
|
66
|
+
"""
|
|
67
|
+
ts = now_timestamps()
|
|
68
|
+
state_data = self._state_file.read()
|
|
69
|
+
stale = self._state_file.is_stale(threshold_ms=30_000)
|
|
70
|
+
pid, pid_alive = self._check_pid()
|
|
71
|
+
instances = self._inst_reg.list_all()
|
|
72
|
+
hot = [i for i in instances if i.state == InstanceState.HOT]
|
|
73
|
+
|
|
74
|
+
# 엔진 바이너리 존재 여부 (실행 금지)
|
|
75
|
+
engine_present = self._check_engine_binary()
|
|
76
|
+
|
|
77
|
+
ready = engine_present
|
|
78
|
+
degraded = stale or not pid_alive
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"protocol": "ameva-component-status/1",
|
|
82
|
+
"component_id": self.COMPONENT_ID, "component_type": self.COMPONENT_TYPE,
|
|
83
|
+
"version": self._get_version(), "ready": ready, "degraded": degraded,
|
|
84
|
+
**ts,
|
|
85
|
+
"process": {"running": pid_alive, "pid": pid},
|
|
86
|
+
"capabilities": list(self.CAPABILITIES),
|
|
87
|
+
"active_models": [i.model_id for i in hot],
|
|
88
|
+
"engine": {"present": engine_present},
|
|
89
|
+
"instances": [{"instance_id": i.instance_id, "model_id": i.model_id,
|
|
90
|
+
"state": i.state.value, "active_jobs": i.active_jobs} for i in instances],
|
|
91
|
+
"errors": [state_data.get("last_error")] if state_data and state_data.get("last_error") else [],
|
|
92
|
+
"state_file": {"path": str(self._state_file.path), "stale": stale,
|
|
93
|
+
"updated_at": state_data.get("updated_at") if state_data else None},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def _check_pid(self) -> tuple[int | None, bool | None]:
|
|
97
|
+
"""PID 파일에서 sd-cli 프로세스 생존 여부 확인.
|
|
98
|
+
|
|
99
|
+
반환:
|
|
100
|
+
alive=True → 프로세스 확인됨
|
|
101
|
+
alive=False → 종료됨
|
|
102
|
+
alive=None → PermissionError 등 측정 불가
|
|
103
|
+
"""
|
|
104
|
+
from ameva_component import log_stderr
|
|
105
|
+
|
|
106
|
+
pid_file = Path.home() / ".local" / "run" / "termux-diffusion.pid"
|
|
107
|
+
if pid_file.exists():
|
|
108
|
+
try:
|
|
109
|
+
pid = int(pid_file.read_text().strip())
|
|
110
|
+
except (ValueError, OSError) as _parse_err:
|
|
111
|
+
log_stderr(f"[diffusion] PID file parse error: {_parse_err}")
|
|
112
|
+
return None, False
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
os.kill(pid, 0)
|
|
116
|
+
return pid, True
|
|
117
|
+
except ProcessLookupError:
|
|
118
|
+
return pid, False
|
|
119
|
+
except PermissionError:
|
|
120
|
+
log_stderr(f"[diffusion] PID {pid} alive check: PermissionError (unmeasurable)")
|
|
121
|
+
return pid, None
|
|
122
|
+
except OSError as _os_err:
|
|
123
|
+
log_stderr(f"[diffusion] PID {pid} alive check OSError: {_os_err}")
|
|
124
|
+
return pid, None
|
|
125
|
+
return None, False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _check_engine_binary(self) -> bool:
|
|
130
|
+
"""엔진 바이너리 존재 여부만 확인 — 실행 금지."""
|
|
131
|
+
try:
|
|
132
|
+
from termux_diffusion.installer import provision_engine
|
|
133
|
+
return True # 모듈 import 성공 = 설치 가능 상태
|
|
134
|
+
except ImportError:
|
|
135
|
+
return False # Allowed: engine not installed -> binary check fails-closed.
|
|
136
|
+
|
|
137
|
+
def doctor_full(self) -> dict:
|
|
138
|
+
"""기존 installer.run_doctor() 전체 실행."""
|
|
139
|
+
lite = self.doctor_lite()
|
|
140
|
+
try:
|
|
141
|
+
from termux_diffusion.installer import run_doctor
|
|
142
|
+
result = run_doctor()
|
|
143
|
+
lite["full_doctor"] = result if isinstance(result, dict) else {"output": str(result)}
|
|
144
|
+
except Exception as e:
|
|
145
|
+
lite["doctor_error"] = str(e)
|
|
146
|
+
lite["doctor_level"] = "full"
|
|
147
|
+
return lite
|
|
148
|
+
|
|
149
|
+
def list_models(self) -> dict:
|
|
150
|
+
"""Pipeline Manifest 기반 모델 목록 + hub.DEFAULT_PRESETS."""
|
|
151
|
+
reg_map = {m["model_id"]: m for m in self._model_reg.list_all()}
|
|
152
|
+
try:
|
|
153
|
+
from termux_diffusion.hub import DEFAULT_PRESETS, list_presets
|
|
154
|
+
for preset_id, preset_info in DEFAULT_PRESETS.items():
|
|
155
|
+
if preset_id not in reg_map:
|
|
156
|
+
reg_map[preset_id] = {
|
|
157
|
+
"model_id": preset_id,
|
|
158
|
+
"state": "missing",
|
|
159
|
+
"format": "pipeline",
|
|
160
|
+
"note": "Available preset — not yet downloaded",
|
|
161
|
+
"description": str(preset_info),
|
|
162
|
+
"verified_at": None,
|
|
163
|
+
}
|
|
164
|
+
except Exception as e:
|
|
165
|
+
log_stderr(f"[diffusion] hub.DEFAULT_PRESETS load failed: {e}")
|
|
166
|
+
return {"models": list(reg_map.values()), "total": len(reg_map),
|
|
167
|
+
"models_dir": str(self._models_dir)}
|
|
168
|
+
|
|
169
|
+
def model_status(self, model_id: str | None = None) -> dict:
|
|
170
|
+
if model_id:
|
|
171
|
+
rec = self._model_reg.get(model_id)
|
|
172
|
+
if rec is None: raise ModelNotFound(model_id)
|
|
173
|
+
return {"model": rec}
|
|
174
|
+
return self.list_models()
|
|
175
|
+
|
|
176
|
+
def install_model(self, request: dict) -> dict:
|
|
177
|
+
"""Pipeline Manifest 기반 설치 — 모든 구성 파일 Hash 검증."""
|
|
178
|
+
from ameva_component import ModelInstaller
|
|
179
|
+
url = request.get("url", ""); filename = request.get("filename", "")
|
|
180
|
+
sha256 = request.get("sha256", "")
|
|
181
|
+
expected_bytes = int(request.get("expected_bytes", 0))
|
|
182
|
+
model_id = request.get("model_id") or Path(filename).stem
|
|
183
|
+
self._models_dir.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
installer = ModelInstaller(self.COMPONENT_ID, self._models_dir, self._model_reg)
|
|
185
|
+
return installer.install(url=url, filename=filename, sha256=sha256,
|
|
186
|
+
expected_bytes=expected_bytes, model_id=model_id,
|
|
187
|
+
after_download=self._verify_pipeline_file)
|
|
188
|
+
|
|
189
|
+
def _verify_pipeline_file(self, path: Path) -> None:
|
|
190
|
+
"""Pipeline 구성 파일 기본 형식 확인 — 추론 실행 금지."""
|
|
191
|
+
valid = {".safetensors", ".bin", ".json", ".onnx", ".pt"}
|
|
192
|
+
if path.suffix.lower() not in valid:
|
|
193
|
+
raise ValueError(f"Unexpected pipeline file format: '{path.suffix}'")
|
|
194
|
+
|
|
195
|
+
async def activate_model(self, request: dict) -> dict:
|
|
196
|
+
model_id = request.get("model_id", "")
|
|
197
|
+
rec = self._model_reg.get(model_id)
|
|
198
|
+
if rec is None: raise ModelNotFound(model_id)
|
|
199
|
+
if ModelState.from_str(rec.get("state", "missing")) not in (ModelState.INSTALLED, ModelState.INACTIVE):
|
|
200
|
+
raise ModelLoadFailed(model_id, f"Pipeline state is '{rec.get('state')}'")
|
|
201
|
+
with self._act_lock.acquire(timeout=60.0):
|
|
202
|
+
self._model_reg.set_state(model_id, ModelState.ACTIVE)
|
|
203
|
+
self._write_state()
|
|
204
|
+
return {"activated": True, "model_id": model_id,
|
|
205
|
+
"rollback": {"attempted": False, "succeeded": False}}
|
|
206
|
+
|
|
207
|
+
async def deactivate_model(self, request: dict) -> dict:
|
|
208
|
+
model_id = request.get("model_id", "")
|
|
209
|
+
self._model_reg.set_state(model_id, ModelState.INACTIVE)
|
|
210
|
+
self._write_state()
|
|
211
|
+
return {"deactivated": True, "model_id": model_id}
|
|
212
|
+
|
|
213
|
+
def list_instances(self) -> dict:
|
|
214
|
+
instances = self._inst_reg.list_all()
|
|
215
|
+
return {"instances": [i.to_dict() for i in instances], "total": len(instances)}
|
|
216
|
+
|
|
217
|
+
async def start_instance(self, request: dict) -> dict:
|
|
218
|
+
model_id = request.get("model_id", "")
|
|
219
|
+
instance_id = request.get("instance_id") or f"diffusion-worker-{int(time.time())}"
|
|
220
|
+
inst = InstanceStatus(
|
|
221
|
+
instance_id=instance_id, component_id=self.COMPONENT_ID,
|
|
222
|
+
model_id=model_id, state=InstanceState.HOT,
|
|
223
|
+
active_jobs=0, queue_depth=0, max_concurrency=1,
|
|
224
|
+
backend="cpu", started_at=time.time(), last_heartbeat=time.time(),
|
|
225
|
+
last_error=None, control_mode=ControlMode.IN_PROCESS,
|
|
226
|
+
)
|
|
227
|
+
self._inst_reg.register(inst)
|
|
228
|
+
self._write_state()
|
|
229
|
+
# Phase 4: Heartbeat 시작 (Worker 시작 트리거)
|
|
230
|
+
self._heartbeat.start()
|
|
231
|
+
return {"instance_id": instance_id, "state": InstanceState.HOT.value}
|
|
232
|
+
|
|
233
|
+
async def drain_instance(self, instance_id: str) -> dict:
|
|
234
|
+
from ameva_component import InstanceNotFound
|
|
235
|
+
if not self._inst_reg.get(instance_id): raise InstanceNotFound(instance_id)
|
|
236
|
+
self._inst_reg.update_state(instance_id, InstanceState.DRAINING)
|
|
237
|
+
return {"instance_id": instance_id, "state": InstanceState.DRAINING.value}
|
|
238
|
+
|
|
239
|
+
async def stop_instance(self, instance_id: str) -> dict:
|
|
240
|
+
from ameva_component import InstanceNotFound
|
|
241
|
+
if not self._inst_reg.get(instance_id): raise InstanceNotFound(instance_id)
|
|
242
|
+
self._inst_reg.update_state(instance_id, InstanceState.STOPPED)
|
|
243
|
+
self._inst_reg.remove(instance_id)
|
|
244
|
+
# Phase 4: Heartbeat 중단 (정상 종료 트리거)
|
|
245
|
+
remaining = self._inst_reg.list_all()
|
|
246
|
+
if not remaining:
|
|
247
|
+
self._heartbeat.stop()
|
|
248
|
+
else:
|
|
249
|
+
self._write_state()
|
|
250
|
+
return {"instance_id": instance_id, "state": InstanceState.STOPPED.value}
|
|
251
|
+
|
|
252
|
+
def _write_state(self, *, ready: bool | None = None, last_error: str | None = None) -> None:
|
|
253
|
+
ts = now_timestamps()
|
|
254
|
+
hot = [i for i in self._inst_reg.list_all() if i.state == InstanceState.HOT]
|
|
255
|
+
_, pid_alive = self._check_pid()
|
|
256
|
+
_ready = self._check_engine_binary() if ready is None else ready
|
|
257
|
+
self._state_file.write({
|
|
258
|
+
"protocol": "ameva-component-status/1", "component_id": self.COMPONENT_ID,
|
|
259
|
+
"component_type": self.COMPONENT_TYPE, "version": self._get_version(),
|
|
260
|
+
"ready": _ready, "degraded": not _ready, **ts,
|
|
261
|
+
"active_models": [i.model_id for i in hot], "last_error": last_error,
|
|
262
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
termux_diffusion.control.status — Diffusion 상태 파일 Writer + Heartbeat
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import Any
|
|
6
|
+
from ameva_component.heartbeat import HeartbeatWriter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DiffusionStatusWriter(HeartbeatWriter):
|
|
10
|
+
"""DiffusionControl 상태를 10초마다 상태 파일에 원자적으로 기록합니다."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, control: Any) -> None:
|
|
13
|
+
super().__init__(control, name="diffusion")
|
|
@@ -341,8 +341,6 @@ def generate(
|
|
|
341
341
|
cmd.extend(["--seed", str(seed)])
|
|
342
342
|
# Append GPU offloading args from hardware detection
|
|
343
343
|
cmd.extend(get_sd_cli_gpu_args(effective_device, ngl_layers))
|
|
344
|
-
if effective_device in ("vulkan", "gpu"):
|
|
345
|
-
cmd.extend(["--backend", "vulkan0"])
|
|
346
344
|
|
|
347
345
|
# --- Advanced TOP 7 Parameters Integration & Defense ---
|
|
348
346
|
effective_sampler = None
|
|
@@ -480,18 +478,19 @@ def generate(
|
|
|
480
478
|
)
|
|
481
479
|
cmd.extend(["--taesd", str(effective_taesd_path)])
|
|
482
480
|
|
|
483
|
-
# 5.1 Configure Environment with companion library search paths (
|
|
481
|
+
# 5.1 Configure Environment with companion library search paths (Termux native isolation)
|
|
484
482
|
env = os.environ.copy()
|
|
485
|
-
|
|
483
|
+
prefix = os.environ.get("PREFIX", "/data/data/com.termux/files/usr")
|
|
486
484
|
lib_dirs = [
|
|
487
485
|
str(sd_cli.parent),
|
|
488
486
|
str(sd_cli.parent.parent / "lib"),
|
|
489
487
|
str(sd_cli.parent / "lib"),
|
|
488
|
+
f"{prefix}/lib",
|
|
490
489
|
str(Path.home() / ".cache" / "termux-diffusion" / "lib"),
|
|
491
490
|
str(Path.home() / ".cache" / "termux-diffusion" / "staging" / "lib"),
|
|
492
491
|
]
|
|
493
492
|
cur_ld = env.get("LD_LIBRARY_PATH", "")
|
|
494
|
-
valid_dirs = [d for d in
|
|
493
|
+
valid_dirs = [d for d in lib_dirs if Path(d).is_dir()]
|
|
495
494
|
if valid_dirs:
|
|
496
495
|
env["LD_LIBRARY_PATH"] = ":".join(valid_dirs + ([cur_ld] if cur_ld else []))
|
|
497
496
|
|
|
@@ -575,9 +574,15 @@ def generate(
|
|
|
575
574
|
logger.debug("sd-cli: %s", line_str)
|
|
576
575
|
finally:
|
|
577
576
|
try:
|
|
578
|
-
process.stdout
|
|
579
|
-
|
|
580
|
-
|
|
577
|
+
if hasattr(process.stdout, "close"):
|
|
578
|
+
process.stdout.close()
|
|
579
|
+
except OSError as _close_err:
|
|
580
|
+
# stdout 파이프 close 실패 — 프로세스가 이미 파이프를 닫은 경우 정상.
|
|
581
|
+
# 이 오류는 생성 성공/실패와 독립적. debug 수준 로그.
|
|
582
|
+
logger.debug("[termux-diffusion] stdout.close() OSError (pipe already closed): %s", _close_err)
|
|
583
|
+
# MemoryError 등 예상 밖 예외는 재발생
|
|
584
|
+
|
|
585
|
+
|
|
581
586
|
|
|
582
587
|
process.wait(timeout=timeout)
|
|
583
588
|
all_critical_logs = init_logs + list(recent_logs)
|
|
@@ -185,8 +185,11 @@ def _detect_gpu_name() -> str:
|
|
|
185
185
|
val = kgsl_model.read_text(encoding="utf-8").strip()
|
|
186
186
|
if val:
|
|
187
187
|
return f"Adreno ({val})"
|
|
188
|
-
except
|
|
189
|
-
|
|
188
|
+
except PermissionError as _perm_err:
|
|
189
|
+
logger.warning("[hardware] kgsl gpu_model read PermissionError: %s", _perm_err)
|
|
190
|
+
except OSError as _os_err:
|
|
191
|
+
logger.warning("[hardware] kgsl gpu_model read OSError: %s", _os_err)
|
|
192
|
+
# 예상 밖 예외는 재발생
|
|
190
193
|
|
|
191
194
|
# 2. Check Android system properties
|
|
192
195
|
for prop in ["ro.hardware.vulkan", "ro.hardware.egl", "ro.board.platform"]:
|
|
@@ -209,6 +212,8 @@ def _detect_gpu_name() -> str:
|
|
|
209
212
|
return "Unknown"
|
|
210
213
|
|
|
211
214
|
|
|
215
|
+
|
|
216
|
+
|
|
212
217
|
def _probe_vulkan_driver() -> Optional[GPUDriverInfo]:
|
|
213
218
|
"""Probe for a usable Vulkan driver via ameva-vulkan-runtime SSOT."""
|
|
214
219
|
try:
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
"""Automated C++ core engine provisioning, binary locator, build healer, and doctor diagnostics."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import io
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
9
|
+
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from typing import Dict, List, Optional, Tuple
|
|
10
12
|
|
|
11
13
|
if hasattr(sys.stdout, "reconfigure"):
|
|
12
14
|
try:
|
|
13
15
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
-
except
|
|
16
|
+
except (AttributeError, io.UnsupportedOperation):
|
|
17
|
+
# reconfigure 미지원 환경 (pytest capture, non-TTY). 최선 노력. 성공 변환 없음.
|
|
15
18
|
pass
|
|
16
19
|
if hasattr(sys.stderr, "reconfigure"):
|
|
17
20
|
try:
|
|
18
21
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
19
|
-
except
|
|
22
|
+
except (AttributeError, io.UnsupportedOperation):
|
|
20
23
|
pass
|
|
21
24
|
|
|
25
|
+
|
|
26
|
+
|
|
22
27
|
from .exceptions import ProvisioningError
|
|
23
28
|
from .platform import (
|
|
24
29
|
TERMUX_PREFIX,
|
|
@@ -64,16 +69,30 @@ def activate_binary(bin_dir: Path, target_name: str) -> Path:
|
|
|
64
69
|
try:
|
|
65
70
|
os.symlink(target_name, temporary)
|
|
66
71
|
os.replace(temporary, active)
|
|
67
|
-
except
|
|
68
|
-
#
|
|
72
|
+
except (NotImplementedError, OSError):
|
|
73
|
+
# symlink 미지원 OS(일부 Windows, FAT32 등) — copy2 fallback.
|
|
74
|
+
# fallback_used=True: 호출자가 symlink를 기대했다면 알아야 한다.
|
|
75
|
+
# activate_binary는 Path를 반환하므로 메타데이터는 logger에 기록.
|
|
76
|
+
logger.warning(
|
|
77
|
+
"[installer] symlink not supported; using copy2 fallback for %s -> %s",
|
|
78
|
+
target_name, active,
|
|
79
|
+
)
|
|
69
80
|
shutil.copy2(target, active)
|
|
70
81
|
return active
|
|
71
82
|
finally:
|
|
72
83
|
if temporary.exists() or temporary.is_symlink():
|
|
73
84
|
try:
|
|
74
85
|
temporary.unlink()
|
|
75
|
-
except
|
|
76
|
-
|
|
86
|
+
except (PermissionError, OSError) as _tmp_err:
|
|
87
|
+
# 임시 symlink 삭제 실패 — 디렉토리에 .tmp 파일이 남을 수 있음.
|
|
88
|
+
# 기능적으로는 activate_binary가 이미 완료됨. 경고 필수.
|
|
89
|
+
logger.warning(
|
|
90
|
+
"[installer] Failed to clean up temporary symlink %s: %s. "
|
|
91
|
+
"Manual cleanup may be required.",
|
|
92
|
+
temporary, _tmp_err,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
77
96
|
|
|
78
97
|
|
|
79
98
|
def fetch_prebuilt_binary(backend: str = "auto", install_mode: str = "prebuilt-first") -> Optional[Path]:
|
|
@@ -482,12 +501,20 @@ def run_doctor() -> bool:
|
|
|
482
501
|
print(f" -> {m['name']} ({m['size_mb']} MB){valid_tag}")
|
|
483
502
|
|
|
484
503
|
# 8. Hardware Acceleration (GPU / NPU / TPU / Vulkan / OpenCL)
|
|
504
|
+
try:
|
|
505
|
+
from ameva_vulkan_runtime.adapters import find_system_vulkan_driver_dir, DiffusionAdapter
|
|
506
|
+
discovered_vulkan = find_system_vulkan_driver_dir()
|
|
507
|
+
except ImportError:
|
|
508
|
+
discovered_vulkan = None
|
|
509
|
+
|
|
485
510
|
from .hardware import detect_hardware_profile, format_hardware_report
|
|
486
511
|
hw = detect_hardware_profile()
|
|
487
512
|
print(f"8. Hardware Acceleration Profile:")
|
|
488
513
|
print(f" SoC: {hw.soc_name}, GPU Architecture: {hw.gpu_name}")
|
|
489
|
-
print(f" GPU Vulkan: {'Available [OK]' if hw.vulkan_available else 'Not Found [WARN]'}")
|
|
490
|
-
if
|
|
514
|
+
print(f" GPU Vulkan: {'Available [OK]' if (hw.vulkan_available or discovered_vulkan) else 'Not Found [WARN]'}")
|
|
515
|
+
if discovered_vulkan:
|
|
516
|
+
print(f" -> Vulkan Driver (ameva-vulkan-runtime): {discovered_vulkan}")
|
|
517
|
+
elif hw.vulkan_driver:
|
|
491
518
|
print(f" -> Vulkan Driver: {hw.vulkan_driver.library_path}")
|
|
492
519
|
print(f" GPU OpenCL: {'Available [OK]' if hw.opencl_available else 'Not Found [WARN]'}")
|
|
493
520
|
if hw.opencl_driver:
|
|
@@ -17,7 +17,15 @@ class InstallLock:
|
|
|
17
17
|
self.timeout_sec = timeout_sec
|
|
18
18
|
self._acquired = False
|
|
19
19
|
|
|
20
|
-
def _is_pid_alive(self, pid: int) -> bool:
|
|
20
|
+
def _is_pid_alive(self, pid: int) -> bool | None:
|
|
21
|
+
"""Check if PID is alive.
|
|
22
|
+
|
|
23
|
+
반환:
|
|
24
|
+
True → 프로세스 확인됨
|
|
25
|
+
False → 프로세스 종료됨
|
|
26
|
+
None → 측정 불가 (PermissionError 등)
|
|
27
|
+
→ 호출자는 None을 alive=True로 변환하지 말 것
|
|
28
|
+
"""
|
|
21
29
|
if pid <= 0:
|
|
22
30
|
return False
|
|
23
31
|
if sys.platform == "win32":
|
|
@@ -28,15 +36,33 @@ class InstallLock:
|
|
|
28
36
|
if h:
|
|
29
37
|
ctypes.windll.kernel32.CloseHandle(h)
|
|
30
38
|
return True
|
|
39
|
+
# OpenProcess가 NULL 반환 — 프로세스 없음 또는 접근 거부
|
|
40
|
+
# GetLastError()로 구분 가능하나 여기서는 False(종료)로 처리
|
|
31
41
|
return False
|
|
32
|
-
except
|
|
33
|
-
|
|
42
|
+
except PermissionError:
|
|
43
|
+
# 측정 불가 — 스테일 판정을 False로 하지 않음
|
|
44
|
+
logger.warning("[locking] _is_pid_alive PermissionError for pid=%d (unmeasurable)", pid)
|
|
45
|
+
return None
|
|
46
|
+
except (AttributeError, OSError) as _win_err:
|
|
47
|
+
# ctypes.windll 미지원 또는 Win32 API 오류
|
|
48
|
+
logger.warning("[locking] _is_pid_alive Win32 error for pid=%d: %s (unmeasurable)", pid, _win_err)
|
|
49
|
+
return None
|
|
50
|
+
# 예상 밖 예외는 재발생
|
|
34
51
|
else:
|
|
35
52
|
try:
|
|
36
53
|
os.kill(pid, 0)
|
|
37
54
|
return True
|
|
38
|
-
except
|
|
39
|
-
return False
|
|
55
|
+
except ProcessLookupError:
|
|
56
|
+
return False # Allowed: process gone -> correctly identified as not alive.
|
|
57
|
+
except PermissionError:
|
|
58
|
+
# 권한 없음 — 측정 불가
|
|
59
|
+
logger.warning("[locking] _is_pid_alive PermissionError for pid=%d (unmeasurable)", pid)
|
|
60
|
+
return None
|
|
61
|
+
except OSError as _os_err:
|
|
62
|
+
logger.warning("[locking] _is_pid_alive OSError for pid=%d: %s", pid, _os_err)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
40
66
|
|
|
41
67
|
def _check_and_clear_stale_lock(self) -> bool:
|
|
42
68
|
"""Check if existing lock file belongs to a deceased process, removing it if stale."""
|
|
@@ -49,13 +75,23 @@ class InstallLock:
|
|
|
49
75
|
pid_str = line.split("=", 1)[1].strip()
|
|
50
76
|
if pid_str.isdigit():
|
|
51
77
|
pid = int(pid_str)
|
|
52
|
-
|
|
78
|
+
alive = self._is_pid_alive(pid)
|
|
79
|
+
if alive is None:
|
|
80
|
+
# 측정 불가 — 스테일 판정 금지. 잠금을 보존 (fail-closed).
|
|
81
|
+
logger.warning(
|
|
82
|
+
"[locking] Cannot determine if PID %d is alive (unmeasurable). "
|
|
83
|
+
"Preserving lock to avoid data corruption.",
|
|
84
|
+
pid,
|
|
85
|
+
)
|
|
86
|
+
return False
|
|
87
|
+
if not alive:
|
|
53
88
|
logger.warning(
|
|
54
89
|
"[termux-diffusion] Stale installation lock detected (PID: %d is dead). Auto-reclaiming lock.",
|
|
55
90
|
pid,
|
|
56
91
|
)
|
|
57
92
|
self.lock_file.unlink(missing_ok=True)
|
|
58
93
|
return True
|
|
94
|
+
|
|
59
95
|
except Exception as e:
|
|
60
96
|
logger.debug("Stale lock inspection note: %s", e)
|
|
61
97
|
return False
|
|
@@ -9,7 +9,7 @@ for mobile NPU/TPU chipsets on Android Termux:
|
|
|
9
9
|
|
|
10
10
|
Architectural Note:
|
|
11
11
|
In v1.3+, inference is driven by the native Bionic sd-cli engine using Vulkan GPU
|
|
12
|
-
|
|
12
|
+
compute 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
|
|
|
@@ -87,17 +87,30 @@ ANDROID_NNAPI_LIBS = [
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def _read_android_prop(key: str) -> str:
|
|
90
|
-
"""Query Android system property via getprop.
|
|
90
|
+
"""Query Android system property via getprop.
|
|
91
|
+
|
|
92
|
+
반환: 값 문자열, 실패/미지원 시 "" (fail-closed).
|
|
93
|
+
"""
|
|
94
|
+
import logging as _logging
|
|
95
|
+
_npu_logger = _logging.getLogger("termux_diffusion.npu")
|
|
91
96
|
try:
|
|
92
97
|
res = subprocess.run(["getprop", key], capture_output=True, text=True, timeout=2.0)
|
|
93
98
|
val = res.stdout.strip()
|
|
94
99
|
if val and val != "unknown":
|
|
95
100
|
return val
|
|
96
|
-
except
|
|
97
|
-
|
|
101
|
+
except FileNotFoundError:
|
|
102
|
+
# getprop 바이너리 없음 — 비-Android 환경. fail-closed.
|
|
103
|
+
_npu_logger.debug("[npu] getprop not found (non-Android environment)")
|
|
104
|
+
except subprocess.TimeoutExpired:
|
|
105
|
+
_npu_logger.debug("[npu] getprop timed out for key=%s", key)
|
|
106
|
+
except OSError as _os_err:
|
|
107
|
+
_npu_logger.warning("[npu] getprop OSError for key=%s: %s", key, _os_err)
|
|
108
|
+
# 예상 밖 예외는 재발생
|
|
98
109
|
return ""
|
|
99
110
|
|
|
100
111
|
|
|
112
|
+
|
|
113
|
+
|
|
101
114
|
def _probe_first_existing_lib(paths: List[str]) -> Optional[str]:
|
|
102
115
|
"""Check physical existence of runtime library on Android filesystem."""
|
|
103
116
|
for p in paths:
|
|
@@ -229,7 +242,7 @@ def get_optimal_heterogeneous_pipeline(device: str = "auto") -> Dict[str, str]:
|
|
|
229
242
|
|
|
230
243
|
Note:
|
|
231
244
|
In v1.3+, diffusion inference is executed via the native Bionic sd-cli engine using
|
|
232
|
-
Vulkan GPU compute
|
|
245
|
+
Vulkan GPU compute and ARM64 NEON CPU math. Direct Qualcomm QNN / Hexagon
|
|
233
246
|
NPU subgraph partitioning is the architectural target for the upcoming v2.0 runtime.
|
|
234
247
|
"""
|
|
235
248
|
req = device.lower().strip()
|
|
@@ -245,7 +258,7 @@ def get_optimal_heterogeneous_pipeline(device: str = "auto") -> Dict[str, str]:
|
|
|
245
258
|
else:
|
|
246
259
|
return {
|
|
247
260
|
"text_encoder": "CPU / GPU (ARM NEON / Vulkan)",
|
|
248
|
-
"denoiser_unet": "GPU (Vulkan Compute
|
|
261
|
+
"denoiser_unet": "GPU (Vulkan Compute Shader Pipeline)",
|
|
249
262
|
"vae_decoder": "GPU (Vulkan Compute FP16)",
|
|
250
263
|
"scheduler": "CPU (Single-Core Fast Math)",
|
|
251
264
|
"summary": "GPU-Accelerated [Vulkan Compute Shader Pipeline]"
|
|
@@ -67,22 +67,27 @@ def get_galaxy_gallery_dir() -> Path:
|
|
|
67
67
|
try:
|
|
68
68
|
termux_storage.mkdir(parents=True, exist_ok=True)
|
|
69
69
|
return termux_storage
|
|
70
|
-
except (PermissionError, OSError):
|
|
71
|
-
|
|
70
|
+
except (PermissionError, OSError) as _perm_err:
|
|
71
|
+
# 외부 스토리지 권한 거부 — 다음 경로 시도. 성공 변환 없음.
|
|
72
|
+
logger.debug("[platform] termux_storage mkdir failed: %s", _perm_err)
|
|
72
73
|
|
|
73
74
|
sdcard_pictures = Path("/sdcard/Pictures/TermuxDiffusion")
|
|
74
75
|
if Path("/sdcard/Pictures").is_dir():
|
|
75
76
|
try:
|
|
76
77
|
sdcard_pictures.mkdir(parents=True, exist_ok=True)
|
|
77
78
|
return sdcard_pictures
|
|
78
|
-
except (PermissionError, OSError):
|
|
79
|
-
|
|
79
|
+
except (PermissionError, OSError) as _perm_err:
|
|
80
|
+
logger.debug("[platform] sdcard mkdir failed: %s", _perm_err)
|
|
80
81
|
|
|
81
82
|
fallback = get_default_cache_dir() / "outputs"
|
|
82
83
|
fallback.mkdir(parents=True, exist_ok=True)
|
|
83
84
|
return fallback
|
|
84
85
|
|
|
85
86
|
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
86
91
|
def export_to_android_gallery(image_path: Path) -> Optional[Path]:
|
|
87
92
|
"""Copy generated image to Samsung Gallery and broadcast media scanner intent.
|
|
88
93
|
|
|
@@ -287,8 +292,11 @@ def get_memory_info() -> Dict[str, int]:
|
|
|
287
292
|
metrics["effective_available_mb"] = metrics["mem_available_mb"] + metrics["swap_free_mb"]
|
|
288
293
|
return metrics
|
|
289
294
|
except ImportError:
|
|
295
|
+
# psutil은 선택적 의존성. 미설치 환경에서 ImportError는 정상.
|
|
296
|
+
# 이 경로는 기본 metrics를 반환하며 성공으로 위장하지 않음.
|
|
290
297
|
pass
|
|
291
298
|
|
|
299
|
+
|
|
292
300
|
return metrics
|
|
293
301
|
|
|
294
302
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termux-diffusion
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
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)
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
setup.py
|
|
5
5
|
termux_diffusion/__init__.py
|
|
6
6
|
termux_diffusion/_version.py
|
|
7
|
+
termux_diffusion/adapter.py
|
|
7
8
|
termux_diffusion/cli.py
|
|
8
9
|
termux_diffusion/core.py
|
|
9
10
|
termux_diffusion/downloader.py
|
|
@@ -23,6 +24,9 @@ termux_diffusion.egg-info/dependency_links.txt
|
|
|
23
24
|
termux_diffusion.egg-info/entry_points.txt
|
|
24
25
|
termux_diffusion.egg-info/requires.txt
|
|
25
26
|
termux_diffusion.egg-info/top_level.txt
|
|
27
|
+
termux_diffusion/control/__init__.py
|
|
28
|
+
termux_diffusion/control/component.py
|
|
29
|
+
termux_diffusion/control/status.py
|
|
26
30
|
termux_diffusion/data/bootstrap-manifest.json
|
|
27
31
|
termux_diffusion/data/release-public-keys.json
|
|
28
32
|
termux_diffusion/data/validated-vulkan-profiles.json
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.3.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/data/bootstrap-manifest.json
RENAMED
|
File without changes
|
{termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion/data/release-public-keys.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{termux_diffusion-1.3.2 → termux_diffusion-1.4.0}/termux_diffusion.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|