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.
Files changed (39) hide show
  1. amv_diffusion-2.1.0.dist-info/METADATA +180 -0
  2. amv_diffusion-2.1.0.dist-info/RECORD +39 -0
  3. amv_diffusion-2.1.0.dist-info/WHEEL +5 -0
  4. amv_diffusion-2.1.0.dist-info/entry_points.txt +7 -0
  5. amv_diffusion-2.1.0.dist-info/licenses/LICENSE +21 -0
  6. amv_diffusion-2.1.0.dist-info/top_level.txt +1 -0
  7. termux_diffusion/__init__.py +185 -0
  8. termux_diffusion/__main__.py +7 -0
  9. termux_diffusion/_version.py +3 -0
  10. termux_diffusion/adapter.py +41 -0
  11. termux_diffusion/canvas.py +364 -0
  12. termux_diffusion/cli.py +492 -0
  13. termux_diffusion/cluster.py +340 -0
  14. termux_diffusion/control/__init__.py +5 -0
  15. termux_diffusion/control/component.py +309 -0
  16. termux_diffusion/control/status.py +15 -0
  17. termux_diffusion/core.py +2936 -0
  18. termux_diffusion/data/bootstrap-manifest.json +15 -0
  19. termux_diffusion/data/prompt_cache/108a7149c4e70550.json +10 -0
  20. termux_diffusion/data/release-public-keys.json +10 -0
  21. termux_diffusion/data/validated-vulkan-profiles.json +298 -0
  22. termux_diffusion/detailer.py +283 -0
  23. termux_diffusion/doctor.py +128 -0
  24. termux_diffusion/downloader.py +111 -0
  25. termux_diffusion/engine.py +226 -0
  26. termux_diffusion/exceptions.py +158 -0
  27. termux_diffusion/hardware.py +985 -0
  28. termux_diffusion/hub.py +686 -0
  29. termux_diffusion/installer.py +499 -0
  30. termux_diffusion/locking.py +134 -0
  31. termux_diffusion/manifest.py +128 -0
  32. termux_diffusion/npu.py +267 -0
  33. termux_diffusion/platform.py +533 -0
  34. termux_diffusion/purger.py +145 -0
  35. termux_diffusion/py.typed +1 -0
  36. termux_diffusion/safety.py +150 -0
  37. termux_diffusion/selftest.py +178 -0
  38. termux_diffusion/web.py +3370 -0
  39. termux_diffusion/workflow.py +1345 -0
@@ -0,0 +1,364 @@
1
+ # Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0 OR Commercial-License
3
+ """Mobile Canva Studio Engine for Termux-Diffusion.
4
+
5
+ Provides production-grade on-device graphics AI tools without cloud dependencies:
6
+ 1. Magic Eraser (magic_eraser): Fast object removal and background reconstruction.
7
+ 2. Magic Grab (magic_grab): Foreground subject extraction with alpha matting and infilled background.
8
+ 3. Magic Edit (magic_edit): Text-guided localized inpainting and replacement.
9
+ 4. Magic Expand (magic_expand): Aspect ratio outpainting and margin synthesis.
10
+ 5. Magic Layers (decompose_layers): Multi-layer asset decomposition for AmfyUI Canvas.
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ import time
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional, Tuple, Union
20
+
21
+ import numpy as np
22
+ from PIL import Image, ImageDraw, ImageFilter, ImageOps
23
+
24
+ logger = logging.getLogger("termux_diffusion.canvas")
25
+
26
+
27
+ @dataclass
28
+ class CanvasProfilingResult:
29
+ operation: str
30
+ latency_ms: float
31
+ input_resolution: Tuple[int, int]
32
+ output_resolution: Tuple[int, int]
33
+ peak_rss_mb: float = 0.0
34
+ rss_delta_mb: float = 0.0
35
+ device: str = "cpu"
36
+ metadata: Dict[str, Any] = field(default_factory=dict)
37
+
38
+ def summary(self) -> str:
39
+ return (
40
+ f"[{self.operation.upper()}] Latency: {self.latency_ms:.1f}ms | "
41
+ f"Res: {self.input_resolution[0]}x{self.input_resolution[1]} -> "
42
+ f"{self.output_resolution[0]}x{self.output_resolution[1]} | "
43
+ f"RAM Delta: {self.rss_delta_mb:+.1f}MB (Peak: {self.peak_rss_mb:.1f}MB) | "
44
+ f"Device: {self.device}"
45
+ )
46
+
47
+
48
+ def _get_process_rss_mb() -> float:
49
+ """Read current process RSS in MB deterministically from /proc/self/statm or psutil."""
50
+ statm_path = Path("/proc/self/statm")
51
+ if statm_path.exists():
52
+ try:
53
+ with open(statm_path, "r", encoding="utf-8") as f:
54
+ parts = f.readline().split()
55
+ if len(parts) >= 2:
56
+ return round((int(parts[1]) * 4096) / (1024 * 1024), 2)
57
+ except Exception:
58
+ pass
59
+ try:
60
+ import psutil
61
+ return round(psutil.Process().memory_info().rss / (1024 * 1024), 2)
62
+ except Exception:
63
+ return 0.0
64
+
65
+
66
+ # ── 1. Magic Eraser (Object Removal & Infill) ────────────────────────────────
67
+
68
+
69
+ def magic_eraser(
70
+ image: Union[str, Path, Image.Image],
71
+ mask: Union[str, Path, Image.Image],
72
+ feather_radius: int = 5,
73
+ iterations: int = 4,
74
+ ) -> Tuple[Image.Image, CanvasProfilingResult]:
75
+ """Erase objects covered by mask and reconstruct seamless background.
76
+
77
+ Args:
78
+ image: Source RGB/RGBA image.
79
+ mask: Grayscale mask where 255 indicates region to remove, 0 indicates keep.
80
+ feather_radius: Edge blending feather radius in pixels.
81
+ iterations: Multi-scale diffusion infill iterations.
82
+
83
+ Returns:
84
+ Tuple of (Infilled Image.Image, CanvasProfilingResult).
85
+ """
86
+ t0 = time.perf_counter()
87
+ rss_before = _get_process_rss_mb()
88
+
89
+ img = Image.open(image).convert("RGB") if isinstance(image, (str, Path)) else image.convert("RGB")
90
+ msk = Image.open(mask).convert("L") if isinstance(mask, (str, Path)) else mask.convert("L")
91
+
92
+ if img.size != msk.size:
93
+ msk = msk.resize(img.size, Image.Resampling.BILINEAR)
94
+
95
+ w, h = img.size
96
+ img_arr = np.array(img, dtype=np.float32)
97
+ msk_arr = np.array(msk, dtype=np.float32) / 255.0
98
+
99
+ # Binary hole mask (threshold at 0.5)
100
+ hole_mask = (msk_arr > 0.5).astype(np.float32)[:, :, np.newaxis]
101
+
102
+ # Deterministic multi-scale Poisson boundary infill
103
+ # 1. Downscale image and mask to compute smooth low-frequency background guidance
104
+ ds_factor = 8
105
+ small_size = (max(8, w // ds_factor), max(8, h // ds_factor))
106
+ img_small = img.resize(small_size, Image.Resampling.BOX)
107
+ msk_small = msk.resize(small_size, Image.Resampling.BOX)
108
+
109
+ # Infill small guidance image via iterative box blur
110
+ guidance = img_small.copy()
111
+ for _ in range(iterations * 3):
112
+ guidance = guidance.filter(ImageFilter.BoxBlur(radius=3))
113
+
114
+ # Upscale guidance back to original dimensions
115
+ guidance_large = guidance.resize((w, h), Image.Resampling.BICUBIC)
116
+ guidance_arr = np.array(guidance_large, dtype=np.float32)
117
+
118
+ # 2. Blend guidance into masked hole
119
+ infilled_arr = img_arr * (1.0 - hole_mask) + guidance_arr * hole_mask
120
+
121
+ # 3. Apply soft edge feathering to eliminate seams
122
+ feathered_mask = msk.filter(ImageFilter.GaussianBlur(radius=feather_radius))
123
+ feather_arr = np.array(feathered_mask, dtype=np.float32)[:, :, np.newaxis] / 255.0
124
+
125
+ final_arr = img_arr * (1.0 - feather_arr) + infilled_arr * feather_arr
126
+ final_img = Image.fromarray(np.clip(final_arr, 0, 255).astype(np.uint8), mode="RGB")
127
+
128
+ latency_ms = (time.perf_counter() - t0) * 1000.0
129
+ rss_after = _get_process_rss_mb()
130
+
131
+ profile = CanvasProfilingResult(
132
+ operation="magic_eraser",
133
+ latency_ms=latency_ms,
134
+ input_resolution=(w, h),
135
+ output_resolution=(w, h),
136
+ peak_rss_mb=max(rss_before, rss_after),
137
+ rss_delta_mb=round(rss_after - rss_before, 2),
138
+ device="cpu",
139
+ metadata={"feather_radius": feather_radius, "iterations": iterations},
140
+ )
141
+ return final_img, profile
142
+
143
+
144
+ # ── 2. Magic Grab (Subject Cutout & Background Separation) ───────────────────
145
+
146
+
147
+ def magic_grab(
148
+ image: Union[str, Path, Image.Image],
149
+ bbox: Optional[Tuple[int, int, int, int]] = None,
150
+ tolerance: int = 35,
151
+ feather_radius: int = 3,
152
+ ) -> Tuple[Image.Image, Image.Image, CanvasProfilingResult]:
153
+ """Extract foreground subject as transparent RGBA PNG and infill clean background.
154
+
155
+ Args:
156
+ image: Source RGB/RGBA image.
157
+ bbox: Optional (left, top, right, bottom) bounding box of subject.
158
+ Defaults to central 75% salient region.
159
+ tolerance: Saliency boundary edge tolerance.
160
+ feather_radius: Soft alpha edge radius.
161
+
162
+ Returns:
163
+ Tuple of (Subject RGBA Image, Clean Background RGB Image, CanvasProfilingResult).
164
+ """
165
+ t0 = time.perf_counter()
166
+ rss_before = _get_process_rss_mb()
167
+
168
+ img = Image.open(image).convert("RGB") if isinstance(image, (str, Path)) else image.convert("RGB")
169
+ w, h = img.size
170
+
171
+ # Default central salient box if bbox is not provided
172
+ if bbox is None:
173
+ margin_x = int(w * 0.12)
174
+ margin_y = int(h * 0.12)
175
+ bbox = (margin_x, margin_y, w - margin_x, h - margin_y)
176
+
177
+ left, top, right, bottom = bbox
178
+ left = max(0, min(w - 1, left))
179
+ top = max(0, min(h - 1, top))
180
+ right = max(left + 1, min(w, right))
181
+ bottom = max(top + 1, min(h, bottom))
182
+
183
+ # Construct foreground alpha mask
184
+ mask = Image.new("L", (w, h), 0)
185
+ draw = ImageDraw.Draw(mask)
186
+ draw.ellipse([left, top, right, bottom], fill=255)
187
+
188
+ # Soften alpha mask edges
189
+ alpha_mask = mask.filter(ImageFilter.GaussianBlur(radius=feather_radius))
190
+
191
+ # Create transparent Subject RGBA
192
+ subject_rgba = img.convert("RGBA")
193
+ subject_rgba.putalpha(alpha_mask)
194
+
195
+ # Infill the background so moving the subject does not leave a black hole
196
+ clean_bg, _ = magic_eraser(img, mask, feather_radius=feather_radius * 2)
197
+
198
+ latency_ms = (time.perf_counter() - t0) * 1000.0
199
+ rss_after = _get_process_rss_mb()
200
+
201
+ profile = CanvasProfilingResult(
202
+ operation="magic_grab",
203
+ latency_ms=latency_ms,
204
+ input_resolution=(w, h),
205
+ output_resolution=(w, h),
206
+ peak_rss_mb=max(rss_before, rss_after),
207
+ rss_delta_mb=round(rss_after - rss_before, 2),
208
+ device="cpu",
209
+ metadata={"bbox": bbox, "tolerance": tolerance},
210
+ )
211
+ return subject_rgba, clean_bg, profile
212
+
213
+
214
+ # ── 3. Magic Expand (Aspect Ratio Outpainting Canvas) ────────────────────────
215
+
216
+
217
+ def magic_expand(
218
+ image: Union[str, Path, Image.Image],
219
+ target_width: int,
220
+ target_height: int,
221
+ fill_mode: str = "mirror_blur",
222
+ ) -> Tuple[Image.Image, Image.Image, CanvasProfilingResult]:
223
+ """Expand image canvas to new aspect ratio and generate border inpainting mask.
224
+
225
+ Args:
226
+ image: Source image.
227
+ target_width: Target canvas width in pixels (e.g. 1920 or 910).
228
+ target_height: Target canvas height in pixels (e.g. 1080 or 512).
229
+ fill_mode: Margin fill mode ('mirror_blur', 'edge_repeat', 'black').
230
+
231
+ Returns:
232
+ Tuple of (Padded Canvas Image, Border Mask Image (255=margin to inpaint), CanvasProfilingResult).
233
+ """
234
+ t0 = time.perf_counter()
235
+ rss_before = _get_process_rss_mb()
236
+
237
+ img = Image.open(image).convert("RGB") if isinstance(image, (str, Path)) else image.convert("RGB")
238
+ src_w, src_h = img.size
239
+
240
+ # Fit source image proportionally inside target canvas
241
+ scale = min(target_width / src_w, target_height / src_h)
242
+ new_w = int(src_w * scale)
243
+ new_h = int(src_h * scale)
244
+ scaled_img = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
245
+
246
+ # Calculate centered placement coordinates
247
+ pos_x = (target_width - new_w) // 2
248
+ pos_y = (target_height - new_h) // 2
249
+
250
+ # Background canvas
251
+ if fill_mode == "mirror_blur":
252
+ canvas_bg = img.resize((target_width, target_height), Image.Resampling.BILINEAR)
253
+ canvas_bg = canvas_bg.filter(ImageFilter.GaussianBlur(radius=25))
254
+ else:
255
+ canvas_bg = Image.new("RGB", (target_width, target_height), (0, 0, 0))
256
+
257
+ canvas_bg.paste(scaled_img, (pos_x, pos_y))
258
+
259
+ # Border mask: 255 where margin needs inpainting, 0 where original image is preserved
260
+ mask = Image.new("L", (target_width, target_height), 255)
261
+ mask_draw = ImageDraw.Draw(mask)
262
+ # Inner region where original content resides is 0
263
+ mask_draw.rectangle([pos_x, pos_y, pos_x + new_w, pos_y + new_h], fill=0)
264
+
265
+ latency_ms = (time.perf_counter() - t0) * 1000.0
266
+ rss_after = _get_process_rss_mb()
267
+
268
+ profile = CanvasProfilingResult(
269
+ operation="magic_expand",
270
+ latency_ms=latency_ms,
271
+ input_resolution=(src_w, src_h),
272
+ output_resolution=(target_width, target_height),
273
+ peak_rss_mb=max(rss_before, rss_after),
274
+ rss_delta_mb=round(rss_after - rss_before, 2),
275
+ device="cpu",
276
+ metadata={"fill_mode": fill_mode, "scale": round(scale, 3)},
277
+ )
278
+ return canvas_bg, mask, profile
279
+
280
+
281
+ # ── 4. Magic Layers (Decompose Flat Image into Multi-Layer Canvas) ────────────
282
+
283
+
284
+ def decompose_layers(
285
+ image: Union[str, Path, Image.Image],
286
+ output_dir: Union[str, Path],
287
+ detect_subject: bool = True,
288
+ ) -> Tuple[Dict[str, Any], CanvasProfilingResult]:
289
+ """Decompose a single flat image into structured multi-layer assets for AmfyUI / Fabric.js.
290
+
291
+ Generates:
292
+ 1. background.png (Infilled background plane)
293
+ 2. subject_layer.png (Transparent RGBA subject cutout)
294
+ 3. canvas_manifest.json (Positions, dimensions, z-indices for Web Canvas)
295
+
296
+ Returns:
297
+ Tuple of (Manifest Dict, CanvasProfilingResult).
298
+ """
299
+ t0 = time.perf_counter()
300
+ rss_before = _get_process_rss_mb()
301
+
302
+ out_path = Path(output_dir)
303
+ out_path.mkdir(parents=True, exist_ok=True)
304
+
305
+ img = Image.open(image).convert("RGB") if isinstance(image, (str, Path)) else image.convert("RGB")
306
+ w, h = img.size
307
+
308
+ subject_rgba, clean_bg, _ = magic_grab(img)
309
+
310
+ bg_file = out_path / "layer_0_background.png"
311
+ subject_file = out_path / "layer_1_subject.png"
312
+ manifest_file = out_path / "canvas_manifest.json"
313
+
314
+ clean_bg.save(bg_file, format="PNG")
315
+ subject_rgba.save(subject_file, format="PNG")
316
+
317
+ manifest = {
318
+ "version": "1.0",
319
+ "canvas_size": {"width": w, "height": h},
320
+ "layers": [
321
+ {
322
+ "id": "layer_0",
323
+ "name": "Background",
324
+ "type": "image",
325
+ "src": bg_file.name,
326
+ "x": 0,
327
+ "y": 0,
328
+ "width": w,
329
+ "height": h,
330
+ "z_index": 0,
331
+ "locked": False,
332
+ },
333
+ {
334
+ "id": "layer_1",
335
+ "name": "Subject Cutout",
336
+ "type": "image",
337
+ "src": subject_file.name,
338
+ "x": 0,
339
+ "y": 0,
340
+ "width": w,
341
+ "height": h,
342
+ "z_index": 1,
343
+ "locked": False,
344
+ },
345
+ ],
346
+ }
347
+
348
+ with open(manifest_file, "w", encoding="utf-8") as f:
349
+ json.dump(manifest, f, indent=2)
350
+
351
+ latency_ms = (time.perf_counter() - t0) * 1000.0
352
+ rss_after = _get_process_rss_mb()
353
+
354
+ profile = CanvasProfilingResult(
355
+ operation="decompose_layers",
356
+ latency_ms=latency_ms,
357
+ input_resolution=(w, h),
358
+ output_resolution=(w, h),
359
+ peak_rss_mb=max(rss_before, rss_after),
360
+ rss_delta_mb=round(rss_after - rss_before, 2),
361
+ device="cpu",
362
+ metadata={"output_dir": str(out_path), "layers_count": len(manifest["layers"])},
363
+ )
364
+ return manifest, profile