termux-diffusion 1.3.2__tar.gz → 1.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. termux_diffusion-1.4.1/PKG-INFO +104 -0
  2. termux_diffusion-1.4.1/README.md +67 -0
  3. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/pyproject.toml +6 -2
  4. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/setup.py +2 -2
  5. termux_diffusion-1.4.1/termux_diffusion/_version.py +1 -0
  6. termux_diffusion-1.4.1/termux_diffusion/adapter.py +35 -0
  7. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/cli.py +18 -0
  8. termux_diffusion-1.4.1/termux_diffusion/control/__init__.py +3 -0
  9. termux_diffusion-1.4.1/termux_diffusion/control/component.py +262 -0
  10. termux_diffusion-1.4.1/termux_diffusion/control/status.py +13 -0
  11. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/core.py +13 -8
  12. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/hardware.py +7 -2
  13. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/installer.py +35 -8
  14. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/locking.py +42 -6
  15. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/npu.py +19 -6
  16. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/platform.py +12 -4
  17. termux_diffusion-1.4.1/termux_diffusion.egg-info/PKG-INFO +104 -0
  18. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/SOURCES.txt +4 -0
  19. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/entry_points.txt +3 -0
  20. termux_diffusion-1.3.2/PKG-INFO +0 -444
  21. termux_diffusion-1.3.2/README.md +0 -407
  22. termux_diffusion-1.3.2/termux_diffusion/_version.py +0 -1
  23. termux_diffusion-1.3.2/termux_diffusion.egg-info/PKG-INFO +0 -444
  24. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/LICENSE +0 -0
  25. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/setup.cfg +0 -0
  26. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/__init__.py +0 -0
  27. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/data/bootstrap-manifest.json +0 -0
  28. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/data/release-public-keys.json +0 -0
  29. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/data/validated-vulkan-profiles.json +0 -0
  30. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/downloader.py +0 -0
  31. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/exceptions.py +0 -0
  32. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/hub.py +0 -0
  33. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/manifest.py +0 -0
  34. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/py.typed +0 -0
  35. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/selftest.py +0 -0
  36. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/dependency_links.txt +0 -0
  37. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/requires.txt +0 -0
  38. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/top_level.txt +0 -0
  39. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_core.py +0 -0
  40. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_hardware.py +0 -0
  41. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_hub.py +0 -0
  42. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_installer.py +0 -0
  43. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_npu.py +0 -0
  44. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_platform.py +0 -0
  45. {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_presets.py +0 -0
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: termux-diffusion
3
+ Version: 1.4.1
4
+ Summary: On-device Stable Diffusion runtime utilizing device resources for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)
5
+ Home-page: https://github.com/uno-km/termux-diffusion
6
+ Author: uno-km (AMEVA Foundation)
7
+ Author-email: "uno-km (AMEVA Foundation)" <hosequelbo@gmail.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/uno-km/termux-diffusion
10
+ Project-URL: Documentation, https://uno-km.github.io/termux-diffusion/
11
+ Project-URL: npm Package, https://www.npmjs.com/package/termux-diffusion
12
+ Project-URL: Bug Tracker, https://github.com/uno-km/termux-diffusion/issues
13
+ Project-URL: Source, https://github.com/uno-km/termux-diffusion
14
+ Keywords: stable-diffusion,diffusion,termux,android,samsung-galaxy,edge-ai,on-device-ai,image-generation,gguf,arm64,bionic-libc,snapdragon,exynos,text-to-image,txt2img,mobile-ai,quantization,generative-ai,vulkan,opencl,offline-ai,private-ai,ai-art,comfyui-alternative,webui-alternative
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Operating System :: Android
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
27
+ Classifier: Topic :: Multimedia :: Graphics
28
+ Requires-Python: >=3.8
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: ameva-vulkan-runtime>=1.0.0
32
+ Dynamic: author
33
+ Dynamic: classifier
34
+ Dynamic: home-page
35
+ Dynamic: license-file
36
+ Dynamic: requires-python
37
+
38
+ # Termux-Diffusion
39
+
40
+ [![PyPI](https://img.shields.io/pypi/v/termux-diffusion.svg?style=flat-square&color=0369a1)](https://pypi.org/project/termux-diffusion/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/termux-diffusion.svg?style=flat-square)](https://pypi.org/project/termux-diffusion/)
42
+ [![npm](https://img.shields.io/npm/v/termux-diffusion.svg?style=flat-square&color=b91c1c)](https://www.npmjs.com/package/termux-diffusion)
43
+ [![npm downloads](https://img.shields.io/npm/dm/termux-diffusion.svg?style=flat-square&color=b91c1c)](https://www.npmjs.com/package/termux-diffusion)
44
+ [![License](https://img.shields.io/badge/License-Apache_2.0-004499.svg?style=flat-square)](https://github.com/uno-km/termux-diffusion)
45
+
46
+ > **디바이스 리소스를 활용한 Android Termux 네이티브 온디바이스 Stable Diffusion 런타임**
47
+ > *Native On-Device Stable Diffusion Runtime Utilizing Device Resources for Android Termux*
48
+
49
+ ---
50
+
51
+ ## 📌 Architecture & Overview
52
+
53
+ 안드로이드 순정 Bionic libc 상에서 디바이스 리소스를 활용하여 ARMv8.2-A DotProd/FP16 SIMD 벡터 연산(Cortex-A78 x4: 61초)을 통해 이미지를 생성하고 삼성 갤러리에 자동 반영합니다.
54
+
55
+ Executes quantized Stable Diffusion models utilizing device resources with ARMv8.2-A DotProd/FP16 SIMD vector execution (Cortex-A78 x4 61s) and direct Samsung Gallery indexing.
56
+
57
+ ---
58
+
59
+ ## 🚀 Installation & Quickstart
60
+
61
+ ### Python (PyPI)
62
+ ```bash
63
+ pip install termux-diffusion
64
+ ```
65
+ ```python
66
+ import termux_diffusion as td
67
+
68
+ # 1. Generate image on Android hardware
69
+ image_path = td.generate(
70
+ prompt="Cyberpunk Seoul city at night, neon lights, 8k",
71
+ model="sd-turbo",
72
+ steps=4
73
+ )
74
+ print(f"Generated and saved to Gallery: {image_path}")
75
+
76
+ ```
77
+
78
+ ### Node.js / TypeScript (npm)
79
+ ```bash
80
+ npm install termux-diffusion
81
+ ```
82
+ ```typescript
83
+ import { generate } from "termux-diffusion";
84
+
85
+ const result = await generate({
86
+ prompt: "Cyberpunk Seoul city at night, neon lights, 8k",
87
+ model: "sd-turbo",
88
+ steps: 4
89
+ });
90
+ console.log("Image created:", result.outputPath);
91
+
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 📖 Official Documentation & Benchmarks
97
+ - [Official Architecture & API Reference](https://uno-km.vercel.app/lib/diffusion/)
98
+ - [Ecosystem Metrics & Registry Stats](https://uno-km.vercel.app/foundation/metrics)
99
+ - [AMEVA Open-Source Foundation Portal](https://uno-km.vercel.app/foundation/index.html)
100
+
101
+ ---
102
+
103
+ ## 📄 License
104
+ Licensed under the Apache-2.0 License. Copyright (c) 2026 Eunho Kim ([@uno-km](https://github.com/uno-km)).
@@ -0,0 +1,67 @@
1
+ # Termux-Diffusion
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/termux-diffusion.svg?style=flat-square&color=0369a1)](https://pypi.org/project/termux-diffusion/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/termux-diffusion.svg?style=flat-square)](https://pypi.org/project/termux-diffusion/)
5
+ [![npm](https://img.shields.io/npm/v/termux-diffusion.svg?style=flat-square&color=b91c1c)](https://www.npmjs.com/package/termux-diffusion)
6
+ [![npm downloads](https://img.shields.io/npm/dm/termux-diffusion.svg?style=flat-square&color=b91c1c)](https://www.npmjs.com/package/termux-diffusion)
7
+ [![License](https://img.shields.io/badge/License-Apache_2.0-004499.svg?style=flat-square)](https://github.com/uno-km/termux-diffusion)
8
+
9
+ > **디바이스 리소스를 활용한 Android Termux 네이티브 온디바이스 Stable Diffusion 런타임**
10
+ > *Native On-Device Stable Diffusion Runtime Utilizing Device Resources for Android Termux*
11
+
12
+ ---
13
+
14
+ ## 📌 Architecture & Overview
15
+
16
+ 안드로이드 순정 Bionic libc 상에서 디바이스 리소스를 활용하여 ARMv8.2-A DotProd/FP16 SIMD 벡터 연산(Cortex-A78 x4: 61초)을 통해 이미지를 생성하고 삼성 갤러리에 자동 반영합니다.
17
+
18
+ Executes quantized Stable Diffusion models utilizing device resources with ARMv8.2-A DotProd/FP16 SIMD vector execution (Cortex-A78 x4 61s) and direct Samsung Gallery indexing.
19
+
20
+ ---
21
+
22
+ ## 🚀 Installation & Quickstart
23
+
24
+ ### Python (PyPI)
25
+ ```bash
26
+ pip install termux-diffusion
27
+ ```
28
+ ```python
29
+ import termux_diffusion as td
30
+
31
+ # 1. Generate image on Android hardware
32
+ image_path = td.generate(
33
+ prompt="Cyberpunk Seoul city at night, neon lights, 8k",
34
+ model="sd-turbo",
35
+ steps=4
36
+ )
37
+ print(f"Generated and saved to Gallery: {image_path}")
38
+
39
+ ```
40
+
41
+ ### Node.js / TypeScript (npm)
42
+ ```bash
43
+ npm install termux-diffusion
44
+ ```
45
+ ```typescript
46
+ import { generate } from "termux-diffusion";
47
+
48
+ const result = await generate({
49
+ prompt: "Cyberpunk Seoul city at night, neon lights, 8k",
50
+ model: "sd-turbo",
51
+ steps: 4
52
+ });
53
+ console.log("Image created:", result.outputPath);
54
+
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 📖 Official Documentation & Benchmarks
60
+ - [Official Architecture & API Reference](https://uno-km.vercel.app/lib/diffusion/)
61
+ - [Ecosystem Metrics & Registry Stats](https://uno-km.vercel.app/foundation/metrics)
62
+ - [AMEVA Open-Source Foundation Portal](https://uno-km.vercel.app/foundation/index.html)
63
+
64
+ ---
65
+
66
+ ## 📄 License
67
+ Licensed under the Apache-2.0 License. Copyright (c) 2026 Eunho Kim ([@uno-km](https://github.com/uno-km)).
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "termux-diffusion"
7
- version = "1.3.2"
8
- description = "Production On-Device AI Image Generation Framework for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)"
7
+ version = "1.4.1"
8
+ description = "On-device Stable Diffusion runtime utilizing device resources for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
11
  license = { text = "MIT" }
@@ -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,8 +11,8 @@ if os.path.exists(readme_path):
11
11
 
12
12
  setup(
13
13
  name="termux-diffusion",
14
- version="1.3.2",
15
- description="Production On-Device AI Image Generation Framework for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)",
14
+ version="1.4.1",
15
+ description="On-device Stable Diffusion runtime utilizing device resources for Android Termux & Samsung Galaxy (Dual-Engine Python & Node.js)",
16
16
  long_description=long_description,
17
17
  long_description_content_type="text/markdown",
18
18
  author="uno-km (AMEVA Foundation)",
@@ -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,3 @@
1
+ """termux_diffusion.control — AMEVA Component Protocol v1."""
2
+ from .component import DiffusionControl
3
+ __all__ = ["DiffusionControl"]
@@ -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 (Bionic priority on Android)
481
+ # 5.1 Configure Environment with companion library search paths (Termux native isolation)
484
482
  env = os.environ.copy()
485
- bionic_sys_dirs = ["/system/lib64", "/vendor/lib64", "/vendor/lib64/egl"]
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 bionic_sys_dirs + lib_dirs if Path(d).is_dir()]
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.close()
579
- except Exception:
580
- pass
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 Exception:
189
- pass
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: