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.
- termux_diffusion-1.4.1/PKG-INFO +104 -0
- termux_diffusion-1.4.1/README.md +67 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/pyproject.toml +6 -2
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/setup.py +2 -2
- termux_diffusion-1.4.1/termux_diffusion/_version.py +1 -0
- termux_diffusion-1.4.1/termux_diffusion/adapter.py +35 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/cli.py +18 -0
- termux_diffusion-1.4.1/termux_diffusion/control/__init__.py +3 -0
- termux_diffusion-1.4.1/termux_diffusion/control/component.py +262 -0
- termux_diffusion-1.4.1/termux_diffusion/control/status.py +13 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/core.py +13 -8
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/hardware.py +7 -2
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/installer.py +35 -8
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/locking.py +42 -6
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/npu.py +19 -6
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/platform.py +12 -4
- termux_diffusion-1.4.1/termux_diffusion.egg-info/PKG-INFO +104 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/SOURCES.txt +4 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/entry_points.txt +3 -0
- termux_diffusion-1.3.2/PKG-INFO +0 -444
- termux_diffusion-1.3.2/README.md +0 -407
- termux_diffusion-1.3.2/termux_diffusion/_version.py +0 -1
- termux_diffusion-1.3.2/termux_diffusion.egg-info/PKG-INFO +0 -444
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/LICENSE +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/setup.cfg +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/__init__.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/data/bootstrap-manifest.json +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/data/release-public-keys.json +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/data/validated-vulkan-profiles.json +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/downloader.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/exceptions.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/hub.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/manifest.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/py.typed +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion/selftest.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/dependency_links.txt +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/requires.txt +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/termux_diffusion.egg-info/top_level.txt +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_core.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_hardware.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_hub.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_installer.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_npu.py +0 -0
- {termux_diffusion-1.3.2 → termux_diffusion-1.4.1}/tests/test_platform.py +0 -0
- {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
|
+
[](https://pypi.org/project/termux-diffusion/)
|
|
41
|
+
[](https://pypi.org/project/termux-diffusion/)
|
|
42
|
+
[](https://www.npmjs.com/package/termux-diffusion)
|
|
43
|
+
[](https://www.npmjs.com/package/termux-diffusion)
|
|
44
|
+
[](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
|
+
[](https://pypi.org/project/termux-diffusion/)
|
|
4
|
+
[](https://pypi.org/project/termux-diffusion/)
|
|
5
|
+
[](https://www.npmjs.com/package/termux-diffusion)
|
|
6
|
+
[](https://www.npmjs.com/package/termux-diffusion)
|
|
7
|
+
[](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.
|
|
8
|
-
description = "
|
|
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.
|
|
15
|
-
description="
|
|
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,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:
|