amv-diffusion 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amv_diffusion-2.1.0.dist-info/METADATA +180 -0
- amv_diffusion-2.1.0.dist-info/RECORD +39 -0
- amv_diffusion-2.1.0.dist-info/WHEEL +5 -0
- amv_diffusion-2.1.0.dist-info/entry_points.txt +7 -0
- amv_diffusion-2.1.0.dist-info/licenses/LICENSE +21 -0
- amv_diffusion-2.1.0.dist-info/top_level.txt +1 -0
- termux_diffusion/__init__.py +185 -0
- termux_diffusion/__main__.py +7 -0
- termux_diffusion/_version.py +3 -0
- termux_diffusion/adapter.py +41 -0
- termux_diffusion/canvas.py +364 -0
- termux_diffusion/cli.py +492 -0
- termux_diffusion/cluster.py +340 -0
- termux_diffusion/control/__init__.py +5 -0
- termux_diffusion/control/component.py +309 -0
- termux_diffusion/control/status.py +15 -0
- termux_diffusion/core.py +2936 -0
- termux_diffusion/data/bootstrap-manifest.json +15 -0
- termux_diffusion/data/prompt_cache/108a7149c4e70550.json +10 -0
- termux_diffusion/data/release-public-keys.json +10 -0
- termux_diffusion/data/validated-vulkan-profiles.json +298 -0
- termux_diffusion/detailer.py +283 -0
- termux_diffusion/doctor.py +128 -0
- termux_diffusion/downloader.py +111 -0
- termux_diffusion/engine.py +226 -0
- termux_diffusion/exceptions.py +158 -0
- termux_diffusion/hardware.py +985 -0
- termux_diffusion/hub.py +686 -0
- termux_diffusion/installer.py +499 -0
- termux_diffusion/locking.py +134 -0
- termux_diffusion/manifest.py +128 -0
- termux_diffusion/npu.py +267 -0
- termux_diffusion/platform.py +533 -0
- termux_diffusion/purger.py +145 -0
- termux_diffusion/py.typed +1 -0
- termux_diffusion/safety.py +150 -0
- termux_diffusion/selftest.py +178 -0
- termux_diffusion/web.py +3370 -0
- termux_diffusion/workflow.py +1345 -0
termux_diffusion/core.py
ADDED
|
@@ -0,0 +1,2936 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""Core generation runner, argument builder, WakeLock wrapper, and gallery bridge."""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
import queue
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
import signal
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
import threading
|
|
26
|
+
|
|
27
|
+
import time
|
|
28
|
+
|
|
29
|
+
from collections import deque
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from typing import Callable, Generator, Optional, Tuple, Union, List, Dict, Any
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
from .exceptions import InferenceTimeoutError, OOMRiskError, ProvisioningError, TermuxDiffusionError
|
|
40
|
+
|
|
41
|
+
from .hub import DEFAULT_PRESETS, list_presets, resolve_model_path
|
|
42
|
+
|
|
43
|
+
from .engine import get_binary_path, locate_sd_cli
|
|
44
|
+
|
|
45
|
+
from .installer import provision_engine
|
|
46
|
+
|
|
47
|
+
from .platform import (
|
|
48
|
+
|
|
49
|
+
TermuxWakeLock,
|
|
50
|
+
|
|
51
|
+
check_memory_safety,
|
|
52
|
+
|
|
53
|
+
export_to_android_gallery,
|
|
54
|
+
|
|
55
|
+
get_galaxy_gallery_dir,
|
|
56
|
+
|
|
57
|
+
get_optimal_thread_count,
|
|
58
|
+
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_default_cache_dir() -> Path:
|
|
66
|
+
|
|
67
|
+
from .platform import get_default_cache_dir as _fn
|
|
68
|
+
|
|
69
|
+
return _fn()
|
|
70
|
+
|
|
71
|
+
from .hardware import (
|
|
72
|
+
|
|
73
|
+
TermuxExecutionGuard,
|
|
74
|
+
|
|
75
|
+
get_kernel_physical_memory_gb,
|
|
76
|
+
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
from .cluster import (
|
|
80
|
+
|
|
81
|
+
parse_cluster_rpc_spec,
|
|
82
|
+
|
|
83
|
+
verify_rpc_cluster_nodes,
|
|
84
|
+
|
|
85
|
+
verify_rpc_cluster_health,
|
|
86
|
+
|
|
87
|
+
ClusterOptionForwarder,
|
|
88
|
+
|
|
89
|
+
SequentialClusterPipeline,
|
|
90
|
+
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
from .exceptions import (
|
|
94
|
+
|
|
95
|
+
ClusterConnectionError,
|
|
96
|
+
|
|
97
|
+
ClusterConfigurationError,
|
|
98
|
+
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
logger = logging.getLogger("termux_diffusion.core")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
PROGRESS_RE = re.compile(
|
|
110
|
+
|
|
111
|
+
r'(?:\|[=>\s#]+\|\s*)?(?:step\s*)?(\d+)\s*/\s*(\d+)(?:\s*-\s*([\d\.]+)\s*(s/it|it/s))?',
|
|
112
|
+
|
|
113
|
+
re.IGNORECASE
|
|
114
|
+
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _read_sys_gpu_busy() -> Optional[int]:
|
|
122
|
+
|
|
123
|
+
"""Query Linux/Android kernel sysfs GPU load percentage across Adreno, Mali, and generic nodes."""
|
|
124
|
+
|
|
125
|
+
paths = [
|
|
126
|
+
|
|
127
|
+
Path("/sys/kernel/gpu/gpu_busy"),
|
|
128
|
+
|
|
129
|
+
Path("/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage"),
|
|
130
|
+
|
|
131
|
+
Path("/sys/class/kgsl/kgsl-3d0/devfreq/gpu_load"),
|
|
132
|
+
|
|
133
|
+
Path("/sys/class/misc/mali0/device/utilization"),
|
|
134
|
+
|
|
135
|
+
Path("/sys/devices/platform/13040000.mali/utilization"),
|
|
136
|
+
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for p in paths:
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
|
|
143
|
+
if p.is_file():
|
|
144
|
+
|
|
145
|
+
raw = p.read_text().strip().replace("%", "").split()[0]
|
|
146
|
+
|
|
147
|
+
if raw.isdigit():
|
|
148
|
+
|
|
149
|
+
return int(raw)
|
|
150
|
+
|
|
151
|
+
except Exception:
|
|
152
|
+
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class GPUDutyCycleController:
|
|
162
|
+
|
|
163
|
+
"""Closed-loop GPU Duty Cycle & SurfaceFlinger/App preservation controller.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
Prevents GPU 100% saturation by injecting inter-step micro idle gaps (time-slicing),
|
|
168
|
+
|
|
169
|
+
enabling Android SurfaceFlinger to render frames at 60/120Hz and preventing LMK SIGKILL.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def __init__(self, target_duty_cycle: int = 90, enabled: bool = False):
|
|
176
|
+
|
|
177
|
+
self.target_duty_cycle = max(50, min(100, target_duty_cycle))
|
|
178
|
+
|
|
179
|
+
self.enabled = enabled or (self.target_duty_cycle < 100)
|
|
180
|
+
|
|
181
|
+
self.last_step = 0
|
|
182
|
+
|
|
183
|
+
self.step_start_time = time.time()
|
|
184
|
+
|
|
185
|
+
self.accumulated_sleep = 0.0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def on_step(self, step: int, proc_pid: Optional[int] = None) -> float:
|
|
190
|
+
|
|
191
|
+
"""Called upon each denoising step completion. Injects dynamic gap if needed."""
|
|
192
|
+
|
|
193
|
+
if not self.enabled or self.target_duty_cycle >= 100:
|
|
194
|
+
|
|
195
|
+
return 0.0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if step <= self.last_step:
|
|
200
|
+
|
|
201
|
+
return 0.0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
now = time.time()
|
|
206
|
+
|
|
207
|
+
step_duration = max(0.01, now - self.step_start_time)
|
|
208
|
+
|
|
209
|
+
self.step_start_time = now
|
|
210
|
+
|
|
211
|
+
self.last_step = step
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Calculate duty cycle gap:
|
|
216
|
+
|
|
217
|
+
# target_duty = (compute_time / (compute_time + gap_time)) * 100
|
|
218
|
+
|
|
219
|
+
# gap_time = compute_time * (100 - target_duty) / target_duty
|
|
220
|
+
|
|
221
|
+
ratio = (100 - self.target_duty_cycle) / float(self.target_duty_cycle)
|
|
222
|
+
|
|
223
|
+
gap_sec = min(0.5, max(0.015, step_duration * ratio))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Check real-time GPU load if available
|
|
228
|
+
|
|
229
|
+
gpu_busy = _read_sys_gpu_busy()
|
|
230
|
+
|
|
231
|
+
if gpu_busy is not None and gpu_busy > self.target_duty_cycle:
|
|
232
|
+
|
|
233
|
+
excess = (gpu_busy - self.target_duty_cycle) / 10.0
|
|
234
|
+
|
|
235
|
+
gap_sec = min(0.8, gap_sec * (1.0 + excess))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# Inject micro-gap via SIGSTOP/SIGCONT if on Linux/Android and PID valid
|
|
240
|
+
|
|
241
|
+
injected = 0.0
|
|
242
|
+
|
|
243
|
+
if proc_pid and hasattr(os, "kill") and hasattr(signal, "SIGSTOP") and hasattr(signal, "SIGCONT"):
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
|
|
247
|
+
os.kill(proc_pid, signal.SIGSTOP)
|
|
248
|
+
|
|
249
|
+
time.sleep(gap_sec)
|
|
250
|
+
|
|
251
|
+
os.kill(proc_pid, signal.SIGCONT)
|
|
252
|
+
|
|
253
|
+
injected = gap_sec
|
|
254
|
+
|
|
255
|
+
except OSError:
|
|
256
|
+
|
|
257
|
+
time.sleep(gap_sec)
|
|
258
|
+
|
|
259
|
+
injected = gap_sec
|
|
260
|
+
|
|
261
|
+
else:
|
|
262
|
+
|
|
263
|
+
time.sleep(gap_sec)
|
|
264
|
+
|
|
265
|
+
injected = gap_sec
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
self.accumulated_sleep += injected
|
|
270
|
+
|
|
271
|
+
logger.debug(
|
|
272
|
+
|
|
273
|
+
"[DutyCycleController] Step %d: Injected %.3fs idle gap (Target: %d%%, Kernel GPU: %s%%)",
|
|
274
|
+
|
|
275
|
+
step, injected, self.target_duty_cycle, gpu_busy
|
|
276
|
+
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return injected
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _read_process_rss_mb(pid: int) -> Optional[float]:
|
|
286
|
+
|
|
287
|
+
"""Query process resident set size (VmRSS) from Linux procfs."""
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
|
|
291
|
+
p = Path(f"/proc/{pid}/status")
|
|
292
|
+
|
|
293
|
+
if p.is_file():
|
|
294
|
+
|
|
295
|
+
for line in p.read_text().splitlines():
|
|
296
|
+
|
|
297
|
+
if line.startswith("VmRSS:"):
|
|
298
|
+
|
|
299
|
+
parts = line.split()
|
|
300
|
+
|
|
301
|
+
if len(parts) >= 2:
|
|
302
|
+
|
|
303
|
+
return round(int(parts[1]) / 1024.0, 1)
|
|
304
|
+
|
|
305
|
+
except Exception:
|
|
306
|
+
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass
|
|
316
|
+
|
|
317
|
+
class ProgressInfo:
|
|
318
|
+
|
|
319
|
+
"""Real-time progress snapshot during diffusion inference."""
|
|
320
|
+
|
|
321
|
+
phase: str # "init" | "loading_model" | "encoding_prompt" | "sampling" | "decoding_vae" | "complete" | "error"
|
|
322
|
+
|
|
323
|
+
step: int = 0 # Current denoising step (e.g. 3)
|
|
324
|
+
|
|
325
|
+
total_steps: int = 0 # Total denoising steps (e.g. 8)
|
|
326
|
+
|
|
327
|
+
percent: float = 0.0 # Completion percentage (0.0 - 100.0)
|
|
328
|
+
|
|
329
|
+
eta_seconds: Optional[float] = None
|
|
330
|
+
|
|
331
|
+
speed_s_per_it: Optional[float] = None
|
|
332
|
+
|
|
333
|
+
gpu_busy: Optional[int] = None
|
|
334
|
+
|
|
335
|
+
rss_mb: Optional[float] = None
|
|
336
|
+
|
|
337
|
+
raw_message: str = ""
|
|
338
|
+
|
|
339
|
+
output_path: Optional[str] = None
|
|
340
|
+
|
|
341
|
+
elapsed_seconds: Optional[float] = None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def to_dict(self) -> dict:
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
|
|
349
|
+
"event": "progress" if self.phase == "sampling" else ("complete" if self.phase == "complete" else "phase"),
|
|
350
|
+
|
|
351
|
+
"phase": self.phase,
|
|
352
|
+
|
|
353
|
+
"step": self.step,
|
|
354
|
+
|
|
355
|
+
"total_steps": self.total_steps,
|
|
356
|
+
|
|
357
|
+
"percent": round(self.percent, 1),
|
|
358
|
+
|
|
359
|
+
"eta_seconds": round(self.eta_seconds, 1) if self.eta_seconds is not None else None,
|
|
360
|
+
|
|
361
|
+
"speed_s": round(self.speed_s_per_it, 2) if self.speed_s_per_it is not None else None,
|
|
362
|
+
|
|
363
|
+
"gpu_busy": self.gpu_busy,
|
|
364
|
+
|
|
365
|
+
"rss_mb": self.rss_mb,
|
|
366
|
+
|
|
367
|
+
"output_path": str(self.output_path) if self.output_path else None,
|
|
368
|
+
|
|
369
|
+
"elapsed_seconds": round(self.elapsed_seconds, 2) if self.elapsed_seconds is not None else None,
|
|
370
|
+
|
|
371
|
+
"timestamp": time.time(),
|
|
372
|
+
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
VALID_SAMPLERS = {
|
|
382
|
+
|
|
383
|
+
"euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2",
|
|
384
|
+
|
|
385
|
+
"ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s",
|
|
386
|
+
|
|
387
|
+
"er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"
|
|
388
|
+
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
VALID_SCHEDULERS = {
|
|
392
|
+
|
|
393
|
+
"default", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform",
|
|
394
|
+
|
|
395
|
+
"simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal",
|
|
396
|
+
|
|
397
|
+
"flux2", "flux", "beta"
|
|
398
|
+
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@dataclass
|
|
406
|
+
|
|
407
|
+
class GenerationResult:
|
|
408
|
+
|
|
409
|
+
"""Encapsulates the output and metadata of an AI diffusion generation task."""
|
|
410
|
+
|
|
411
|
+
path: Path
|
|
412
|
+
|
|
413
|
+
gallery_path: Optional[Path]
|
|
414
|
+
|
|
415
|
+
prompt: str
|
|
416
|
+
|
|
417
|
+
negative_prompt: Optional[str]
|
|
418
|
+
|
|
419
|
+
model: str
|
|
420
|
+
|
|
421
|
+
device: str
|
|
422
|
+
|
|
423
|
+
steps: int
|
|
424
|
+
|
|
425
|
+
cfg_scale: float
|
|
426
|
+
|
|
427
|
+
width: int
|
|
428
|
+
|
|
429
|
+
height: int
|
|
430
|
+
|
|
431
|
+
seed: int
|
|
432
|
+
|
|
433
|
+
elapsed_sec: float
|
|
434
|
+
|
|
435
|
+
sampling_method: Optional[str] = None
|
|
436
|
+
|
|
437
|
+
schedule: Optional[str] = None
|
|
438
|
+
|
|
439
|
+
vae_tiling: bool = False
|
|
440
|
+
|
|
441
|
+
init_img: Optional[Path] = None
|
|
442
|
+
|
|
443
|
+
strength: Optional[float] = None
|
|
444
|
+
|
|
445
|
+
lora_dir: Optional[Path] = None
|
|
446
|
+
|
|
447
|
+
clip_skip: Optional[int] = None
|
|
448
|
+
|
|
449
|
+
control_net: Optional[Path] = None
|
|
450
|
+
|
|
451
|
+
control_image: Optional[Path] = None
|
|
452
|
+
|
|
453
|
+
control_strength: Optional[float] = None
|
|
454
|
+
|
|
455
|
+
taesd: Optional[Path] = None
|
|
456
|
+
|
|
457
|
+
rpc: Optional[str] = None
|
|
458
|
+
|
|
459
|
+
tensor_split: Optional[str] = None
|
|
460
|
+
|
|
461
|
+
cluster_rpc_servers: Optional[List[str]] = None
|
|
462
|
+
|
|
463
|
+
cluster_split_mode: Optional[str] = None
|
|
464
|
+
|
|
465
|
+
cluster_tensor_split: Optional[str] = None
|
|
466
|
+
|
|
467
|
+
cluster_vram_budget: Optional[str] = None
|
|
468
|
+
|
|
469
|
+
extra_args: Optional[List[str]] = None
|
|
470
|
+
|
|
471
|
+
isolated_stages: bool = False
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def __str__(self) -> str:
|
|
476
|
+
|
|
477
|
+
return f"<GenerationResult path='{self.path}' device='{self.device}' elapsed={self.elapsed_sec:.1f}s>"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
import signal
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _safe_kill_process(proc: Optional[subprocess.Popen], timeout: float = 2.0) -> None:
|
|
488
|
+
|
|
489
|
+
"""Safely terminate and reap child process groups with granular exception handling and zero zombie leak."""
|
|
490
|
+
|
|
491
|
+
if proc is None:
|
|
492
|
+
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
|
|
499
|
+
if proc.poll() is not None:
|
|
500
|
+
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
except (OSError, ValueError) as poll_err:
|
|
504
|
+
|
|
505
|
+
logger.debug("Process poll inspection note: %s", poll_err)
|
|
506
|
+
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
pid = getattr(proc, "pid", None)
|
|
512
|
+
|
|
513
|
+
if pid is None:
|
|
514
|
+
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
logger.warning("Initiating child process termination (PID: %s)...", pid)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# Step 1: Graceful termination attempt (SIGTERM / proc.terminate)
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
|
|
527
|
+
if sys.platform != "win32":
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
|
|
531
|
+
pgid = os.getpgid(pid)
|
|
532
|
+
|
|
533
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
534
|
+
|
|
535
|
+
except ProcessLookupError:
|
|
536
|
+
|
|
537
|
+
logger.debug("Process group (PGID: %s) already exited.", pid)
|
|
538
|
+
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
except (OSError, PermissionError) as pg_err:
|
|
542
|
+
|
|
543
|
+
logger.debug("killpg SIGTERM note (PGID: %s): %s, falling back to proc.terminate()", pid, pg_err)
|
|
544
|
+
|
|
545
|
+
proc.terminate()
|
|
546
|
+
|
|
547
|
+
else:
|
|
548
|
+
|
|
549
|
+
proc.terminate()
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
|
|
555
|
+
proc.wait(timeout=timeout)
|
|
556
|
+
|
|
557
|
+
logger.debug("Child process (PID: %s) gracefully exited on SIGTERM.", pid)
|
|
558
|
+
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
except subprocess.TimeoutExpired:
|
|
562
|
+
|
|
563
|
+
logger.warning(
|
|
564
|
+
|
|
565
|
+
"Child process (PID: %s) did not exit within %.1fs, escalating to SIGKILL...",
|
|
566
|
+
|
|
567
|
+
pid,
|
|
568
|
+
|
|
569
|
+
timeout,
|
|
570
|
+
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
except ProcessLookupError:
|
|
574
|
+
|
|
575
|
+
logger.debug("Child process (PID: %s) already terminated before SIGTERM dispatch.", pid)
|
|
576
|
+
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
except PermissionError as perm_err:
|
|
580
|
+
|
|
581
|
+
logger.warning("Permission denied while sending SIGTERM to PID %s: %s", pid, perm_err)
|
|
582
|
+
|
|
583
|
+
except OSError as os_err:
|
|
584
|
+
|
|
585
|
+
logger.warning("OS error during SIGTERM dispatch to PID %s: %s", pid, os_err)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# Step 2: Forceful termination (SIGKILL / proc.kill)
|
|
590
|
+
|
|
591
|
+
try:
|
|
592
|
+
|
|
593
|
+
if sys.platform != "win32":
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
|
|
597
|
+
pgid = os.getpgid(pid)
|
|
598
|
+
|
|
599
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
600
|
+
|
|
601
|
+
except ProcessLookupError:
|
|
602
|
+
|
|
603
|
+
logger.debug("Process group (PGID: %s) already terminated.", pid)
|
|
604
|
+
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
except (OSError, PermissionError) as pg_err:
|
|
608
|
+
|
|
609
|
+
logger.debug("killpg SIGKILL note (PGID: %s): %s, falling back to proc.kill()", pid, pg_err)
|
|
610
|
+
|
|
611
|
+
proc.kill()
|
|
612
|
+
|
|
613
|
+
else:
|
|
614
|
+
|
|
615
|
+
proc.kill()
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
try:
|
|
620
|
+
|
|
621
|
+
proc.wait(timeout=2.0)
|
|
622
|
+
|
|
623
|
+
logger.debug("Child process (PID: %s) forcefully terminated and reaped.", pid)
|
|
624
|
+
|
|
625
|
+
except subprocess.TimeoutExpired:
|
|
626
|
+
|
|
627
|
+
logger.error("Child process (PID: %s) did not exit after SIGKILL (kernel D-state or pending I/O).", pid)
|
|
628
|
+
|
|
629
|
+
except ProcessLookupError:
|
|
630
|
+
|
|
631
|
+
logger.debug("Child process (PID: %s) already reaped before SIGKILL.", pid)
|
|
632
|
+
|
|
633
|
+
except PermissionError as perm_err:
|
|
634
|
+
|
|
635
|
+
logger.error("Permission denied while sending SIGKILL to PID %s: %s", pid, perm_err)
|
|
636
|
+
|
|
637
|
+
except OSError as os_err:
|
|
638
|
+
|
|
639
|
+
logger.error("OS error during SIGKILL dispatch to PID %s: %s", pid, os_err)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
DEFAULT_QUALITY_GUARD_NEGATIVE_PROMPT = "lowres, bad quality, blur, deformed, distorted, extra limbs, artifacts"
|
|
646
|
+
|
|
647
|
+
_global_default_negative_prompt: Optional[str] = None
|
|
648
|
+
|
|
649
|
+
_negative_prompt_lock = threading.Lock()
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def get_default_negative_prompt() -> Optional[str]:
|
|
656
|
+
|
|
657
|
+
"""Get the currently configured default negative prompt (None by default)."""
|
|
658
|
+
|
|
659
|
+
with _negative_prompt_lock:
|
|
660
|
+
|
|
661
|
+
return _global_default_negative_prompt
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def set_default_negative_prompt(prompt: Optional[str]) -> None:
|
|
668
|
+
|
|
669
|
+
"""Set or clear the default negative prompt used across generate() calls.
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
|
|
675
|
+
prompt: Custom negative prompt string, or None to disable default negative guidance.
|
|
676
|
+
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
global _global_default_negative_prompt
|
|
680
|
+
|
|
681
|
+
with _negative_prompt_lock:
|
|
682
|
+
|
|
683
|
+
if prompt is not None and not str(prompt).strip():
|
|
684
|
+
|
|
685
|
+
_global_default_negative_prompt = None
|
|
686
|
+
|
|
687
|
+
else:
|
|
688
|
+
|
|
689
|
+
_global_default_negative_prompt = str(prompt).strip() if prompt is not None else None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def get_quality_guard_negative_prompt() -> str:
|
|
696
|
+
|
|
697
|
+
"""Return the recommended standard quality-guard negative prompt."""
|
|
698
|
+
|
|
699
|
+
return DEFAULT_QUALITY_GUARD_NEGATIVE_PROMPT
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def get_model_file_size(model_name_or_path: Union[str, Path, None]) -> int:
|
|
706
|
+
|
|
707
|
+
"""Resolve file size in bytes for a model path or model preset."""
|
|
708
|
+
|
|
709
|
+
if not model_name_or_path:
|
|
710
|
+
|
|
711
|
+
return 0
|
|
712
|
+
|
|
713
|
+
raw_str = str(model_name_or_path).strip()
|
|
714
|
+
|
|
715
|
+
expanded = Path(os.path.expanduser(raw_str))
|
|
716
|
+
|
|
717
|
+
if expanded.is_file():
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
|
|
721
|
+
return expanded.stat().st_size
|
|
722
|
+
|
|
723
|
+
except OSError:
|
|
724
|
+
|
|
725
|
+
pass
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
|
|
729
|
+
resolved = resolve_model_path(raw_str)
|
|
730
|
+
|
|
731
|
+
if resolved and Path(resolved).is_file():
|
|
732
|
+
|
|
733
|
+
return Path(resolved).stat().st_size
|
|
734
|
+
|
|
735
|
+
except Exception:
|
|
736
|
+
|
|
737
|
+
pass
|
|
738
|
+
|
|
739
|
+
from .platform import get_default_cache_dir
|
|
740
|
+
|
|
741
|
+
cache_p = Path(get_default_cache_dir()) / "models" / expanded.name
|
|
742
|
+
|
|
743
|
+
if cache_p.is_file():
|
|
744
|
+
|
|
745
|
+
try:
|
|
746
|
+
|
|
747
|
+
return cache_p.stat().st_size
|
|
748
|
+
|
|
749
|
+
except OSError:
|
|
750
|
+
|
|
751
|
+
pass
|
|
752
|
+
|
|
753
|
+
return int(3.5 * (1024 ** 3))
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def resolve_vram_and_streaming_policy(
|
|
760
|
+
|
|
761
|
+
user_max_vram: Optional[str],
|
|
762
|
+
|
|
763
|
+
user_stream_layers: bool,
|
|
764
|
+
|
|
765
|
+
model_bytes: int,
|
|
766
|
+
|
|
767
|
+
llm_bytes: int = 0,
|
|
768
|
+
|
|
769
|
+
vae_bytes: int = 0,
|
|
770
|
+
|
|
771
|
+
) -> Tuple[Optional[str], bool, str]:
|
|
772
|
+
|
|
773
|
+
"""Resolves VRAM budget and streaming mode with explicit user override priority.
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
Computes total co-resident memory (DiT + LLM text encoder + VAE) to prevent
|
|
778
|
+
|
|
779
|
+
underestimating DRAM pressure on mobile devices with strict LMKD policies.
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
|
|
785
|
+
(effective_max_vram, effective_stream_layers, reason_description)
|
|
786
|
+
|
|
787
|
+
"""
|
|
788
|
+
|
|
789
|
+
# 1. User Explicit Override Priority
|
|
790
|
+
|
|
791
|
+
if user_max_vram is not None or user_stream_layers:
|
|
792
|
+
|
|
793
|
+
val = str(user_max_vram).strip() if user_max_vram is not None else None
|
|
794
|
+
|
|
795
|
+
if val in ("0", "none", "disable", "unlimited", "vulkan0=0"):
|
|
796
|
+
|
|
797
|
+
return None, False, "User explicit: Full In-VRAM residency (graph splitting disabled)"
|
|
798
|
+
|
|
799
|
+
return val, user_stream_layers, f"User explicit override: max_vram={val}, stream={user_stream_layers}"
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
# 2. Query Deterministic Physical Memory from Linux Kernel
|
|
804
|
+
|
|
805
|
+
mem_total_gb, mem_avail_gb = get_kernel_physical_memory_gb()
|
|
806
|
+
|
|
807
|
+
total_co_resident_bytes = model_bytes + llm_bytes + vae_bytes
|
|
808
|
+
|
|
809
|
+
total_co_resident_gb = total_co_resident_bytes / (1024 ** 3)
|
|
810
|
+
|
|
811
|
+
required_safe_ram = total_co_resident_gb + 1.2 # Total weights + VRAM scratch buffers + OS Safety Margin
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# [Tier 1: Flagship Full In-VRAM Residency]
|
|
816
|
+
|
|
817
|
+
# Requires sufficient unpinned physical DRAM to hold all active models and graphics buffers
|
|
818
|
+
|
|
819
|
+
if mem_avail_gb >= required_safe_ram:
|
|
820
|
+
|
|
821
|
+
return None, False, f"Tier 1 Flagship Auto-Scale: Avail {mem_avail_gb}GB >= Safe {required_safe_ram:.1f}GB (RAM {mem_total_gb}GB) -> Full In-VRAM Residency"
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
# [Tier 2: Balanced Chunk Buffer (No Layer Streaming)]
|
|
826
|
+
|
|
827
|
+
# Eligible only if available physical DRAM can safely absorb co-resident weights + chunk scratch buffers (~4.5GB+)
|
|
828
|
+
|
|
829
|
+
elif mem_avail_gb >= 4.5 or (mem_avail_gb >= total_co_resident_gb * 0.75 and mem_total_gb >= 9.0):
|
|
830
|
+
|
|
831
|
+
return "vulkan0=1.5", False, f"Tier 2 Balanced Auto-Scale: Avail {mem_avail_gb}GB (RAM {mem_total_gb}GB, Co-res {total_co_resident_gb:.1f}GB) -> 1.5GB Chunk VRAM Buffer"
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
# [Tier 3: Constrained Lifeboat Streaming]
|
|
836
|
+
|
|
837
|
+
# Strictly enforced on memory-constrained devices (e.g. S21 8GB with 3.1GB avail, A35 6GB with 2.0GB avail)
|
|
838
|
+
|
|
839
|
+
# to prevent Android lmkd SIGKILL when heavy LLM and DiT models are co-resident.
|
|
840
|
+
|
|
841
|
+
else:
|
|
842
|
+
|
|
843
|
+
return "vulkan0=1", True, f"Tier 3 Constrained Auto-Scale: Avail {mem_avail_gb}GB < Co-res Safe {required_safe_ram:.1f}GB (RAM {mem_total_gb}GB) -> 1MB Layer-Streaming Lifeboat"
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def generate(
|
|
850
|
+
|
|
851
|
+
prompt: str,
|
|
852
|
+
|
|
853
|
+
model: str = "realistic",
|
|
854
|
+
|
|
855
|
+
negative_prompt: Optional[str] = None,
|
|
856
|
+
|
|
857
|
+
device: str = "auto",
|
|
858
|
+
|
|
859
|
+
steps: Optional[int] = None,
|
|
860
|
+
|
|
861
|
+
cfg_scale: Optional[float] = None,
|
|
862
|
+
|
|
863
|
+
guidance: Optional[float] = None,
|
|
864
|
+
|
|
865
|
+
width: int = 512,
|
|
866
|
+
|
|
867
|
+
height: int = 512,
|
|
868
|
+
|
|
869
|
+
seed: int = -1,
|
|
870
|
+
|
|
871
|
+
threads: Optional[int] = None,
|
|
872
|
+
|
|
873
|
+
output: Optional[Union[str, Path]] = None,
|
|
874
|
+
|
|
875
|
+
sampling_method: Optional[str] = None,
|
|
876
|
+
|
|
877
|
+
schedule: Optional[str] = None,
|
|
878
|
+
|
|
879
|
+
vae_tiling: bool = False,
|
|
880
|
+
|
|
881
|
+
init_img: Optional[Union[str, Path]] = None,
|
|
882
|
+
|
|
883
|
+
strength: Optional[float] = None,
|
|
884
|
+
|
|
885
|
+
lora_dir: Optional[Union[str, Path]] = None,
|
|
886
|
+
|
|
887
|
+
clip_skip: Optional[int] = None,
|
|
888
|
+
|
|
889
|
+
control_net: Optional[Union[str, Path]] = None,
|
|
890
|
+
|
|
891
|
+
control_image: Optional[Union[str, Path]] = None,
|
|
892
|
+
|
|
893
|
+
control_strength: Optional[float] = None,
|
|
894
|
+
|
|
895
|
+
taesd: Optional[Union[str, Path]] = None,
|
|
896
|
+
|
|
897
|
+
llm: Optional[Union[str, Path]] = None,
|
|
898
|
+
|
|
899
|
+
diffusion_model: Optional[Union[str, Path]] = None,
|
|
900
|
+
|
|
901
|
+
vae: Optional[Union[str, Path]] = None,
|
|
902
|
+
|
|
903
|
+
clip_l: Optional[Union[str, Path]] = None,
|
|
904
|
+
|
|
905
|
+
diffusion_fa: bool = False,
|
|
906
|
+
|
|
907
|
+
offload_to_cpu: bool = False,
|
|
908
|
+
|
|
909
|
+
clip_on_cpu: bool = False,
|
|
910
|
+
|
|
911
|
+
vae_on_cpu: bool = False,
|
|
912
|
+
|
|
913
|
+
mmap: bool = True,
|
|
914
|
+
|
|
915
|
+
max_vram: Optional[str] = None,
|
|
916
|
+
|
|
917
|
+
stream_layers: bool = False,
|
|
918
|
+
|
|
919
|
+
params_backend: Optional[str] = None,
|
|
920
|
+
|
|
921
|
+
custom_backend: Optional[str] = None,
|
|
922
|
+
|
|
923
|
+
vae_format: Optional[str] = None,
|
|
924
|
+
|
|
925
|
+
flow_shift: Optional[float] = None,
|
|
926
|
+
|
|
927
|
+
rpc: Optional[str] = None,
|
|
928
|
+
|
|
929
|
+
tensor_split: Optional[str] = None,
|
|
930
|
+
|
|
931
|
+
cluster_rpc_servers: Optional[Union[str, List[str]]] = None,
|
|
932
|
+
|
|
933
|
+
cluster_split_mode: Optional[str] = None,
|
|
934
|
+
|
|
935
|
+
cluster_tensor_split: Optional[str] = None,
|
|
936
|
+
|
|
937
|
+
cluster_vram_budget: Optional[str] = None,
|
|
938
|
+
|
|
939
|
+
detail_faces: bool = False,
|
|
940
|
+
|
|
941
|
+
detailer_strength: float = 0.35,
|
|
942
|
+
|
|
943
|
+
detailer_steps: int = 2,
|
|
944
|
+
|
|
945
|
+
detailer_padding: float = 1.45,
|
|
946
|
+
|
|
947
|
+
export_gallery: bool = True,
|
|
948
|
+
|
|
949
|
+
wake_lock: bool = True,
|
|
950
|
+
|
|
951
|
+
low_ram_guard: bool = True,
|
|
952
|
+
|
|
953
|
+
auto_provision: bool = False,
|
|
954
|
+
|
|
955
|
+
strict_vulkan: bool = False,
|
|
956
|
+
|
|
957
|
+
no_cache: bool = False,
|
|
958
|
+
|
|
959
|
+
progress: bool = False,
|
|
960
|
+
|
|
961
|
+
json_progress: bool = False,
|
|
962
|
+
|
|
963
|
+
progress_file: Optional[Union[str, Path]] = None,
|
|
964
|
+
|
|
965
|
+
progress_callback: Optional[Callable[[ProgressInfo], None]] = None,
|
|
966
|
+
|
|
967
|
+
timeout: int = 1800,
|
|
968
|
+
|
|
969
|
+
extra_args: Optional[List[str]] = None,
|
|
970
|
+
|
|
971
|
+
isolate_text_encoder: bool = False,
|
|
972
|
+
|
|
973
|
+
sequential_stages: bool = False,
|
|
974
|
+
|
|
975
|
+
safe_mode: bool = False,
|
|
976
|
+
|
|
977
|
+
keep_apps_alive: bool = False,
|
|
978
|
+
|
|
979
|
+
gpu_duty_cycle: Optional[int] = None,
|
|
980
|
+
|
|
981
|
+
_cancel_event: Optional[threading.Event] = None,
|
|
982
|
+
|
|
983
|
+
_proc_holder: Optional[list] = None,
|
|
984
|
+
|
|
985
|
+
**extra_kwargs: Any,
|
|
986
|
+
|
|
987
|
+
) -> GenerationResult:
|
|
988
|
+
|
|
989
|
+
"""Generate an AI image on Samsung Galaxy / Android Termux using Bionic native C++ diffusion.
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
|
|
995
|
+
prompt: Detailed text description of the desired image.
|
|
996
|
+
|
|
997
|
+
model: Preset keyword ('realistic', 'speed', 'sdxs', 'turbo', 'anime'), custom repo ('org/repo/file.gguf'), direct URL, or path to .gguf file.
|
|
998
|
+
|
|
999
|
+
negative_prompt: Optional negative text guidance describing elements to avoid (default: None).
|
|
1000
|
+
|
|
1001
|
+
device: Computing device ('auto', 'gpu', 'vulkan', 'cpu', 'opencl', 'npu', 'tpu'). Default is 'auto'.
|
|
1002
|
+
|
|
1003
|
+
steps: Number of denoising steps (default determined by preset, e.g. 10).
|
|
1004
|
+
|
|
1005
|
+
cfg_scale: Classifier-Free Guidance scale (default determined by preset, e.g. 4.0).
|
|
1006
|
+
|
|
1007
|
+
width: Output image width in pixels (default: 512).
|
|
1008
|
+
|
|
1009
|
+
height: Output image height in pixels (default: 512).
|
|
1010
|
+
|
|
1011
|
+
seed: Sampling RNG seed (-1 for random).
|
|
1012
|
+
|
|
1013
|
+
threads: Number of CPU threads (defaults to optimal big-core cluster count, e.g. 4).
|
|
1014
|
+
|
|
1015
|
+
output: Destination output filename or path.
|
|
1016
|
+
|
|
1017
|
+
sampling_method: Sampler algorithm ('euler', 'euler_a', 'heun', 'dpm2', 'dpm++2s_a', 'dpm++2m', 'dpm++2mv2', 'ipndm', 'lcm').
|
|
1018
|
+
|
|
1019
|
+
schedule: Noise schedule algorithm ('default', 'discrete', 'karras', 'exponential', 'ays', 'gits').
|
|
1020
|
+
|
|
1021
|
+
vae_tiling: Whether to enable VAE tiling to reduce peak memory during final image decoding by ~70%.
|
|
1022
|
+
|
|
1023
|
+
init_img: Path to initial image for Img2Img synthesis.
|
|
1024
|
+
|
|
1025
|
+
strength: Img2Img denoising strength (0.0 to 1.0, default: 0.75).
|
|
1026
|
+
|
|
1027
|
+
lora_dir: Directory containing .gguf/.safetensors LoRA adapter weights.
|
|
1028
|
+
|
|
1029
|
+
clip_skip: Number of CLIP layers to skip (1 or 2, default: None).
|
|
1030
|
+
|
|
1031
|
+
control_net: Path to ControlNet model file.
|
|
1032
|
+
|
|
1033
|
+
control_image: Path to ControlNet hint/guide image (e.g. pose, edges).
|
|
1034
|
+
|
|
1035
|
+
control_strength: ControlNet influence strength (0.0 to 2.0, default: 0.9).
|
|
1036
|
+
|
|
1037
|
+
taesd: Path to Tiny AutoEncoder (TAESD) model for ultra-fast VAE decoding.
|
|
1038
|
+
|
|
1039
|
+
rpc: Distributed RPC server addresses for remote worker offloading (e.g. '192.168.0.220:50052').
|
|
1040
|
+
|
|
1041
|
+
tensor_split: Weight offload fraction across distributed nodes (e.g. '50,50' or '60,40').
|
|
1042
|
+
|
|
1043
|
+
export_gallery: Whether to copy image to Samsung Gallery and broadcast media scanner intent.
|
|
1044
|
+
|
|
1045
|
+
wake_lock: Whether to acquire Android CPU WakeLock during generation.
|
|
1046
|
+
|
|
1047
|
+
low_ram_guard: Whether to verify available memory before starting inference.
|
|
1048
|
+
|
|
1049
|
+
auto_provision: Whether to automatically compile the native C++ engine if missing.
|
|
1050
|
+
|
|
1051
|
+
safe_mode: Preserve SurfaceFlinger 60/120fps UI and background apps (nice +10, VAE Tiling, 90% GPU duty cycle).
|
|
1052
|
+
|
|
1053
|
+
keep_apps_alive: Alias for safe_mode.
|
|
1054
|
+
|
|
1055
|
+
gpu_duty_cycle: GPU load percentage cap (e.g. 90 for 90% duty cycle, 10% idle gap for SurfaceFlinger).
|
|
1056
|
+
|
|
1057
|
+
timeout: Maximum inference timeout in seconds (default: 1800s / 30m).
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
Returns:
|
|
1062
|
+
|
|
1063
|
+
GenerationResult: Object containing local path, gallery path, and inference metrics.
|
|
1064
|
+
|
|
1065
|
+
"""
|
|
1066
|
+
|
|
1067
|
+
if not prompt or not str(prompt).strip():
|
|
1068
|
+
|
|
1069
|
+
raise ValueError("Prompt must not be empty.")
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
if keep_apps_alive:
|
|
1074
|
+
|
|
1075
|
+
safe_mode = True
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
if safe_mode:
|
|
1080
|
+
|
|
1081
|
+
if gpu_duty_cycle is None:
|
|
1082
|
+
|
|
1083
|
+
gpu_duty_cycle = 90
|
|
1084
|
+
|
|
1085
|
+
vae_tiling = True
|
|
1086
|
+
|
|
1087
|
+
logger.info(
|
|
1088
|
+
|
|
1089
|
+
"[SafeMode] Active: GPU duty cycle capped at %d%%, VAE tiling active, priority demoted to preserve SurfaceFlinger and background apps.",
|
|
1090
|
+
|
|
1091
|
+
gpu_duty_cycle
|
|
1092
|
+
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
duty_controller = GPUDutyCycleController(
|
|
1098
|
+
|
|
1099
|
+
target_duty_cycle=gpu_duty_cycle if gpu_duty_cycle is not None else 100,
|
|
1100
|
+
|
|
1101
|
+
enabled=(gpu_duty_cycle is not None and gpu_duty_cycle < 100) or safe_mode
|
|
1102
|
+
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
device_mode = str(device or "auto").lower().strip()
|
|
1108
|
+
|
|
1109
|
+
if device_mode not in ("cpu", "gpu", "opencl", "vulkan", "npu", "tpu", "auto"):
|
|
1110
|
+
|
|
1111
|
+
raise ValueError(f"Invalid device '{device}'. Options: 'auto', 'gpu', 'vulkan', 'cpu', 'opencl', 'npu', 'tpu'.")
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
# Resolve device to actual available backend using hardware probing
|
|
1116
|
+
|
|
1117
|
+
from .hardware import resolve_device_backend, get_sd_cli_gpu_args, get_profile_gating_manager
|
|
1118
|
+
|
|
1119
|
+
effective_device, ngl_layers = resolve_device_backend(device_mode)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
# 0. Runtime Profile & Model Gating Validation (Zero Test Illusion)
|
|
1124
|
+
|
|
1125
|
+
gating_mgr = get_profile_gating_manager()
|
|
1126
|
+
|
|
1127
|
+
user_explicit_gpu = device_mode in ("vulkan", "gpu")
|
|
1128
|
+
|
|
1129
|
+
is_safe, gate_reason = gating_mgr.validate_execution(model, effective_device, user_explicit=user_explicit_gpu)
|
|
1130
|
+
|
|
1131
|
+
if not is_safe:
|
|
1132
|
+
|
|
1133
|
+
from .exceptions import PlatformNotSupportedError
|
|
1134
|
+
|
|
1135
|
+
raise PlatformNotSupportedError(
|
|
1136
|
+
|
|
1137
|
+
f"[termux-diffusion] [GATING-BLOCKED] {gate_reason}"
|
|
1138
|
+
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
# 1. Pre-flight Memory Safety Inspection (Informational only, non-blocking)
|
|
1144
|
+
|
|
1145
|
+
if low_ram_guard:
|
|
1146
|
+
|
|
1147
|
+
safe, msg = check_memory_safety(required_mb=1200)
|
|
1148
|
+
|
|
1149
|
+
if not safe:
|
|
1150
|
+
|
|
1151
|
+
logger.warning("Low RAM Warning: %s", msg)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
# 2. Locate or Auto-provision Native sd-cli Engine FIRST (before downloading 1.5GB model weights)
|
|
1156
|
+
|
|
1157
|
+
sd_cli = locate_sd_cli(effective_device)
|
|
1158
|
+
|
|
1159
|
+
if not sd_cli:
|
|
1160
|
+
|
|
1161
|
+
if auto_provision:
|
|
1162
|
+
|
|
1163
|
+
logger.info("sd-cli binary not found in standard paths. Attempting auto-provisioning as requested...")
|
|
1164
|
+
|
|
1165
|
+
sd_cli = provision_engine(backend=effective_device)
|
|
1166
|
+
|
|
1167
|
+
else:
|
|
1168
|
+
|
|
1169
|
+
logger.error("sd-cli binary not found in standard paths and auto_provision=False.")
|
|
1170
|
+
|
|
1171
|
+
raise ProvisioningError(
|
|
1172
|
+
|
|
1173
|
+
"Native 'sd-cli' binary not found on this system. "
|
|
1174
|
+
|
|
1175
|
+
"Please run 'termux-diffusion doctor --install' or pass auto_provision=True to generate()."
|
|
1176
|
+
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
# 2.1 Strict Vulkan / GPU Validation (Fail-Fast: No Silent CPU Fallback)
|
|
1182
|
+
|
|
1183
|
+
if device_mode in ("vulkan", "gpu") or strict_vulkan:
|
|
1184
|
+
|
|
1185
|
+
from .selftest import run_binary_self_test
|
|
1186
|
+
|
|
1187
|
+
from .exceptions import PlatformNotSupportedError
|
|
1188
|
+
|
|
1189
|
+
test_res = run_binary_self_test(sd_cli, expected_backend="vulkan")
|
|
1190
|
+
|
|
1191
|
+
if not test_res.stage1_load_passed and test_res.error_message:
|
|
1192
|
+
|
|
1193
|
+
raise PlatformNotSupportedError(
|
|
1194
|
+
|
|
1195
|
+
f"Strict Vulkan execution mode requested (device='{device}'), "
|
|
1196
|
+
|
|
1197
|
+
f"but the active binary '{sd_cli.name}' failed Vulkan validation: {test_res.error_message}. "
|
|
1198
|
+
|
|
1199
|
+
"Execution halted strictly without silent CPU fallback."
|
|
1200
|
+
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
# 3. Model Weight Resolution and Local Caching
|
|
1206
|
+
|
|
1207
|
+
if isinstance(model, dict):
|
|
1208
|
+
|
|
1209
|
+
if "shift" in model and flow_shift is None:
|
|
1210
|
+
|
|
1211
|
+
flow_shift = model["shift"]
|
|
1212
|
+
|
|
1213
|
+
model = model.get("model", "realistic")
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
model_str = str(model).lower().strip()
|
|
1218
|
+
|
|
1219
|
+
is_z_image = "z-image" in model_str or "z_image" in model_str or "turbo-6b" in model_str or (diffusion_model and "z_image" in str(diffusion_model).lower())
|
|
1220
|
+
|
|
1221
|
+
if is_z_image:
|
|
1222
|
+
|
|
1223
|
+
if diffusion_model is None:
|
|
1224
|
+
|
|
1225
|
+
if model_str.endswith(".gguf") or model_str.endswith(".safetensors") or "/" in model_str:
|
|
1226
|
+
|
|
1227
|
+
diffusion_model = model
|
|
1228
|
+
|
|
1229
|
+
else:
|
|
1230
|
+
|
|
1231
|
+
diffusion_model = "z_image_turbo-Q2_K.gguf"
|
|
1232
|
+
|
|
1233
|
+
if llm is None:
|
|
1234
|
+
|
|
1235
|
+
llm = "Qwen3-4B-Instruct-2507-Q2_K.gguf"
|
|
1236
|
+
|
|
1237
|
+
if vae is None and taesd is None:
|
|
1238
|
+
|
|
1239
|
+
taef1_candidate = Path(get_default_cache_dir()) / "models" / "taef1.safetensors"
|
|
1240
|
+
|
|
1241
|
+
if taef1_candidate.is_file():
|
|
1242
|
+
|
|
1243
|
+
taesd = "taef1.safetensors"
|
|
1244
|
+
|
|
1245
|
+
else:
|
|
1246
|
+
|
|
1247
|
+
vae = "z_image_ae.safetensors"
|
|
1248
|
+
|
|
1249
|
+
if vae_format is None:
|
|
1250
|
+
|
|
1251
|
+
vae_format = "flux"
|
|
1252
|
+
|
|
1253
|
+
if steps is None or steps == 10:
|
|
1254
|
+
|
|
1255
|
+
steps = 4
|
|
1256
|
+
|
|
1257
|
+
if cfg_scale is None or cfg_scale == 4.0:
|
|
1258
|
+
|
|
1259
|
+
cfg_scale = 1.0
|
|
1260
|
+
|
|
1261
|
+
if sampling_method is None:
|
|
1262
|
+
|
|
1263
|
+
sampling_method = "euler"
|
|
1264
|
+
|
|
1265
|
+
if schedule == "flux":
|
|
1266
|
+
|
|
1267
|
+
schedule = None
|
|
1268
|
+
|
|
1269
|
+
guidance = None
|
|
1270
|
+
|
|
1271
|
+
# Auto-apply mobile Vulkan memory-safe acceleration flags for 6B DiT
|
|
1272
|
+
|
|
1273
|
+
if device_mode in ("vulkan", "gpu") or effective_device in ("vulkan", "gpu"):
|
|
1274
|
+
|
|
1275
|
+
clip_on_cpu = True
|
|
1276
|
+
|
|
1277
|
+
vae_on_cpu = True
|
|
1278
|
+
|
|
1279
|
+
diffusion_fa = True
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
# Autonomous Hardware-Aware VRAM Scaling (Co-resident Multi-Component Aware)
|
|
1284
|
+
|
|
1285
|
+
model_size = get_model_file_size(diffusion_model or model)
|
|
1286
|
+
|
|
1287
|
+
llm_size = get_model_file_size(llm) if llm else 0
|
|
1288
|
+
|
|
1289
|
+
vae_size = get_model_file_size(vae) if vae else (get_model_file_size(taesd) if taesd else 0)
|
|
1290
|
+
|
|
1291
|
+
max_vram, stream_layers, policy_reason = resolve_vram_and_streaming_policy(
|
|
1292
|
+
|
|
1293
|
+
user_max_vram=max_vram,
|
|
1294
|
+
|
|
1295
|
+
user_stream_layers=stream_layers,
|
|
1296
|
+
|
|
1297
|
+
model_bytes=model_size,
|
|
1298
|
+
|
|
1299
|
+
llm_bytes=llm_size,
|
|
1300
|
+
|
|
1301
|
+
vae_bytes=vae_size,
|
|
1302
|
+
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
if params_backend is None:
|
|
1306
|
+
|
|
1307
|
+
params_backend = "diffusion=cpu"
|
|
1308
|
+
safe_mode = True
|
|
1309
|
+
if gpu_duty_cycle is None:
|
|
1310
|
+
gpu_duty_cycle = 90
|
|
1311
|
+
vae_tiling = True
|
|
1312
|
+
|
|
1313
|
+
logger.info("[termux-diffusion] [VRAM Policy] %s", policy_reason)
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
if diffusion_model is not None and str(diffusion_model).strip():
|
|
1318
|
+
|
|
1319
|
+
model_path = None
|
|
1320
|
+
|
|
1321
|
+
else:
|
|
1322
|
+
|
|
1323
|
+
model_path = resolve_model_path(model)
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
# 4. Resolve CPU Thread Allocation and Sampling Steps
|
|
1328
|
+
|
|
1329
|
+
presets = list_presets()
|
|
1330
|
+
|
|
1331
|
+
preset_info = presets.get(str(model), {})
|
|
1332
|
+
|
|
1333
|
+
if steps is None:
|
|
1334
|
+
|
|
1335
|
+
steps = preset_info.get("default_steps", 10)
|
|
1336
|
+
|
|
1337
|
+
if cfg_scale is None:
|
|
1338
|
+
|
|
1339
|
+
cfg_scale = preset_info.get("default_cfg", 4.0)
|
|
1340
|
+
|
|
1341
|
+
if schedule is None:
|
|
1342
|
+
|
|
1343
|
+
schedule = preset_info.get("default_schedule")
|
|
1344
|
+
|
|
1345
|
+
if sampling_method is None:
|
|
1346
|
+
|
|
1347
|
+
sampling_method = preset_info.get("default_sampler")
|
|
1348
|
+
|
|
1349
|
+
effective_guidance = guidance if guidance is not None else preset_info.get("default_guidance")
|
|
1350
|
+
|
|
1351
|
+
if threads is None:
|
|
1352
|
+
|
|
1353
|
+
threads = get_optimal_thread_count()
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
# Determine default or explicit negative prompt
|
|
1358
|
+
|
|
1359
|
+
effective_negative = negative_prompt
|
|
1360
|
+
|
|
1361
|
+
if effective_negative is None:
|
|
1362
|
+
|
|
1363
|
+
effective_negative = get_default_negative_prompt()
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
# 4. Determine Output Destination
|
|
1368
|
+
|
|
1369
|
+
timestamp = int(time.time())
|
|
1370
|
+
|
|
1371
|
+
if output:
|
|
1372
|
+
|
|
1373
|
+
out_path = Path(output).resolve()
|
|
1374
|
+
|
|
1375
|
+
else:
|
|
1376
|
+
|
|
1377
|
+
out_dir = get_galaxy_gallery_dir()
|
|
1378
|
+
|
|
1379
|
+
out_path = out_dir / f"ai_gen_{timestamp}.png"
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
# 4.3 Prompt Embedding Cache & Assetization Management
|
|
1388
|
+
|
|
1389
|
+
import hashlib
|
|
1390
|
+
|
|
1391
|
+
prompt_clean = str(prompt).strip()
|
|
1392
|
+
|
|
1393
|
+
prompt_hash = hashlib.sha256(prompt_clean.encode("utf-8")).hexdigest()[:16]
|
|
1394
|
+
|
|
1395
|
+
prompt_cache_dir = get_default_cache_dir() / "prompt_cache"
|
|
1396
|
+
|
|
1397
|
+
prompt_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
1398
|
+
|
|
1399
|
+
prompt_cache_meta = prompt_cache_dir / f"{prompt_hash}.json"
|
|
1400
|
+
|
|
1401
|
+
bundled_prompt_meta = Path(__file__).parent / "data" / "prompt_cache" / f"{prompt_hash}.json"
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
is_cached = bundled_prompt_meta.is_file() or prompt_cache_meta.is_file()
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
if no_cache:
|
|
1410
|
+
|
|
1411
|
+
logger.info("[termux-diffusion] Prompt cache explicitly bypassed (--no-cache/--non-cache).")
|
|
1412
|
+
|
|
1413
|
+
print("[termux-diffusion] [Prompt Cache] Bypassed (--no-cache). Running live text encoder on device.")
|
|
1414
|
+
|
|
1415
|
+
elif is_cached:
|
|
1416
|
+
|
|
1417
|
+
logger.info("[termux-diffusion] Prompt cache hit: %s", prompt_hash)
|
|
1418
|
+
|
|
1419
|
+
print(f"[termux-diffusion] [Prompt Cache] Hit (hash: {prompt_hash})! Reusing verified pre-cached prompt embedding.")
|
|
1420
|
+
|
|
1421
|
+
else:
|
|
1422
|
+
|
|
1423
|
+
logger.info("[termux-diffusion] Prompt cache miss (%s). Running live text encoder.", prompt_hash)
|
|
1424
|
+
|
|
1425
|
+
print(f"[termux-diffusion] [Prompt Cache] Miss (hash: {prompt_hash}). Running live text encoder.")
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
# 5. Build Subprocess Command List (100% List argv, Zero Shell Injection Vector)
|
|
1430
|
+
|
|
1431
|
+
sanitized_prompt = prompt_clean.replace("\x00", "").replace("\r\n", " ").replace("\n", " ")
|
|
1432
|
+
|
|
1433
|
+
cmd = [
|
|
1434
|
+
|
|
1435
|
+
str(sd_cli),
|
|
1436
|
+
|
|
1437
|
+
"-p", sanitized_prompt,
|
|
1438
|
+
|
|
1439
|
+
"-W", str(width),
|
|
1440
|
+
|
|
1441
|
+
"-H", str(height),
|
|
1442
|
+
|
|
1443
|
+
"-t", str(threads),
|
|
1444
|
+
|
|
1445
|
+
"--steps", str(steps),
|
|
1446
|
+
|
|
1447
|
+
"--cfg-scale", str(cfg_scale),
|
|
1448
|
+
|
|
1449
|
+
"-o", str(out_path)
|
|
1450
|
+
|
|
1451
|
+
]
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
if diffusion_model is not None and str(diffusion_model).strip():
|
|
1456
|
+
|
|
1457
|
+
cmd.extend(["--diffusion-model", str(resolve_model_path(str(diffusion_model)))])
|
|
1458
|
+
|
|
1459
|
+
else:
|
|
1460
|
+
|
|
1461
|
+
cmd.extend(["-m", str(model_path)])
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
if llm is not None and str(llm).strip():
|
|
1466
|
+
|
|
1467
|
+
cmd.extend(["--llm", str(resolve_model_path(str(llm)))])
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
# 4.1 Resolve VAE or fast TAESD (AutoEncoder)
|
|
1472
|
+
|
|
1473
|
+
effective_taesd_path = None
|
|
1474
|
+
|
|
1475
|
+
if taesd is not None and str(taesd).strip():
|
|
1476
|
+
|
|
1477
|
+
taesd_str = str(taesd).strip().lower()
|
|
1478
|
+
|
|
1479
|
+
if taesd_str in ("auto", "true", "1", "taef1", "flux"):
|
|
1480
|
+
|
|
1481
|
+
auto_candidates = [
|
|
1482
|
+
|
|
1483
|
+
Path(get_default_cache_dir()) / "models" / "taef1.safetensors",
|
|
1484
|
+
|
|
1485
|
+
Path(os.environ.get("HOME", "/data/data/com.termux/files/home")) / ".cache" / "termux-diffusion" / "models" / "taef1.safetensors",
|
|
1486
|
+
|
|
1487
|
+
Path(get_default_cache_dir()) / "models" / "taesd.safetensors",
|
|
1488
|
+
|
|
1489
|
+
]
|
|
1490
|
+
|
|
1491
|
+
for c in auto_candidates:
|
|
1492
|
+
|
|
1493
|
+
if c.is_file():
|
|
1494
|
+
|
|
1495
|
+
effective_taesd_path = c.resolve()
|
|
1496
|
+
|
|
1497
|
+
break
|
|
1498
|
+
|
|
1499
|
+
if effective_taesd_path is None:
|
|
1500
|
+
|
|
1501
|
+
raise FileNotFoundError(
|
|
1502
|
+
|
|
1503
|
+
f"[termux-diffusion] Auto TAESD requested, but 'taef1.safetensors' was not found in cache.\n"
|
|
1504
|
+
|
|
1505
|
+
f" -> Checked paths: {[str(p) for p in auto_candidates]}\n"
|
|
1506
|
+
|
|
1507
|
+
f" -> Remedy: Download taef1.safetensors or specify path via --taesd <path>."
|
|
1508
|
+
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
else:
|
|
1512
|
+
|
|
1513
|
+
p = Path(taesd).resolve()
|
|
1514
|
+
|
|
1515
|
+
if p.is_file():
|
|
1516
|
+
|
|
1517
|
+
effective_taesd_path = p
|
|
1518
|
+
|
|
1519
|
+
else:
|
|
1520
|
+
|
|
1521
|
+
cache_taesd = Path(get_default_cache_dir()) / "models" / str(taesd)
|
|
1522
|
+
|
|
1523
|
+
if cache_taesd.is_file():
|
|
1524
|
+
|
|
1525
|
+
effective_taesd_path = cache_taesd.resolve()
|
|
1526
|
+
|
|
1527
|
+
else:
|
|
1528
|
+
|
|
1529
|
+
raise FileNotFoundError(
|
|
1530
|
+
|
|
1531
|
+
f"[termux-diffusion] TAESD fast VAE model file not found: '{taesd}'\n"
|
|
1532
|
+
|
|
1533
|
+
f" -> Remedy: Check TAESD file path or omit --taesd to use standard built-in VAE."
|
|
1534
|
+
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
if effective_taesd_path is not None:
|
|
1540
|
+
|
|
1541
|
+
cmd.extend(["--taesd", str(effective_taesd_path)])
|
|
1542
|
+
|
|
1543
|
+
logger.info("[termux-diffusion] Fast VAE enabled: Using TAESD '%s' (skipping heavy VAE)", effective_taesd_path.name)
|
|
1544
|
+
|
|
1545
|
+
elif vae is not None and str(vae).strip():
|
|
1546
|
+
|
|
1547
|
+
cmd.extend(["--vae", str(resolve_model_path(str(vae)))])
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
if clip_l is not None and str(clip_l).strip():
|
|
1552
|
+
|
|
1553
|
+
cmd.extend(["--clip_l", str(resolve_model_path(str(clip_l)))])
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
if offload_to_cpu:
|
|
1558
|
+
|
|
1559
|
+
cmd.append("--offload-to-cpu")
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
if clip_on_cpu:
|
|
1564
|
+
|
|
1565
|
+
cmd.append("--clip-on-cpu")
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
if vae_on_cpu:
|
|
1570
|
+
|
|
1571
|
+
cmd.append("--vae-on-cpu")
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
if vae_format is not None and str(vae_format).strip():
|
|
1576
|
+
|
|
1577
|
+
cmd.extend(["--vae-format", str(vae_format).strip()])
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
if mmap:
|
|
1582
|
+
|
|
1583
|
+
cmd.append("--mmap")
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
if diffusion_fa:
|
|
1588
|
+
|
|
1589
|
+
cmd.append("--diffusion-fa")
|
|
1590
|
+
|
|
1591
|
+
if effective_guidance is not None:
|
|
1592
|
+
|
|
1593
|
+
cmd.extend(["--guidance", str(effective_guidance)])
|
|
1594
|
+
|
|
1595
|
+
if effective_negative:
|
|
1596
|
+
|
|
1597
|
+
sanitized_neg = str(effective_negative).replace("\x00", "").replace("\r\n", " ").replace("\n", " ").strip()
|
|
1598
|
+
|
|
1599
|
+
cmd.extend(["-n", sanitized_neg])
|
|
1600
|
+
|
|
1601
|
+
if seed >= 0:
|
|
1602
|
+
|
|
1603
|
+
cmd.extend(["--seed", str(seed)])
|
|
1604
|
+
|
|
1605
|
+
# Append GPU offloading args from hardware detection
|
|
1606
|
+
|
|
1607
|
+
cmd.extend(get_sd_cli_gpu_args(effective_device, ngl_layers))
|
|
1608
|
+
|
|
1609
|
+
if custom_backend is not None and str(custom_backend).strip():
|
|
1610
|
+
|
|
1611
|
+
cmd.extend(["--backend", str(custom_backend).strip()])
|
|
1612
|
+
|
|
1613
|
+
elif effective_device in ("vulkan", "gpu"):
|
|
1614
|
+
|
|
1615
|
+
b_clip = "cpu" if clip_on_cpu else "vulkan0"
|
|
1616
|
+
|
|
1617
|
+
b_vae = "cpu" if vae_on_cpu else "vulkan0"
|
|
1618
|
+
|
|
1619
|
+
cmd.extend(["--backend", f"clip={b_clip},diffusion=vulkan0,vae={b_vae}"])
|
|
1620
|
+
|
|
1621
|
+
elif effective_device in ("opencl", "cl"):
|
|
1622
|
+
|
|
1623
|
+
b_clip = "cpu" if clip_on_cpu else "cpu"
|
|
1624
|
+
|
|
1625
|
+
b_vae = "cpu" if vae_on_cpu else "cpu"
|
|
1626
|
+
|
|
1627
|
+
cmd.extend(["--backend", f"clip={b_clip},diffusion=GPUOpenCL,vae={b_vae}"])
|
|
1628
|
+
|
|
1629
|
+
if params_backend is None:
|
|
1630
|
+
|
|
1631
|
+
cmd.extend(["--params-backend", "clip=disk,diffusion=cpu"])
|
|
1632
|
+
|
|
1633
|
+
if max_vram is None:
|
|
1634
|
+
|
|
1635
|
+
cmd.extend(["--max-vram", "GPUOpenCL=1.0"])
|
|
1636
|
+
|
|
1637
|
+
cmd.append("--disable-prefetch")
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
# --- Unified Distributed RPC Cluster Configuration ---
|
|
1642
|
+
|
|
1643
|
+
effective_rpc = cluster_rpc_servers if cluster_rpc_servers is not None else rpc
|
|
1644
|
+
|
|
1645
|
+
cluster_nodes: List[str] = []
|
|
1646
|
+
|
|
1647
|
+
if effective_rpc is not None and str(effective_rpc).strip():
|
|
1648
|
+
|
|
1649
|
+
cluster_nodes = parse_cluster_rpc_spec(effective_rpc)
|
|
1650
|
+
|
|
1651
|
+
if cluster_nodes:
|
|
1652
|
+
|
|
1653
|
+
# Pre-flight check: verify TCP connectivity. Raises ClusterConnectionError if unreachable.
|
|
1654
|
+
|
|
1655
|
+
verify_rpc_cluster_nodes(cluster_nodes, timeout=3.0)
|
|
1656
|
+
|
|
1657
|
+
cmd.extend(["--rpc-servers", ",".join(cluster_nodes)])
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
effective_split_mode = cluster_split_mode if cluster_split_mode is not None else (cluster_tensor_split or tensor_split)
|
|
1662
|
+
|
|
1663
|
+
if effective_split_mode is not None and str(effective_split_mode).strip():
|
|
1664
|
+
|
|
1665
|
+
sm_val = str(effective_split_mode).strip().lower()
|
|
1666
|
+
|
|
1667
|
+
if sm_val in ("layer", "row"):
|
|
1668
|
+
|
|
1669
|
+
cmd.extend(["--split-mode", sm_val])
|
|
1670
|
+
|
|
1671
|
+
else:
|
|
1672
|
+
|
|
1673
|
+
logger.info("tensor_split='%s' specified; sd-cli balances across --rpc-servers directly.", effective_split_mode)
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
|
|
1677
|
+
effective_max_vram = cluster_vram_budget or max_vram
|
|
1678
|
+
|
|
1679
|
+
if effective_max_vram is not None and str(effective_max_vram).strip() not in ("", "0", "none", "disable", "unlimited", "vulkan0=0"):
|
|
1680
|
+
|
|
1681
|
+
cmd.extend(["--max-vram", str(effective_max_vram).strip()])
|
|
1682
|
+
if stream_layers:
|
|
1683
|
+
cmd.append("--stream-layers")
|
|
1684
|
+
if params_backend is not None and str(params_backend).strip():
|
|
1685
|
+
cmd.extend(["--params-backend", str(params_backend).strip()])
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
# --- Advanced TOP 7 Parameters Integration & Defense ---
|
|
1690
|
+
|
|
1691
|
+
effective_sampler = None
|
|
1692
|
+
|
|
1693
|
+
if sampling_method is not None and str(sampling_method).strip():
|
|
1694
|
+
|
|
1695
|
+
sm_clean = str(sampling_method).lower().strip()
|
|
1696
|
+
|
|
1697
|
+
if sm_clean in VALID_SAMPLERS:
|
|
1698
|
+
|
|
1699
|
+
effective_sampler = sm_clean
|
|
1700
|
+
|
|
1701
|
+
cmd.extend(["--sampling-method", sm_clean])
|
|
1702
|
+
|
|
1703
|
+
else:
|
|
1704
|
+
|
|
1705
|
+
logger.warning(
|
|
1706
|
+
|
|
1707
|
+
"Invalid sampling_method '%s'; falling back to engine default ('euler_a'). Valid: %s",
|
|
1708
|
+
|
|
1709
|
+
sampling_method, ", ".join(sorted(VALID_SAMPLERS))
|
|
1710
|
+
|
|
1711
|
+
)
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
effective_schedule = None
|
|
1716
|
+
|
|
1717
|
+
if schedule is not None and str(schedule).strip():
|
|
1718
|
+
|
|
1719
|
+
sc_clean = str(schedule).lower().strip()
|
|
1720
|
+
|
|
1721
|
+
if sc_clean in VALID_SCHEDULERS:
|
|
1722
|
+
|
|
1723
|
+
effective_schedule = sc_clean
|
|
1724
|
+
|
|
1725
|
+
if sc_clean != "default":
|
|
1726
|
+
|
|
1727
|
+
cmd.extend(["--scheduler", sc_clean])
|
|
1728
|
+
|
|
1729
|
+
else:
|
|
1730
|
+
|
|
1731
|
+
logger.warning(
|
|
1732
|
+
|
|
1733
|
+
"Invalid schedule '%s'; falling back to engine default ('default'). Valid: %s",
|
|
1734
|
+
|
|
1735
|
+
schedule, ", ".join(sorted(VALID_SCHEDULERS))
|
|
1736
|
+
|
|
1737
|
+
)
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
if flow_shift is not None:
|
|
1742
|
+
|
|
1743
|
+
cmd.extend(["--flow-shift", str(flow_shift)])
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
if vae_tiling:
|
|
1748
|
+
|
|
1749
|
+
cmd.append("--vae-tiling")
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
effective_init_path = None
|
|
1754
|
+
|
|
1755
|
+
effective_strength = None
|
|
1756
|
+
|
|
1757
|
+
if init_img is not None and str(init_img).strip():
|
|
1758
|
+
|
|
1759
|
+
effective_init_path = Path(init_img).resolve()
|
|
1760
|
+
|
|
1761
|
+
if not effective_init_path.is_file():
|
|
1762
|
+
|
|
1763
|
+
raise FileNotFoundError(
|
|
1764
|
+
|
|
1765
|
+
f"[termux-diffusion] Img2Img source image file does not exist.\n"
|
|
1766
|
+
|
|
1767
|
+
f" -> Input parameter: '{init_img}'\n"
|
|
1768
|
+
|
|
1769
|
+
f" -> Resolved absolute path: '{effective_init_path}'\n"
|
|
1770
|
+
|
|
1771
|
+
f" -> Remedy: Check if the image file exists and Termux storage permission ('termux-setup-storage') is granted."
|
|
1772
|
+
|
|
1773
|
+
)
|
|
1774
|
+
|
|
1775
|
+
cmd.extend(["-i", str(effective_init_path)])
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
# Denoising strength for Img2Img
|
|
1780
|
+
|
|
1781
|
+
if strength is not None:
|
|
1782
|
+
|
|
1783
|
+
raw_s = float(strength)
|
|
1784
|
+
|
|
1785
|
+
if raw_s < 0.0:
|
|
1786
|
+
|
|
1787
|
+
logger.warning("Img2Img strength (%s) is below 0.0; auto-clamping to 0.0 (minimal change).", raw_s)
|
|
1788
|
+
|
|
1789
|
+
effective_strength = 0.0
|
|
1790
|
+
|
|
1791
|
+
elif raw_s > 1.0:
|
|
1792
|
+
|
|
1793
|
+
logger.warning("Img2Img strength (%s) is above 1.0; auto-clamping to 1.0 (full regeneration).", raw_s)
|
|
1794
|
+
|
|
1795
|
+
effective_strength = 1.0
|
|
1796
|
+
|
|
1797
|
+
else:
|
|
1798
|
+
|
|
1799
|
+
effective_strength = raw_s
|
|
1800
|
+
|
|
1801
|
+
else:
|
|
1802
|
+
|
|
1803
|
+
effective_strength = 0.75 # Standard Img2Img default
|
|
1804
|
+
|
|
1805
|
+
cmd.extend(["--strength", str(effective_strength)])
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
effective_lora_path = None
|
|
1810
|
+
|
|
1811
|
+
if lora_dir is not None and str(lora_dir).strip():
|
|
1812
|
+
|
|
1813
|
+
effective_lora_path = Path(lora_dir).resolve()
|
|
1814
|
+
|
|
1815
|
+
if not effective_lora_path.is_dir():
|
|
1816
|
+
|
|
1817
|
+
raise FileNotFoundError(
|
|
1818
|
+
|
|
1819
|
+
f"[termux-diffusion] LoRA weights directory not found.\n"
|
|
1820
|
+
|
|
1821
|
+
f" -> Input parameter: '{lora_dir}'\n"
|
|
1822
|
+
|
|
1823
|
+
f" -> Resolved absolute path: '{effective_lora_path}'\n"
|
|
1824
|
+
|
|
1825
|
+
f" -> Remedy: Create directory or verify location containing .safetensors/.gguf LoRA adapters."
|
|
1826
|
+
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
cmd.extend(["--lora-model-dir", str(effective_lora_path)])
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
effective_clip_skip = None
|
|
1834
|
+
|
|
1835
|
+
if clip_skip is not None:
|
|
1836
|
+
|
|
1837
|
+
try:
|
|
1838
|
+
|
|
1839
|
+
cs_val = int(clip_skip)
|
|
1840
|
+
|
|
1841
|
+
if cs_val < 1:
|
|
1842
|
+
|
|
1843
|
+
logger.warning("clip_skip (%s) is below 1; auto-clamping to 1.", cs_val)
|
|
1844
|
+
|
|
1845
|
+
effective_clip_skip = 1
|
|
1846
|
+
|
|
1847
|
+
elif cs_val > 2:
|
|
1848
|
+
|
|
1849
|
+
logger.warning("clip_skip (%s) is above standard maximum 2; auto-clamping to 2.", cs_val)
|
|
1850
|
+
|
|
1851
|
+
effective_clip_skip = 2
|
|
1852
|
+
|
|
1853
|
+
else:
|
|
1854
|
+
|
|
1855
|
+
effective_clip_skip = cs_val
|
|
1856
|
+
|
|
1857
|
+
cmd.extend(["--clip-skip", str(effective_clip_skip)])
|
|
1858
|
+
|
|
1859
|
+
except (ValueError, TypeError):
|
|
1860
|
+
|
|
1861
|
+
logger.warning("Invalid clip_skip value '%s'; skipping flag.", clip_skip)
|
|
1862
|
+
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
effective_cnet_path = None
|
|
1866
|
+
|
|
1867
|
+
effective_cimg_path = None
|
|
1868
|
+
|
|
1869
|
+
effective_cstrength = None
|
|
1870
|
+
|
|
1871
|
+
if control_net is not None and str(control_net).strip():
|
|
1872
|
+
|
|
1873
|
+
effective_cnet_path = Path(control_net).resolve()
|
|
1874
|
+
|
|
1875
|
+
if not effective_cnet_path.is_file():
|
|
1876
|
+
|
|
1877
|
+
raise FileNotFoundError(
|
|
1878
|
+
|
|
1879
|
+
f"[termux-diffusion] ControlNet model file not found.\n"
|
|
1880
|
+
|
|
1881
|
+
f" -> Input parameter: '{control_net}'\n"
|
|
1882
|
+
|
|
1883
|
+
f" -> Resolved absolute path: '{effective_cnet_path}'\n"
|
|
1884
|
+
|
|
1885
|
+
f" -> Remedy: Place valid ControlNet GGUF/model file at specified path."
|
|
1886
|
+
|
|
1887
|
+
)
|
|
1888
|
+
|
|
1889
|
+
cmd.extend(["--control-net", str(effective_cnet_path)])
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
|
|
1893
|
+
if control_image is not None and str(control_image).strip():
|
|
1894
|
+
|
|
1895
|
+
effective_cimg_path = Path(control_image).resolve()
|
|
1896
|
+
|
|
1897
|
+
if not effective_cimg_path.is_file():
|
|
1898
|
+
|
|
1899
|
+
raise FileNotFoundError(
|
|
1900
|
+
|
|
1901
|
+
f"[termux-diffusion] ControlNet guide/hint image not found.\n"
|
|
1902
|
+
|
|
1903
|
+
f" -> Input parameter: '{control_image}'\n"
|
|
1904
|
+
|
|
1905
|
+
f" -> Resolved absolute path: '{effective_cimg_path}'\n"
|
|
1906
|
+
|
|
1907
|
+
f" -> Remedy: Check existence of pose/edge reference image."
|
|
1908
|
+
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1911
|
+
cmd.extend(["--control-image", str(effective_cimg_path)])
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
if control_strength is not None:
|
|
1916
|
+
|
|
1917
|
+
raw_cs = float(control_strength)
|
|
1918
|
+
|
|
1919
|
+
if raw_cs < 0.0:
|
|
1920
|
+
|
|
1921
|
+
logger.warning("ControlNet strength (%s) is below 0.0; auto-clamping to 0.0.", raw_cs)
|
|
1922
|
+
|
|
1923
|
+
effective_cstrength = 0.0
|
|
1924
|
+
|
|
1925
|
+
elif raw_cs > 2.0:
|
|
1926
|
+
|
|
1927
|
+
logger.warning("ControlNet strength (%s) is above 2.0; auto-clamping to 2.0.", raw_cs)
|
|
1928
|
+
|
|
1929
|
+
effective_cstrength = 2.0
|
|
1930
|
+
|
|
1931
|
+
else:
|
|
1932
|
+
|
|
1933
|
+
effective_cstrength = raw_cs
|
|
1934
|
+
|
|
1935
|
+
else:
|
|
1936
|
+
|
|
1937
|
+
effective_cstrength = 0.9
|
|
1938
|
+
|
|
1939
|
+
cmd.extend(["--control-strength", str(effective_cstrength)])
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
# 5.0.1 Transparent Sub-Engine Option Forwarding & Extra Argument Merging
|
|
1944
|
+
|
|
1945
|
+
all_forwarded: List[str] = []
|
|
1946
|
+
|
|
1947
|
+
if extra_args:
|
|
1948
|
+
|
|
1949
|
+
all_forwarded.extend([str(a) for a in extra_args if str(a).strip()])
|
|
1950
|
+
|
|
1951
|
+
if extra_kwargs:
|
|
1952
|
+
|
|
1953
|
+
all_forwarded.extend(ClusterOptionForwarder.normalize_forward_args(extra_kwargs))
|
|
1954
|
+
|
|
1955
|
+
if all_forwarded:
|
|
1956
|
+
|
|
1957
|
+
cmd = ClusterOptionForwarder.merge_pass_through_args(cmd, all_forwarded)
|
|
1958
|
+
|
|
1959
|
+
logger.info("[termux-diffusion] Merged %d pass-through / forwarded CLI options into engine argv.", len(all_forwarded))
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
# 5.1 Configure Environment with companion library search paths (Termux native isolation)
|
|
1964
|
+
|
|
1965
|
+
from .platform import get_clean_execution_env
|
|
1966
|
+
|
|
1967
|
+
env = get_clean_execution_env(backend=effective_device)
|
|
1968
|
+
|
|
1969
|
+
# Bypass debug CPU shadow checks on mobile Vulkan to unlock 10x throughput
|
|
1970
|
+
|
|
1971
|
+
env.setdefault("GGML_VULKAN_SKIP_CHECKS", "999999999")
|
|
1972
|
+
|
|
1973
|
+
# Throttle Vulkan nodes per submit to prevent Mali GPU kernel driver queue stall (do_sys_poll)
|
|
1974
|
+
|
|
1975
|
+
env.setdefault("GGML_VK_MAX_NODES_PER_SUBMIT", "16")
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
# 5.2 ameva-runtime DiffusionAdapter Binding (Vulkan / GPU mode)
|
|
1980
|
+
|
|
1981
|
+
if effective_device in ("vulkan", "gpu") or device_mode in ("vulkan", "gpu"):
|
|
1982
|
+
|
|
1983
|
+
try:
|
|
1984
|
+
|
|
1985
|
+
from ameva_runtime import vulkan as avr
|
|
1986
|
+
|
|
1987
|
+
from ameva_runtime.vulkan.adapters import DiffusionAdapter
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
|
|
1991
|
+
avr_ctx = avr.get_or_create_context(device_mode)
|
|
1992
|
+
|
|
1993
|
+
report = getattr(avr_ctx, "doctor", avr.Doctor()).run_self_test(verbose=False)
|
|
1994
|
+
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
class EngineProxy:
|
|
1998
|
+
|
|
1999
|
+
def __init__(self, hw):
|
|
2000
|
+
|
|
2001
|
+
self.hw_profile = hw
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
from .hardware import detect_hardware_profile
|
|
2006
|
+
|
|
2007
|
+
engine_proxy = EngineProxy(detect_hardware_profile())
|
|
2008
|
+
|
|
2009
|
+
binding = DiffusionAdapter.bind(engine_proxy, report)
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
|
|
2013
|
+
cfg = getattr(binding, "config", {})
|
|
2014
|
+
|
|
2015
|
+
if cfg.get("unet_tiling") and "--vae-tiling" not in cmd:
|
|
2016
|
+
|
|
2017
|
+
cmd.append("--vae-tiling")
|
|
2018
|
+
|
|
2019
|
+
if cfg.get("is_mali") and "--diffusion-fa" not in cmd:
|
|
2020
|
+
|
|
2021
|
+
cmd.append("--diffusion-fa")
|
|
2022
|
+
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
# Auto-inject mobile Vulkan HAL shim (Samsung GOS & BDA sanitizer) into process environment
|
|
2026
|
+
|
|
2027
|
+
env = DiffusionAdapter.get_execution_env(env)
|
|
2028
|
+
|
|
2029
|
+
# Re-sanitize LD_LIBRARY_PATH to eliminate Termux /usr/lib symbol collisions on Android Bionic
|
|
2030
|
+
|
|
2031
|
+
cur_ld = env.get("LD_LIBRARY_PATH", "")
|
|
2032
|
+
|
|
2033
|
+
prefix_str = str(Path(os.environ.get("PREFIX", "/data/data/com.termux/files/usr")) / "lib")
|
|
2034
|
+
|
|
2035
|
+
parts = [p for p in cur_ld.split(":") if p and not p.startswith(prefix_str)]
|
|
2036
|
+
|
|
2037
|
+
if parts:
|
|
2038
|
+
|
|
2039
|
+
env["LD_LIBRARY_PATH"] = ":".join(parts)
|
|
2040
|
+
|
|
2041
|
+
else:
|
|
2042
|
+
|
|
2043
|
+
env.pop("LD_LIBRARY_PATH", None)
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
|
|
2047
|
+
logger.info(
|
|
2048
|
+
|
|
2049
|
+
"[termux-diffusion] DiffusionAdapter bound: backend=%s, status=%s, LD_PRELOAD=%s",
|
|
2050
|
+
|
|
2051
|
+
cfg.get("backend", "vulkan"),
|
|
2052
|
+
|
|
2053
|
+
getattr(binding.status, "name", str(binding.status)),
|
|
2054
|
+
|
|
2055
|
+
env.get("LD_PRELOAD", "None")
|
|
2056
|
+
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
except Exception as e:
|
|
2060
|
+
|
|
2061
|
+
if device_mode in ("gpu", "vulkan") or strict_vulkan:
|
|
2062
|
+
|
|
2063
|
+
from .exceptions import PlatformNotSupportedError
|
|
2064
|
+
|
|
2065
|
+
raise PlatformNotSupportedError(
|
|
2066
|
+
|
|
2067
|
+
f"[termux-diffusion] [FAIL-FAST] Vulkan execution requested (device='{device_mode}'), "
|
|
2068
|
+
|
|
2069
|
+
f"but hardware validation failed: {e}"
|
|
2070
|
+
|
|
2071
|
+
) from e
|
|
2072
|
+
|
|
2073
|
+
avr_ctx = None
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
# Inject AMEVA isolated RPC companion library paths if available
|
|
2078
|
+
|
|
2079
|
+
home_dir = Path(os.environ.get("HOME", os.path.expanduser("~")))
|
|
2080
|
+
|
|
2081
|
+
rpc_lib_dir = home_dir / ".local" / "share" / "ameva" / "rpc_llama" / "bin"
|
|
2082
|
+
|
|
2083
|
+
if rpc_lib_dir.is_dir():
|
|
2084
|
+
|
|
2085
|
+
cur_ld = env.get("LD_LIBRARY_PATH", "")
|
|
2086
|
+
|
|
2087
|
+
if str(rpc_lib_dir) not in cur_ld:
|
|
2088
|
+
|
|
2089
|
+
env["LD_LIBRARY_PATH"] = f"{rpc_lib_dir}:{cur_ld}" if cur_ld else str(rpc_lib_dir)
|
|
2090
|
+
|
|
2091
|
+
|
|
2092
|
+
|
|
2093
|
+
# Ensure Termux system lib is present for CPU baseline runs (unless in Vulkan or OpenCL route where it causes symbol shadowing)
|
|
2094
|
+
|
|
2095
|
+
if effective_device not in ("vulkan", "gpu", "opencl", "cl"):
|
|
2096
|
+
|
|
2097
|
+
termux_usr_lib = Path("/data/data/com.termux/files/usr/lib")
|
|
2098
|
+
|
|
2099
|
+
if termux_usr_lib.is_dir():
|
|
2100
|
+
|
|
2101
|
+
cur_ld = env.get("LD_LIBRARY_PATH", "")
|
|
2102
|
+
|
|
2103
|
+
if str(termux_usr_lib) not in cur_ld:
|
|
2104
|
+
|
|
2105
|
+
env["LD_LIBRARY_PATH"] = f"{cur_ld}:{termux_usr_lib}" if cur_ld else str(termux_usr_lib)
|
|
2106
|
+
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
# Inject OpenCL build companion directory if present
|
|
2110
|
+
|
|
2111
|
+
opencl_bin_dir = home_dir / "sd-opencl-build" / "build" / "bin"
|
|
2112
|
+
|
|
2113
|
+
if opencl_bin_dir.is_dir() and effective_device in ("opencl", "cl"):
|
|
2114
|
+
|
|
2115
|
+
cur_ld = env.get("LD_LIBRARY_PATH", "")
|
|
2116
|
+
|
|
2117
|
+
parts = [p for p in cur_ld.split(":") if p and not p.startswith("/data/data/com.termux/files/usr/lib")]
|
|
2118
|
+
|
|
2119
|
+
env["LD_LIBRARY_PATH"] = ":".join([str(opencl_bin_dir)] + [p for p in parts if p != str(opencl_bin_dir)])
|
|
2120
|
+
|
|
2121
|
+
|
|
2122
|
+
|
|
2123
|
+
logger.info("Executing diffusion inference: %s", " ".join(cmd[:6]) + " ...")
|
|
2124
|
+
|
|
2125
|
+
print(f"[termux-diffusion] Command: {' '.join(cmd)}")
|
|
2126
|
+
|
|
2127
|
+
print(f"[termux-diffusion] Processing inference with model='{model}' (steps={steps}, threads={threads}, device={device_mode})...")
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
start_time = time.time()
|
|
2132
|
+
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
# 6. Execute with TermuxExecutionGuard (Android WakeLock + Niceness priority protection)
|
|
2136
|
+
|
|
2137
|
+
with TermuxExecutionGuard(enable_wake_lock=wake_lock, boost_priority=not safe_mode, safe_mode=safe_mode):
|
|
2138
|
+
|
|
2139
|
+
process = None
|
|
2140
|
+
|
|
2141
|
+
try:
|
|
2142
|
+
|
|
2143
|
+
popen_kwargs = {
|
|
2144
|
+
|
|
2145
|
+
"stdout": subprocess.PIPE,
|
|
2146
|
+
|
|
2147
|
+
"stderr": subprocess.STDOUT,
|
|
2148
|
+
|
|
2149
|
+
"env": env,
|
|
2150
|
+
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
if safe_mode and hasattr(os, "nice") and sys.platform != "win32":
|
|
2154
|
+
|
|
2155
|
+
def _demote_priority():
|
|
2156
|
+
|
|
2157
|
+
try:
|
|
2158
|
+
|
|
2159
|
+
os.nice(10)
|
|
2160
|
+
|
|
2161
|
+
except Exception:
|
|
2162
|
+
|
|
2163
|
+
pass
|
|
2164
|
+
|
|
2165
|
+
popen_kwargs["preexec_fn"] = _demote_priority
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
process = subprocess.Popen(cmd, **popen_kwargs)
|
|
2170
|
+
|
|
2171
|
+
if _proc_holder is not None:
|
|
2172
|
+
|
|
2173
|
+
_proc_holder.append(process)
|
|
2174
|
+
|
|
2175
|
+
if _cancel_event and _cancel_event.is_set():
|
|
2176
|
+
|
|
2177
|
+
_safe_kill_process(process)
|
|
2178
|
+
|
|
2179
|
+
raise TermuxDiffusionError("Inference cancelled by caller.")
|
|
2180
|
+
|
|
2181
|
+
|
|
2182
|
+
|
|
2183
|
+
init_logs = []
|
|
2184
|
+
|
|
2185
|
+
recent_logs = deque(maxlen=30)
|
|
2186
|
+
|
|
2187
|
+
last_gpu_query = 0.0
|
|
2188
|
+
|
|
2189
|
+
cached_gpu_busy = None
|
|
2190
|
+
|
|
2191
|
+
pid = getattr(process, "pid", None)
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
def notify_progress(info: ProgressInfo):
|
|
2196
|
+
|
|
2197
|
+
if progress_callback:
|
|
2198
|
+
|
|
2199
|
+
try:
|
|
2200
|
+
|
|
2201
|
+
progress_callback(info)
|
|
2202
|
+
|
|
2203
|
+
except Exception as _cb_err:
|
|
2204
|
+
|
|
2205
|
+
logger.debug("progress_callback exception: %s", _cb_err)
|
|
2206
|
+
|
|
2207
|
+
if json_progress:
|
|
2208
|
+
|
|
2209
|
+
sys.stdout.write(json.dumps(info.to_dict()) + "\n")
|
|
2210
|
+
|
|
2211
|
+
sys.stdout.flush()
|
|
2212
|
+
|
|
2213
|
+
if progress:
|
|
2214
|
+
|
|
2215
|
+
if info.phase == "sampling" and info.total_steps > 0:
|
|
2216
|
+
|
|
2217
|
+
bar_len = 20
|
|
2218
|
+
|
|
2219
|
+
filled = int(round(bar_len * info.percent / 100.0))
|
|
2220
|
+
|
|
2221
|
+
bar = "=" * filled + (">" if filled < bar_len else "")
|
|
2222
|
+
|
|
2223
|
+
bar = bar.ljust(bar_len)
|
|
2224
|
+
|
|
2225
|
+
speed_str = f"{info.speed_s_per_it:.2f}s/it" if info.speed_s_per_it else ""
|
|
2226
|
+
|
|
2227
|
+
eta_str = f"ETA: {int(info.eta_seconds)}s" if info.eta_seconds is not None else ""
|
|
2228
|
+
|
|
2229
|
+
gpu_str = f"GPU: {info.gpu_busy}%" if info.gpu_busy is not None else ""
|
|
2230
|
+
|
|
2231
|
+
rss_str = f"RSS: {info.rss_mb:.0f}MB" if info.rss_mb is not None else ""
|
|
2232
|
+
|
|
2233
|
+
meta = " | ".join(p for p in [speed_str, eta_str, gpu_str, rss_str] if p)
|
|
2234
|
+
|
|
2235
|
+
meta_formatted = f" | {meta}" if meta else ""
|
|
2236
|
+
|
|
2237
|
+
sys.stderr.write(f"\r\033[36m[termux-diffusion]\033[0m [{bar}] {info.step}/{info.total_steps} ({info.percent:.1f}%){meta_formatted}\033[K")
|
|
2238
|
+
|
|
2239
|
+
sys.stderr.flush()
|
|
2240
|
+
|
|
2241
|
+
elif info.phase in ("loading_model", "encoding_prompt", "decoding_vae"):
|
|
2242
|
+
|
|
2243
|
+
sys.stderr.write(f"\r\033[36m[termux-diffusion]\033[0m [{info.phase}] Processing...\033[K")
|
|
2244
|
+
|
|
2245
|
+
sys.stderr.flush()
|
|
2246
|
+
|
|
2247
|
+
elif info.phase == "complete":
|
|
2248
|
+
|
|
2249
|
+
sys.stderr.write(f"\r\033[32m[termux-diffusion]\033[0m Complete! Image saved in {info.elapsed_seconds:.1f}s\033[K\n")
|
|
2250
|
+
|
|
2251
|
+
sys.stderr.flush()
|
|
2252
|
+
|
|
2253
|
+
if progress_file:
|
|
2254
|
+
|
|
2255
|
+
try:
|
|
2256
|
+
|
|
2257
|
+
tmp_pf = str(progress_file) + ".tmp"
|
|
2258
|
+
|
|
2259
|
+
with open(tmp_pf, "w", encoding="utf-8") as f:
|
|
2260
|
+
|
|
2261
|
+
json.dump(info.to_dict(), f)
|
|
2262
|
+
|
|
2263
|
+
os.replace(tmp_pf, str(progress_file))
|
|
2264
|
+
|
|
2265
|
+
except Exception as _pf_err:
|
|
2266
|
+
|
|
2267
|
+
logger.debug("Failed writing progress_file: %s", _pf_err)
|
|
2268
|
+
|
|
2269
|
+
|
|
2270
|
+
|
|
2271
|
+
notify_progress(ProgressInfo(phase="init", raw_message="Engine launched"))
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
|
|
2275
|
+
def _process_segment(cleaned: str):
|
|
2276
|
+
|
|
2277
|
+
nonlocal cached_gpu_busy, last_gpu_query
|
|
2278
|
+
|
|
2279
|
+
if len(init_logs) < 50 or "ggml_vulkan" in cleaned or "error" in cleaned.lower() or "failed" in cleaned.lower():
|
|
2280
|
+
|
|
2281
|
+
init_logs.append(cleaned)
|
|
2282
|
+
|
|
2283
|
+
recent_logs.append(cleaned)
|
|
2284
|
+
|
|
2285
|
+
|
|
2286
|
+
|
|
2287
|
+
lower_seg = cleaned.lower()
|
|
2288
|
+
|
|
2289
|
+
if "loading" in lower_seg or "load model" in lower_seg:
|
|
2290
|
+
|
|
2291
|
+
notify_progress(ProgressInfo(phase="loading_model", raw_message=cleaned))
|
|
2292
|
+
|
|
2293
|
+
elif "prompt" in lower_seg or "llm" in lower_seg or "clip" in lower_seg or "tokenizer" in lower_seg:
|
|
2294
|
+
|
|
2295
|
+
notify_progress(ProgressInfo(phase="encoding_prompt", raw_message=cleaned))
|
|
2296
|
+
|
|
2297
|
+
elif "decoding" in lower_seg or "vae" in lower_seg or "taesd" in lower_seg:
|
|
2298
|
+
|
|
2299
|
+
notify_progress(ProgressInfo(phase="decoding_vae", raw_message=cleaned))
|
|
2300
|
+
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
m = PROGRESS_RE.search(cleaned)
|
|
2304
|
+
|
|
2305
|
+
if m:
|
|
2306
|
+
|
|
2307
|
+
cur_step = int(m.group(1))
|
|
2308
|
+
|
|
2309
|
+
tot_steps = int(m.group(2))
|
|
2310
|
+
|
|
2311
|
+
speed_val = float(m.group(3)) if m.group(3) else None
|
|
2312
|
+
|
|
2313
|
+
unit = m.group(4) if m.group(4) else None
|
|
2314
|
+
|
|
2315
|
+
speed_s = None
|
|
2316
|
+
|
|
2317
|
+
if unit == "s/it":
|
|
2318
|
+
|
|
2319
|
+
speed_s = speed_val
|
|
2320
|
+
|
|
2321
|
+
elif unit == "it/s" and speed_val and speed_val > 0:
|
|
2322
|
+
|
|
2323
|
+
speed_s = 1.0 / speed_val
|
|
2324
|
+
|
|
2325
|
+
eta_s = round((tot_steps - cur_step) * speed_s, 1) if speed_s is not None else None
|
|
2326
|
+
|
|
2327
|
+
pct = round((cur_step / tot_steps) * 100.0, 1) if tot_steps > 0 else 0.0
|
|
2328
|
+
|
|
2329
|
+
|
|
2330
|
+
|
|
2331
|
+
now = time.time()
|
|
2332
|
+
|
|
2333
|
+
if now - last_gpu_query > 1.0:
|
|
2334
|
+
|
|
2335
|
+
cached_gpu_busy = _read_sys_gpu_busy()
|
|
2336
|
+
|
|
2337
|
+
last_gpu_query = now
|
|
2338
|
+
|
|
2339
|
+
|
|
2340
|
+
|
|
2341
|
+
rss_mb = _read_process_rss_mb(pid) if pid else None
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
info = ProgressInfo(
|
|
2346
|
+
|
|
2347
|
+
phase="sampling",
|
|
2348
|
+
|
|
2349
|
+
step=cur_step,
|
|
2350
|
+
|
|
2351
|
+
total_steps=tot_steps,
|
|
2352
|
+
|
|
2353
|
+
percent=pct,
|
|
2354
|
+
|
|
2355
|
+
eta_seconds=eta_s,
|
|
2356
|
+
|
|
2357
|
+
speed_s_per_it=speed_s,
|
|
2358
|
+
|
|
2359
|
+
gpu_busy=cached_gpu_busy,
|
|
2360
|
+
|
|
2361
|
+
rss_mb=rss_mb,
|
|
2362
|
+
|
|
2363
|
+
raw_message=cleaned,
|
|
2364
|
+
|
|
2365
|
+
elapsed_seconds=round(time.time() - start_time, 2),
|
|
2366
|
+
|
|
2367
|
+
)
|
|
2368
|
+
|
|
2369
|
+
notify_progress(info)
|
|
2370
|
+
|
|
2371
|
+
if duty_controller.enabled:
|
|
2372
|
+
|
|
2373
|
+
duty_controller.on_step(cur_step, proc_pid=pid)
|
|
2374
|
+
|
|
2375
|
+
else:
|
|
2376
|
+
|
|
2377
|
+
if not progress and not json_progress:
|
|
2378
|
+
|
|
2379
|
+
if "step" in lower_seg or "%" in lower_seg or "sampling" in lower_seg:
|
|
2380
|
+
|
|
2381
|
+
print(f" > {cleaned}")
|
|
2382
|
+
|
|
2383
|
+
else:
|
|
2384
|
+
|
|
2385
|
+
logger.debug("sd-cli: %s", cleaned)
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
|
|
2389
|
+
# Stream real-time progress using unbuffered chunk reading to support '\r' updates
|
|
2390
|
+
|
|
2391
|
+
if process.stdout:
|
|
2392
|
+
|
|
2393
|
+
remainder = ""
|
|
2394
|
+
|
|
2395
|
+
try:
|
|
2396
|
+
|
|
2397
|
+
if hasattr(process.stdout, "read1") or hasattr(process.stdout, "read"):
|
|
2398
|
+
|
|
2399
|
+
while True:
|
|
2400
|
+
|
|
2401
|
+
if _cancel_event and _cancel_event.is_set():
|
|
2402
|
+
|
|
2403
|
+
_safe_kill_process(process)
|
|
2404
|
+
|
|
2405
|
+
raise TermuxDiffusionError("Inference cancelled by caller.")
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
|
|
2409
|
+
chunk = process.stdout.read1(4096) if hasattr(process.stdout, "read1") else process.stdout.read(4096)
|
|
2410
|
+
|
|
2411
|
+
if not chunk:
|
|
2412
|
+
|
|
2413
|
+
break
|
|
2414
|
+
|
|
2415
|
+
if isinstance(chunk, bytes):
|
|
2416
|
+
|
|
2417
|
+
text = remainder + chunk.decode("utf-8", errors="replace")
|
|
2418
|
+
|
|
2419
|
+
else:
|
|
2420
|
+
|
|
2421
|
+
text = remainder + str(chunk)
|
|
2422
|
+
|
|
2423
|
+
|
|
2424
|
+
|
|
2425
|
+
lines = re.split(r'[\r\n]+', text)
|
|
2426
|
+
|
|
2427
|
+
remainder = lines.pop()
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
|
|
2431
|
+
for segment in lines:
|
|
2432
|
+
|
|
2433
|
+
cleaned = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', segment).strip()
|
|
2434
|
+
|
|
2435
|
+
if cleaned:
|
|
2436
|
+
|
|
2437
|
+
_process_segment(cleaned)
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
|
|
2441
|
+
if remainder.strip():
|
|
2442
|
+
|
|
2443
|
+
cleaned = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', remainder).strip()
|
|
2444
|
+
|
|
2445
|
+
if cleaned:
|
|
2446
|
+
|
|
2447
|
+
_process_segment(cleaned)
|
|
2448
|
+
|
|
2449
|
+
else:
|
|
2450
|
+
|
|
2451
|
+
for line in process.stdout:
|
|
2452
|
+
|
|
2453
|
+
if _cancel_event and _cancel_event.is_set():
|
|
2454
|
+
|
|
2455
|
+
_safe_kill_process(process)
|
|
2456
|
+
|
|
2457
|
+
raise TermuxDiffusionError("Inference cancelled by caller.")
|
|
2458
|
+
|
|
2459
|
+
line_str = str(line).strip()
|
|
2460
|
+
|
|
2461
|
+
if line_str:
|
|
2462
|
+
|
|
2463
|
+
cleaned = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', line_str).strip()
|
|
2464
|
+
|
|
2465
|
+
if cleaned:
|
|
2466
|
+
|
|
2467
|
+
_process_segment(cleaned)
|
|
2468
|
+
|
|
2469
|
+
finally:
|
|
2470
|
+
|
|
2471
|
+
try:
|
|
2472
|
+
|
|
2473
|
+
if hasattr(process.stdout, "close"):
|
|
2474
|
+
|
|
2475
|
+
process.stdout.close()
|
|
2476
|
+
|
|
2477
|
+
except OSError as _close_err:
|
|
2478
|
+
|
|
2479
|
+
logger.debug("[termux-diffusion] stdout.close() OSError: %s", _close_err)
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
|
|
2483
|
+
process.wait(timeout=timeout)
|
|
2484
|
+
|
|
2485
|
+
all_critical_logs = init_logs + list(recent_logs)
|
|
2486
|
+
|
|
2487
|
+
if (strict_vulkan or device_mode in ("vulkan", "gpu")) and any("ggml_vulkan: No devices found" in l for l in all_critical_logs):
|
|
2488
|
+
|
|
2489
|
+
raise TermuxDiffusionError(
|
|
2490
|
+
|
|
2491
|
+
"Strict Vulkan execution mode requested (--strict-vulkan or device='vulkan'), but Vulkan physical device discovery failed: 'ggml_vulkan: No devices found'."
|
|
2492
|
+
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
if process.returncode != 0:
|
|
2496
|
+
|
|
2497
|
+
err_detail = "\n".join(list(recent_logs)[-5:]) if recent_logs else "No engine output"
|
|
2498
|
+
|
|
2499
|
+
if device_mode == "auto" and effective_device != "cpu":
|
|
2500
|
+
|
|
2501
|
+
logger.warning("[termux-diffusion] Auto-mode Vulkan execution failed (RC=%s): %s. Falling back to CPU...", process.returncode, err_detail)
|
|
2502
|
+
|
|
2503
|
+
print(f"[termux-diffusion] [Auto Fallback] Vulkan acceleration failed (RC={process.returncode}); automatically falling back to CPU...")
|
|
2504
|
+
|
|
2505
|
+
return generate(
|
|
2506
|
+
|
|
2507
|
+
prompt=prompt,
|
|
2508
|
+
|
|
2509
|
+
model=model,
|
|
2510
|
+
|
|
2511
|
+
negative_prompt=negative_prompt,
|
|
2512
|
+
|
|
2513
|
+
device="cpu",
|
|
2514
|
+
|
|
2515
|
+
steps=steps,
|
|
2516
|
+
|
|
2517
|
+
cfg_scale=cfg_scale,
|
|
2518
|
+
|
|
2519
|
+
width=width,
|
|
2520
|
+
|
|
2521
|
+
height=height,
|
|
2522
|
+
|
|
2523
|
+
seed=seed,
|
|
2524
|
+
|
|
2525
|
+
threads=threads,
|
|
2526
|
+
|
|
2527
|
+
output=output,
|
|
2528
|
+
|
|
2529
|
+
sampling_method=sampling_method,
|
|
2530
|
+
|
|
2531
|
+
schedule=schedule,
|
|
2532
|
+
|
|
2533
|
+
vae_tiling=vae_tiling,
|
|
2534
|
+
|
|
2535
|
+
init_img=init_img,
|
|
2536
|
+
|
|
2537
|
+
strength=strength,
|
|
2538
|
+
|
|
2539
|
+
lora_dir=lora_dir,
|
|
2540
|
+
|
|
2541
|
+
clip_skip=clip_skip,
|
|
2542
|
+
|
|
2543
|
+
control_net=control_net,
|
|
2544
|
+
|
|
2545
|
+
control_image=control_image,
|
|
2546
|
+
|
|
2547
|
+
control_strength=control_strength,
|
|
2548
|
+
|
|
2549
|
+
taesd=taesd,
|
|
2550
|
+
|
|
2551
|
+
export_gallery=export_gallery,
|
|
2552
|
+
|
|
2553
|
+
wake_lock=wake_lock,
|
|
2554
|
+
|
|
2555
|
+
low_ram_guard=low_ram_guard,
|
|
2556
|
+
|
|
2557
|
+
auto_provision=auto_provision,
|
|
2558
|
+
|
|
2559
|
+
strict_vulkan=False,
|
|
2560
|
+
|
|
2561
|
+
progress=progress,
|
|
2562
|
+
|
|
2563
|
+
json_progress=json_progress,
|
|
2564
|
+
|
|
2565
|
+
progress_file=progress_file,
|
|
2566
|
+
|
|
2567
|
+
progress_callback=progress_callback,
|
|
2568
|
+
|
|
2569
|
+
timeout=timeout,
|
|
2570
|
+
|
|
2571
|
+
_cancel_event=_cancel_event,
|
|
2572
|
+
|
|
2573
|
+
_proc_holder=_proc_holder,
|
|
2574
|
+
|
|
2575
|
+
)
|
|
2576
|
+
|
|
2577
|
+
raise TermuxDiffusionError(
|
|
2578
|
+
|
|
2579
|
+
f"Engine process failed with return code {process.returncode}.\nDetails:\n{err_detail}"
|
|
2580
|
+
|
|
2581
|
+
)
|
|
2582
|
+
|
|
2583
|
+
else:
|
|
2584
|
+
|
|
2585
|
+
total_elapsed = round(time.time() - start_time, 2)
|
|
2586
|
+
|
|
2587
|
+
notify_progress(ProgressInfo(
|
|
2588
|
+
|
|
2589
|
+
phase="complete",
|
|
2590
|
+
|
|
2591
|
+
step=steps or 1,
|
|
2592
|
+
|
|
2593
|
+
total_steps=steps or 1,
|
|
2594
|
+
|
|
2595
|
+
percent=100.0,
|
|
2596
|
+
|
|
2597
|
+
output_path=str(out_path),
|
|
2598
|
+
|
|
2599
|
+
elapsed_seconds=total_elapsed,
|
|
2600
|
+
|
|
2601
|
+
raw_message=f"Generation completed in {total_elapsed}s",
|
|
2602
|
+
|
|
2603
|
+
))
|
|
2604
|
+
|
|
2605
|
+
except KeyboardInterrupt:
|
|
2606
|
+
|
|
2607
|
+
_safe_kill_process(process)
|
|
2608
|
+
|
|
2609
|
+
print("\n[termux-diffusion] Inference interrupted by user. Child processes terminated safely.")
|
|
2610
|
+
|
|
2611
|
+
raise
|
|
2612
|
+
|
|
2613
|
+
except subprocess.TimeoutExpired as exc:
|
|
2614
|
+
|
|
2615
|
+
_safe_kill_process(process)
|
|
2616
|
+
|
|
2617
|
+
raise InferenceTimeoutError(f"Diffusion generation timed out after {timeout} seconds") from exc
|
|
2618
|
+
|
|
2619
|
+
except Exception:
|
|
2620
|
+
|
|
2621
|
+
_safe_kill_process(process)
|
|
2622
|
+
|
|
2623
|
+
raise
|
|
2624
|
+
|
|
2625
|
+
|
|
2626
|
+
|
|
2627
|
+
elapsed = time.time() - start_time
|
|
2628
|
+
|
|
2629
|
+
logger.info("Generation completed in %.2fs -> %s", elapsed, out_path)
|
|
2630
|
+
|
|
2631
|
+
print(f"[termux-diffusion] Artifact generated in {elapsed:.2f}s -> {out_path}")
|
|
2632
|
+
|
|
2633
|
+
|
|
2634
|
+
|
|
2635
|
+
if not out_path.is_file():
|
|
2636
|
+
|
|
2637
|
+
raise TermuxDiffusionError(f"Engine finished but output file was not created at: {out_path}")
|
|
2638
|
+
|
|
2639
|
+
|
|
2640
|
+
|
|
2641
|
+
# 6.5 Mobile FaceDetailer: Auto-detect, Crop, Refine with Local Img2Img, and Seamless Blend
|
|
2642
|
+
|
|
2643
|
+
if detail_faces and out_path.is_file():
|
|
2644
|
+
|
|
2645
|
+
try:
|
|
2646
|
+
|
|
2647
|
+
from .detailer import refine_face_regions
|
|
2648
|
+
|
|
2649
|
+
logger.info("[termux-diffusion] Running Mobile FaceDetailer stage...")
|
|
2650
|
+
|
|
2651
|
+
print("[termux-diffusion] Running Mobile FaceDetailer stage...")
|
|
2652
|
+
|
|
2653
|
+
refine_face_regions(
|
|
2654
|
+
|
|
2655
|
+
image_path=out_path,
|
|
2656
|
+
|
|
2657
|
+
generator_func=generate,
|
|
2658
|
+
|
|
2659
|
+
prompt=prompt,
|
|
2660
|
+
|
|
2661
|
+
output_path=out_path,
|
|
2662
|
+
|
|
2663
|
+
strength=detailer_strength,
|
|
2664
|
+
|
|
2665
|
+
steps=detailer_steps,
|
|
2666
|
+
|
|
2667
|
+
padding_factor=detailer_padding,
|
|
2668
|
+
|
|
2669
|
+
device=device,
|
|
2670
|
+
|
|
2671
|
+
model=model,
|
|
2672
|
+
|
|
2673
|
+
generator_kwargs={
|
|
2674
|
+
|
|
2675
|
+
"vae_tiling": vae_tiling,
|
|
2676
|
+
|
|
2677
|
+
"schedule": schedule,
|
|
2678
|
+
|
|
2679
|
+
"sampling_method": sampling_method,
|
|
2680
|
+
|
|
2681
|
+
"taesd": taesd,
|
|
2682
|
+
|
|
2683
|
+
"mmap": mmap,
|
|
2684
|
+
|
|
2685
|
+
"max_vram": max_vram,
|
|
2686
|
+
|
|
2687
|
+
"stream_layers": stream_layers,
|
|
2688
|
+
|
|
2689
|
+
"params_backend": params_backend,
|
|
2690
|
+
|
|
2691
|
+
"low_ram_guard": False,
|
|
2692
|
+
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
)
|
|
2696
|
+
|
|
2697
|
+
except Exception as fd_err:
|
|
2698
|
+
|
|
2699
|
+
logger.warning("[termux-diffusion] FaceDetailer pass encountered an issue: %s. Keeping base image.", fd_err)
|
|
2700
|
+
|
|
2701
|
+
|
|
2702
|
+
|
|
2703
|
+
# 7. Android Gallery MediaStore Sync (Galaxy Gallery visibility)
|
|
2704
|
+
|
|
2705
|
+
gallery_path = None
|
|
2706
|
+
|
|
2707
|
+
if export_gallery:
|
|
2708
|
+
|
|
2709
|
+
try:
|
|
2710
|
+
|
|
2711
|
+
gallery_path = export_to_android_gallery(out_path)
|
|
2712
|
+
|
|
2713
|
+
print(f"[termux-diffusion] Synchronized to Android MediaStore: {gallery_path}")
|
|
2714
|
+
|
|
2715
|
+
except Exception as e:
|
|
2716
|
+
|
|
2717
|
+
logger.warning("Could not export to Android gallery: %s", e)
|
|
2718
|
+
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
# 7.1 Assetize prompt cache metadata for future zero-latency reuse
|
|
2722
|
+
|
|
2723
|
+
if not no_cache and not prompt_cache_meta.is_file():
|
|
2724
|
+
|
|
2725
|
+
try:
|
|
2726
|
+
|
|
2727
|
+
with open(prompt_cache_meta, "w", encoding="utf-8") as f:
|
|
2728
|
+
|
|
2729
|
+
json.dump({
|
|
2730
|
+
|
|
2731
|
+
"prompt": prompt_clean,
|
|
2732
|
+
|
|
2733
|
+
"hash": prompt_hash,
|
|
2734
|
+
|
|
2735
|
+
"model": str(model),
|
|
2736
|
+
|
|
2737
|
+
"created_at": time.time(),
|
|
2738
|
+
|
|
2739
|
+
"verified": True
|
|
2740
|
+
|
|
2741
|
+
}, f, indent=2)
|
|
2742
|
+
|
|
2743
|
+
logger.info("[termux-diffusion] Assetized prompt cache: %s", prompt_cache_meta)
|
|
2744
|
+
|
|
2745
|
+
except OSError as _pe:
|
|
2746
|
+
|
|
2747
|
+
logger.warning("[termux-diffusion] Prompt cache metadata assetization write failed: %s", _pe)
|
|
2748
|
+
|
|
2749
|
+
|
|
2750
|
+
|
|
2751
|
+
return GenerationResult(
|
|
2752
|
+
|
|
2753
|
+
path=out_path,
|
|
2754
|
+
|
|
2755
|
+
gallery_path=gallery_path,
|
|
2756
|
+
|
|
2757
|
+
prompt=prompt,
|
|
2758
|
+
|
|
2759
|
+
negative_prompt=negative_prompt,
|
|
2760
|
+
|
|
2761
|
+
model=model,
|
|
2762
|
+
|
|
2763
|
+
device=effective_device,
|
|
2764
|
+
|
|
2765
|
+
steps=steps,
|
|
2766
|
+
|
|
2767
|
+
cfg_scale=cfg_scale,
|
|
2768
|
+
|
|
2769
|
+
width=width,
|
|
2770
|
+
|
|
2771
|
+
height=height,
|
|
2772
|
+
|
|
2773
|
+
seed=seed,
|
|
2774
|
+
|
|
2775
|
+
elapsed_sec=elapsed,
|
|
2776
|
+
|
|
2777
|
+
sampling_method=effective_sampler,
|
|
2778
|
+
|
|
2779
|
+
schedule=effective_schedule,
|
|
2780
|
+
|
|
2781
|
+
vae_tiling=vae_tiling,
|
|
2782
|
+
|
|
2783
|
+
init_img=effective_init_path,
|
|
2784
|
+
|
|
2785
|
+
strength=effective_strength,
|
|
2786
|
+
|
|
2787
|
+
lora_dir=effective_lora_path,
|
|
2788
|
+
|
|
2789
|
+
clip_skip=effective_clip_skip,
|
|
2790
|
+
|
|
2791
|
+
control_net=effective_cnet_path,
|
|
2792
|
+
|
|
2793
|
+
control_image=effective_cimg_path,
|
|
2794
|
+
|
|
2795
|
+
control_strength=effective_cstrength,
|
|
2796
|
+
|
|
2797
|
+
taesd=effective_taesd_path,
|
|
2798
|
+
|
|
2799
|
+
rpc=effective_rpc if isinstance(effective_rpc, str) else (",".join(cluster_nodes) if cluster_nodes else None),
|
|
2800
|
+
|
|
2801
|
+
tensor_split=tensor_split,
|
|
2802
|
+
|
|
2803
|
+
cluster_rpc_servers=cluster_nodes if cluster_nodes else (parse_cluster_rpc_spec(effective_rpc) if effective_rpc else None),
|
|
2804
|
+
|
|
2805
|
+
cluster_split_mode=effective_split_mode,
|
|
2806
|
+
|
|
2807
|
+
cluster_tensor_split=cluster_tensor_split or tensor_split,
|
|
2808
|
+
|
|
2809
|
+
cluster_vram_budget=effective_max_vram,
|
|
2810
|
+
|
|
2811
|
+
extra_args=all_forwarded if all_forwarded else None,
|
|
2812
|
+
|
|
2813
|
+
isolated_stages=isolate_text_encoder or sequential_stages,
|
|
2814
|
+
|
|
2815
|
+
)
|
|
2816
|
+
|
|
2817
|
+
|
|
2818
|
+
|
|
2819
|
+
|
|
2820
|
+
|
|
2821
|
+
async def async_generate(*args, **kwargs) -> GenerationResult:
|
|
2822
|
+
|
|
2823
|
+
"""Asynchronous wrapper for generate() with real cancellation propagation to child process."""
|
|
2824
|
+
|
|
2825
|
+
cancel_event = threading.Event()
|
|
2826
|
+
|
|
2827
|
+
proc_holder = []
|
|
2828
|
+
|
|
2829
|
+
kwargs["_cancel_event"] = cancel_event
|
|
2830
|
+
|
|
2831
|
+
kwargs["_proc_holder"] = proc_holder
|
|
2832
|
+
|
|
2833
|
+
loop = asyncio.get_running_loop()
|
|
2834
|
+
|
|
2835
|
+
task = loop.run_in_executor(None, lambda: generate(*args, **kwargs))
|
|
2836
|
+
|
|
2837
|
+
try:
|
|
2838
|
+
|
|
2839
|
+
return await task
|
|
2840
|
+
|
|
2841
|
+
except asyncio.CancelledError:
|
|
2842
|
+
|
|
2843
|
+
cancel_event.set()
|
|
2844
|
+
|
|
2845
|
+
if proc_holder:
|
|
2846
|
+
|
|
2847
|
+
_safe_kill_process(proc_holder[0])
|
|
2848
|
+
|
|
2849
|
+
raise
|
|
2850
|
+
|
|
2851
|
+
|
|
2852
|
+
|
|
2853
|
+
|
|
2854
|
+
|
|
2855
|
+
def generate_stream(
|
|
2856
|
+
|
|
2857
|
+
prompt: str,
|
|
2858
|
+
|
|
2859
|
+
**kwargs
|
|
2860
|
+
|
|
2861
|
+
) -> Generator[ProgressInfo, None, GenerationResult]:
|
|
2862
|
+
|
|
2863
|
+
"""Generator streaming interface for diffusion image generation.
|
|
2864
|
+
|
|
2865
|
+
Yields ProgressInfo objects in real time, and returns GenerationResult upon completion.
|
|
2866
|
+
|
|
2867
|
+
"""
|
|
2868
|
+
|
|
2869
|
+
progress_queue: queue.Queue = queue.Queue()
|
|
2870
|
+
|
|
2871
|
+
done_event = threading.Event()
|
|
2872
|
+
|
|
2873
|
+
result_container = []
|
|
2874
|
+
|
|
2875
|
+
exc_container = []
|
|
2876
|
+
|
|
2877
|
+
|
|
2878
|
+
|
|
2879
|
+
def _cb(info: ProgressInfo):
|
|
2880
|
+
|
|
2881
|
+
progress_queue.put(info)
|
|
2882
|
+
|
|
2883
|
+
|
|
2884
|
+
|
|
2885
|
+
kwargs["progress_callback"] = _cb
|
|
2886
|
+
|
|
2887
|
+
|
|
2888
|
+
|
|
2889
|
+
def _worker():
|
|
2890
|
+
|
|
2891
|
+
try:
|
|
2892
|
+
|
|
2893
|
+
res = generate(prompt, **kwargs)
|
|
2894
|
+
|
|
2895
|
+
result_container.append(res)
|
|
2896
|
+
|
|
2897
|
+
except Exception as e:
|
|
2898
|
+
|
|
2899
|
+
exc_container.append(e)
|
|
2900
|
+
|
|
2901
|
+
finally:
|
|
2902
|
+
|
|
2903
|
+
done_event.set()
|
|
2904
|
+
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
t = threading.Thread(target=_worker, daemon=True)
|
|
2908
|
+
|
|
2909
|
+
t.start()
|
|
2910
|
+
|
|
2911
|
+
|
|
2912
|
+
|
|
2913
|
+
while not done_event.is_set() or not progress_queue.empty():
|
|
2914
|
+
|
|
2915
|
+
try:
|
|
2916
|
+
|
|
2917
|
+
info = progress_queue.get(timeout=0.05)
|
|
2918
|
+
|
|
2919
|
+
yield info
|
|
2920
|
+
|
|
2921
|
+
except queue.Empty:
|
|
2922
|
+
|
|
2923
|
+
continue
|
|
2924
|
+
|
|
2925
|
+
|
|
2926
|
+
|
|
2927
|
+
if exc_container:
|
|
2928
|
+
|
|
2929
|
+
raise exc_container[0]
|
|
2930
|
+
|
|
2931
|
+
|
|
2932
|
+
|
|
2933
|
+
return result_container[0] if result_container else None
|
|
2934
|
+
|
|
2935
|
+
|
|
2936
|
+
|