alibaybay commited on
Commit
01d9f93
·
verified ·
1 Parent(s): 5790cb2

Deploy DualSpace H3 Clip Conditioner (Comfy2API)

Browse files
README.md CHANGED
@@ -1,13 +1,27 @@
1
  ---
2
- title: Dualspace H3 Clip
3
- emoji: 👁
4
- colorFrom: purple
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.29.1
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: DualSpace MiniMax-H3 Clip
3
+ emoji: 🧠
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.44.1
 
8
  app_file: app.py
9
  pinned: false
10
+ license: apache-2.0
11
+ tags:
12
+ - comfyui
13
+ - zerogpu
14
+ - minimax-h3
15
+ - clip
16
+ - text-encoder
17
  ---
18
 
19
+ # 🧠 DualSpace MiniMax-H3 — Text/Vision Conditioner Service (I2V)
20
+
21
+ Layanan backend berarsitektur **Hot-Standby Thin Wire** untuk memproses visual keyframe dan prompt menggunakan **Qwen3-VL 32B NVFP4/AWQ** dan **Video VAE INT8 ConvRot** via ComfyUI Core Native.
22
+
23
+ ## 🚀 Fitur:
24
+ - **Thin Wire Protocol**: Mengemas tensor embedding Qwen3-VL, token tags, dan keyframe latent ke dalam file `.safetensors` ramping tanpa overhead memindahkan canvas kosong.
25
+ - **Pure ComfyUI Core Native**: Bebas custom nodes eksternal, memaksimalkan stabilitas dan kompatibilitas ComfyUI.
26
+ - **Zero-Overhead GPU Forward**: Waktu GPU 100% dialokasikan murni untuk inferensi forward pass.
27
+ - **Root Startup Pre-Warming**: Model dan engine ComfyUI sudah diinisialisasi ke RAM sebelum request pertama masuk.
__pycache__/app.cpython-311.pyc ADDED
Binary file (40.9 kB). View file
 
app.py ADDED
@@ -0,0 +1,642 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import subprocess
6
+ import pathlib
7
+ import shutil
8
+ import re
9
+ import uuid
10
+ import json
11
+ import glob
12
+ import time
13
+ import asyncio
14
+ from typing import Any
15
+
16
+ # ============================================================
17
+ # 1. HUGGINGFACE_HUB SELF-HEALING REPAIR
18
+ # ============================================================
19
+ def _hf_hub_version() -> str:
20
+ try:
21
+ from importlib.metadata import version as _pkg_version
22
+ return _pkg_version("huggingface_hub")
23
+ except Exception:
24
+ return ""
25
+
26
+ def _hf_hub_is_broken() -> bool:
27
+ import importlib
28
+ import importlib.util
29
+ for module_name in ("huggingface_hub._snapshot_download", "huggingface_hub._tree_cache"):
30
+ try:
31
+ if importlib.util.find_spec(module_name) is None:
32
+ continue
33
+ except Exception:
34
+ return True
35
+ try:
36
+ importlib.import_module(module_name)
37
+ except ImportError:
38
+ return True
39
+ except Exception:
40
+ continue
41
+ return False
42
+
43
+ def _hf_hub_reinstall(upgrade: bool) -> None:
44
+ cmd = [
45
+ sys.executable,
46
+ "-m",
47
+ "pip",
48
+ "install",
49
+ "--no-cache-dir",
50
+ "--force-reinstall",
51
+ "--no-deps",
52
+ ]
53
+ if upgrade:
54
+ cmd += ["--upgrade", "huggingface_hub"]
55
+ else:
56
+ pinned = _hf_hub_version()
57
+ cmd.append(f"huggingface_hub=={pinned}" if pinned else "huggingface_hub")
58
+ print(f"[hf-repair] {' '.join(cmd)}", flush=True)
59
+ subprocess.run(cmd, check=False)
60
+
61
+ def _repair_huggingface_hub_and_restart() -> None:
62
+ stage = int(os.environ.get("_HF_HUB_REPAIR_STAGE", "0") or "0")
63
+ if stage >= 2 or not _hf_hub_is_broken():
64
+ return
65
+ _hf_hub_reinstall(upgrade=stage == 1)
66
+ os.environ["_HF_HUB_REPAIR_STAGE"] = str(stage + 1)
67
+ os.execv(sys.executable, [sys.executable, *sys.argv])
68
+
69
+ _repair_huggingface_hub_and_restart()
70
+
71
+ # ============================================================
72
+ # 2. IMPORTS UTAMA (SPACES WAJIB PERTAMA SEBELUM TORCH)
73
+ # ============================================================
74
+ import spaces # WAJIB PERTAMA sebelum torch!
75
+ import torch
76
+
77
+ # ============================================================
78
+ # ZEROGPU COMPATIBILITY PATCH FOR PYTORCH CUDA MOCK PROPERTIES
79
+ # ============================================================
80
+ if hasattr(torch, "cuda") and hasattr(torch.cuda, "get_device_properties"):
81
+ _orig_cuda_get_device_properties = torch.cuda.get_device_properties
82
+ def _safe_cuda_get_device_properties(device=None):
83
+ props = _orig_cuda_get_device_properties(device)
84
+ if not hasattr(props, "is_integrated"):
85
+ try:
86
+ setattr(props, "is_integrated", False)
87
+ except Exception:
88
+ class _PropsProxy:
89
+ def __init__(self, p):
90
+ self._p = p
91
+ self.is_integrated = False
92
+ def __getattr__(self, name):
93
+ return getattr(self._p, name)
94
+ return _PropsProxy(props)
95
+ return props
96
+ torch.cuda.get_device_properties = _safe_cuda_get_device_properties
97
+
98
+ import gradio as gr
99
+ import gradio_client.utils
100
+ from huggingface_hub import hf_hub_download
101
+
102
+ # ============================================================
103
+ # 2.1 MONKEY-PATCH GRADIO_CLIENT OPENAPI SCHEMA BUG
104
+ # ============================================================
105
+ _orig_get_type = gradio_client.utils.get_type
106
+ def _safe_get_type(schema):
107
+ if isinstance(schema, bool):
108
+ return "boolean"
109
+ if not isinstance(schema, dict):
110
+ return "str"
111
+ return _orig_get_type(schema)
112
+ gradio_client.utils.get_type = _safe_get_type
113
+
114
+ _orig_json_schema = gradio_client.utils._json_schema_to_python_type
115
+ def _safe_json_schema(schema, defs=None):
116
+ if isinstance(schema, bool):
117
+ return "bool"
118
+ if not isinstance(schema, dict):
119
+ return "str"
120
+ return _orig_json_schema(schema, defs)
121
+ gradio_client.utils._json_schema_to_python_type = _safe_json_schema
122
+
123
+ # ============================================================
124
+ # 3. KONFIGURASI PATH & DIREKTORI
125
+ # ============================================================
126
+ ROOT = pathlib.Path(__file__).resolve().parent
127
+ COMFY = ROOT / "ComfyUI"
128
+ MODELS = COMFY / "models"
129
+ INPUT = COMFY / "input"
130
+ OUTPUT = COMFY / "output"
131
+ LOCAL_CUSTOM_NODES = ROOT / "custom_nodes"
132
+
133
+ WORKFLOW_FILE = ROOT / "workflow_clip.json"
134
+ NODE_OUTPUT_ID = "save_h3_cond"
135
+
136
+ # ============================================================
137
+ # 4. DAFTAR MODEL KONDISIONER MINIMAX-H3 (INT8 OPTIMIZED)
138
+ # ============================================================
139
+ DOWNLOADS = [
140
+ {
141
+ "repo": "Comfy-Org/MiniMax-H3",
142
+ "file": "text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
143
+ "dest": MODELS / "text_encoders" / "qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
144
+ "alt_dest": MODELS / "clip" / "qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
145
+ "label": "Text Encoder (Qwen3-VL 32B NVFP4/AWQ ~15.7GB)",
146
+ },
147
+ {
148
+ "repo": "Comfy-Org/MiniMax-H3",
149
+ "file": "vae/minimax_h3_video_vae_int8_convrot.safetensors",
150
+ "dest": MODELS / "vae" / "minimax_h3_video_vae_int8_convrot.safetensors",
151
+ "alt_dest": None,
152
+ "label": "Video VAE INT8 ConvRot (~2.6GB)",
153
+ },
154
+ ]
155
+
156
+ CUSTOM_NODES: list[tuple[str, str]] = []
157
+
158
+ _comfy_ready = False
159
+ _nodes_ready = False
160
+ server_instance = None
161
+
162
+ # ============================================================
163
+ # 5. HELPER & MODEL DOWNLOADER
164
+ # ============================================================
165
+ def _run_cmd(cmd: list[str], cwd: pathlib.Path = ROOT, check: bool = True) -> None:
166
+ print(f"[*] Menjalankan: {' '.join(cmd)} di {cwd}", flush=True)
167
+ subprocess.run(cmd, cwd=cwd, check=check)
168
+
169
+ def _link_or_copy(src: pathlib.Path, dest: pathlib.Path) -> None:
170
+ dest.parent.mkdir(parents=True, exist_ok=True)
171
+ if dest.is_symlink():
172
+ dest.unlink()
173
+ if dest.exists() and dest.stat().st_size > 1000:
174
+ return
175
+ try:
176
+ os.link(src, dest)
177
+ return
178
+ except OSError:
179
+ pass
180
+ shutil.copy2(src, dest)
181
+
182
+ def _download_to_dest(repo: str, file_path: str, dest: pathlib.Path, token: str | None) -> None:
183
+ dest.parent.mkdir(parents=True, exist_ok=True)
184
+ if dest.is_symlink():
185
+ dest.unlink()
186
+ if dest.exists() and dest.stat().st_size > 1000:
187
+ return
188
+
189
+ p = pathlib.Path(file_path)
190
+ filename = p.name
191
+ subfolder = str(p.parent) if str(p.parent) != "." else None
192
+
193
+ print(f"[*] Mengunduh {filename} dari {repo} ke {dest.parent}...", flush=True)
194
+ downloaded_str = hf_hub_download(
195
+ repo_id=repo,
196
+ filename=filename,
197
+ subfolder=subfolder,
198
+ local_dir=str(dest.parent),
199
+ token=token,
200
+ )
201
+ downloaded = pathlib.Path(downloaded_str)
202
+
203
+ if downloaded.resolve() == dest.resolve():
204
+ return
205
+
206
+ if dest.exists() or dest.is_symlink():
207
+ dest.unlink()
208
+ dest.parent.mkdir(parents=True, exist_ok=True)
209
+ try:
210
+ os.replace(downloaded, dest)
211
+ except OSError:
212
+ shutil.copy2(downloaded, dest)
213
+ if downloaded.exists():
214
+ downloaded.unlink()
215
+
216
+ sub_dir = dest.parent / "split_files"
217
+ if sub_dir.exists():
218
+ shutil.rmtree(sub_dir, ignore_errors=True)
219
+
220
+ def _install_filtered_requirements(req_path: pathlib.Path, cwd: pathlib.Path) -> None:
221
+ if not req_path.exists():
222
+ return
223
+ blocked = {"torch", "torchvision", "torchaudio", "transformers", "huggingface-hub", "accelerate", "xformers"}
224
+ safe: list[str] = []
225
+ for line in req_path.read_text(encoding="utf-8", errors="ignore").splitlines():
226
+ item = line.strip()
227
+ if not item or item.startswith("#"):
228
+ continue
229
+ low = item.lower().replace("_", "-")
230
+ package = re.split(r"[<>=!~;\[\s]", low, maxsplit=1)[0]
231
+ if package in blocked:
232
+ continue
233
+ safe.append(item)
234
+ if safe:
235
+ filtered_file = cwd / "requirements_filtered.txt"
236
+ filtered_file.write_text("\n".join(safe) + "\n", encoding="utf-8")
237
+ _run_cmd([sys.executable, "-m", "pip", "install", "-r", "requirements_filtered.txt", "--no-cache-dir"], cwd=cwd, check=False)
238
+
239
+ def _apply_comfy_utils_namespace_fix() -> None:
240
+ utils_path = COMFY / "utils"
241
+ utilities_path = COMFY / "utilities"
242
+ if utils_path.exists() and not utilities_path.exists():
243
+ try:
244
+ utils_path.rename(utilities_path)
245
+ except OSError:
246
+ pass
247
+
248
+ replacements = [
249
+ (re.compile(r"(^|\n)(\s*)from utils(\s|\.)"), r"\1\2from utilities\3"),
250
+ (re.compile(r"(^|\n)(\s*)import utils(\s|\.|$)"), r"\1\2import utilities\3"),
251
+ ]
252
+ for path in COMFY.rglob("*.py"):
253
+ if "__pycache__" in path.parts:
254
+ continue
255
+ try:
256
+ text = path.read_text(encoding="utf-8")
257
+ except UnicodeDecodeError:
258
+ continue
259
+ updated = text
260
+ for pattern, repl in replacements:
261
+ updated = pattern.sub(repl, updated)
262
+ updated = updated.replace("from utils import", "from utilities import")
263
+ if updated != text:
264
+ path.write_text(updated, encoding="utf-8")
265
+
266
+ def _ensure_comfy() -> None:
267
+ global _comfy_ready
268
+ if _comfy_ready:
269
+ return
270
+
271
+ print("[1/3] Menyiapkan ComfyUI Runtime untuk H3 Conditioner...", flush=True)
272
+ if not COMFY.exists():
273
+ _run_cmd(["git", "clone", "--depth", "1", "--branch", "v0.38.2", "https://github.com/comfyanonymous/ComfyUI.git", str(COMFY)])
274
+ _install_filtered_requirements(COMFY / "requirements.txt", COMFY)
275
+
276
+ custom_root = COMFY / "custom_nodes"
277
+ custom_root.mkdir(parents=True, exist_ok=True)
278
+
279
+ # Pasang local thin wire node: save_h3_conditioning
280
+ if LOCAL_CUSTOM_NODES.exists():
281
+ for src_node in LOCAL_CUSTOM_NODES.iterdir():
282
+ if src_node.is_dir() and not src_node.name.startswith("."):
283
+ target_node = custom_root / src_node.name
284
+ if target_node.exists():
285
+ shutil.rmtree(target_node, ignore_errors=True)
286
+ shutil.copytree(src_node, target_node)
287
+ print(f"[*] Terpasang local custom node: {src_node.name}", flush=True)
288
+
289
+ _apply_comfy_utils_namespace_fix()
290
+
291
+ for folder in ("text_encoders", "clip", "vae"):
292
+ (MODELS / folder).mkdir(parents=True, exist_ok=True)
293
+ INPUT.mkdir(parents=True, exist_ok=True)
294
+ OUTPUT.mkdir(parents=True, exist_ok=True)
295
+
296
+ _comfy_ready = True
297
+ print("[1/3] ComfyUI Runtime H3 Conditioner Siap.", flush=True)
298
+
299
+ def _ensure_models(progress=None) -> None:
300
+ print("[2/3] Memeriksa & Mengunduh Model Conditioner MiniMax-H3...", flush=True)
301
+ token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
302
+ for row in DOWNLOADS:
303
+ dest = pathlib.Path(row["dest"])
304
+ dest.parent.mkdir(parents=True, exist_ok=True)
305
+ if dest.is_symlink():
306
+ dest.unlink()
307
+ if not (dest.exists() and dest.stat().st_size > 1000):
308
+ print(f"[*] Mengunduh {row['label']}...", flush=True)
309
+ _download_to_dest(row["repo"], row["file"], dest, token)
310
+
311
+ alt = row.get("alt_dest")
312
+ if alt is not None:
313
+ alt_path = pathlib.Path(alt)
314
+ if dest.exists() and dest.stat().st_size > 1000:
315
+ _link_or_copy(dest, alt_path)
316
+ print("[2/3] Semua model conditioner MiniMax-H3 telah siap.", flush=True)
317
+
318
+ def _init_comfy_nodes() -> None:
319
+ global _nodes_ready, server_instance
320
+ if _nodes_ready:
321
+ return
322
+
323
+ print("[3/3] Menginisialisasi Engine ComfyUI Conditioner (Full Standby)...", flush=True)
324
+ comfy_path = str(COMFY)
325
+ sys.path = [p for p in sys.path if p != comfy_path]
326
+ sys.path.insert(0, comfy_path)
327
+ for module_name in list(sys.modules):
328
+ if module_name in ("utils", "app") or module_name.startswith(("utils.", "app.")):
329
+ del sys.modules[module_name]
330
+
331
+ os.chdir(COMFY)
332
+
333
+ import execution
334
+ import nodes
335
+ import server
336
+
337
+ loop = asyncio.new_event_loop()
338
+ asyncio.set_event_loop(loop)
339
+
340
+ import inspect
341
+ sig = inspect.signature(server.PromptServer.__init__)
342
+ if "asset_manager" in sig.parameters:
343
+ try:
344
+ from app.assets.manager import default_asset_manager
345
+ asset_mgr = default_asset_manager()
346
+ except Exception:
347
+ class DummyAssetManager:
348
+ enabled = False
349
+ def startup(self): pass
350
+ def shutdown(self): pass
351
+ def register_routes(self, app, user_manager=None): pass
352
+ def ensure_scan_started(self): pass
353
+ def pause_background_scan(self): pass
354
+ def queue_output_scan(self): pass
355
+ def resume_background_scan(self): pass
356
+ def register_upload(self, *args, **kwargs): return None
357
+ def register_executed_output(self, *args, **kwargs): return None
358
+ def register_cached_output(self, *args, **kwargs): return None
359
+ def set_event_sink(self, sink): pass
360
+ asset_mgr = DummyAssetManager()
361
+ server_instance = server.PromptServer(loop, asset_mgr)
362
+ else:
363
+ server_instance = server.PromptServer(loop)
364
+
365
+ try:
366
+ execution.PromptQueue(server_instance)
367
+ except Exception:
368
+ pass
369
+
370
+ res = nodes.init_extra_nodes()
371
+ if asyncio.iscoroutine(res):
372
+ loop.run_until_complete(res)
373
+
374
+ _nodes_ready = True
375
+ print("[3/3] Engine ComfyUI Conditioner Siap & Berada dalam Mode Hot-Standby.", flush=True)
376
+
377
+ executor_instance = None
378
+
379
+ def _get_or_create_executor():
380
+ global executor_instance
381
+ if executor_instance is None:
382
+ import execution
383
+ executor_instance = execution.PromptExecutor(
384
+ server_instance,
385
+ cache_type=execution.CacheType.RAM_PRESSURE,
386
+ cache_args={"lru": 32, "ram": 60.0, "ram_inactive": 60.0},
387
+ )
388
+ return executor_instance
389
+
390
+ def _preload_models_to_ram():
391
+ """Me-load Qwen3-VL 32B (~15.7GB) dan Video VAE INT8 (~2.6GB) ke RAM saat boot."""
392
+ print("[*] Pre-loading Text Encoder (Qwen3-VL 32B) & Video VAE INT8 ke RAM...", flush=True)
393
+ try:
394
+ import nodes
395
+ clip_loader = nodes.CLIPLoader()
396
+ vae_loader = nodes.VAELoader()
397
+
398
+ print("[*] Pre-loading Qwen3-VL 32B...", flush=True)
399
+ clip_loader.load_clip("qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors", type="minimax", device="default")
400
+
401
+ print("[*] Pre-loading Video VAE INT8...", flush=True)
402
+ vae_loader.load_vae("minimax_h3_video_vae_int8_convrot.safetensors")
403
+
404
+ print("[*] Pre-load Qwen3-VL & VAE ke RAM berhasil! (Zero Disk Reload).", flush=True)
405
+ except Exception as e:
406
+ print(f"[!] Warning saat pre-load model: {e}", flush=True)
407
+
408
+ # ============================================================
409
+ # 6. ROOT STARTUP PRE-WARMING (HOT-STANDBY OPTIMIZATION)
410
+ # ============================================================
411
+ def _startup_prewarm():
412
+ print("=" * 60, flush=True)
413
+ print("[startup] Memulai Pre-Warming Engine H3 Conditioner...", flush=True)
414
+ _ensure_comfy()
415
+ _ensure_models()
416
+ _init_comfy_nodes()
417
+ _get_or_create_executor()
418
+ _preload_models_to_ram()
419
+ print("[startup] Pre-Warming Selesai. Siap Melayani Permintaan Thin Wire.", flush=True)
420
+ print("=" * 60, flush=True)
421
+
422
+ _startup_prewarm()
423
+
424
+ # ============================================================
425
+ # 7. EKSEKUSI WORKFLOW COMFYUI
426
+ # ============================================================
427
+ def _load_base_workflow() -> dict[str, Any]:
428
+ with open(WORKFLOW_FILE, "r", encoding="utf-8") as f:
429
+ return json.load(f)
430
+
431
+ def _run_comfy_workflow(workflow: dict[str, Any]) -> str:
432
+ """Eksekusi workflow ComfyUI menggunakan persistent PromptExecutor dengan Profiler."""
433
+ import execution
434
+
435
+ executor = _get_or_create_executor()
436
+ prompt_id = str(uuid.uuid4())
437
+
438
+ node_durations: list[tuple[str, str, float]] = []
439
+ orig_get_output_data = execution.get_output_data
440
+
441
+ def _profiling_get_output_data(obj, input_data_all, *args, **kwargs):
442
+ if isinstance(obj, str):
443
+ node_id = obj
444
+ node_info = workflow.get(node_id, {})
445
+ class_type = node_info.get("class_type", "UnknownNode")
446
+ node_title = node_info.get("_meta", {}).get("title", class_type)
447
+ label = f"[Node {node_id}: {node_title}]"
448
+ else:
449
+ node_id = "?"
450
+ class_type = obj.__class__.__name__
451
+ label = f"[{class_type}]"
452
+
453
+ t0 = time.time()
454
+ print(f"🚀 {label} Mulai dieksekusi...", flush=True)
455
+ try:
456
+ res = orig_get_output_data(obj, input_data_all, *args, **kwargs)
457
+ dur = time.time() - t0
458
+ node_durations.append((node_id, label, dur))
459
+ print(f"⏱️ {label} Selesai dalam: {dur:.2f}s", flush=True)
460
+ return res
461
+ except Exception as e:
462
+ dur = time.time() - t0
463
+ print(f"❌ {label} Gagal setelah: {dur:.2f}s ({e})", flush=True)
464
+ raise
465
+
466
+ execution.get_output_data = _profiling_get_output_data
467
+ t_workflow_start = time.time()
468
+
469
+ try:
470
+ executor.execute(
471
+ workflow,
472
+ prompt_id,
473
+ extra_data={},
474
+ execute_outputs=[NODE_OUTPUT_ID],
475
+ )
476
+ finally:
477
+ execution.get_output_data = orig_get_output_data
478
+ t_workflow_total = time.time() - t_workflow_start
479
+ print("\n" + "=" * 70, flush=True)
480
+ print("📊 REKAPITULASI PROFILING WAKTU SPACE 1 (CONDITIONER):", flush=True)
481
+ print("=" * 70, flush=True)
482
+ sorted_nodes = sorted(node_durations, key=lambda x: x[2], reverse=True)
483
+ for nid, label, dur in sorted_nodes:
484
+ pct = (dur / t_workflow_total * 100) if t_workflow_total > 0 else 0
485
+ bar = "█" * int(pct // 5)
486
+ print(f" {label:<45} : {dur:>6.2f}s ({pct:>5.1f}%) {bar}", flush=True)
487
+ print("-" * 70, flush=True)
488
+ print(f" ⏱️ TOTAL DURASI ENCODE CONDITIONING : {t_workflow_total:.2f} detik", flush=True)
489
+ print("=" * 70 + "\n", flush=True)
490
+
491
+ if not executor.success:
492
+ err = (
493
+ executor.status_messages[-1]
494
+ if hasattr(executor, "status_messages") and executor.status_messages
495
+ else "ComfyUI execution gagal"
496
+ )
497
+ raise RuntimeError(str(err))
498
+
499
+ # Cari file safetensors terbaru di OUTPUT
500
+ files = [
501
+ pathlib.Path(p)
502
+ for p in glob.glob(str(OUTPUT / "**" / "*.safetensors"), recursive=True)
503
+ ]
504
+ if not files:
505
+ raise RuntimeError("Encoding selesai tetapi file .safetensors tidak ditemukan di output.")
506
+
507
+ latest_file = sorted(files, key=lambda p: p.stat().st_mtime, reverse=True)[0]
508
+ return str(latest_file)
509
+
510
+ # ============================================================
511
+ # 8. LOGIKA RUNNER ZERO-OVERHEAD @SPACES.GPU (DYNAMIC DURATION)
512
+ # ============================================================
513
+ def get_conditioner_duration(
514
+ prompt: str = "",
515
+ first_frame_path: str = "",
516
+ last_frame_path: str = "",
517
+ duration: float | str = "5s",
518
+ megapixels: float = 0.4,
519
+ aspect_ratio: str = "",
520
+ ) -> int:
521
+ """
522
+ Kalkulasi alokasi ZeroGPU dinamis dan aman untuk Space 1.
523
+ - Hanya First Frame: 45 detik (aktual ~11.6s, reserve: 67.5s)
524
+ - First Frame + Last Frame: 60 detik (aktual ~20-25s, reserve: 90s)
525
+ """
526
+ has_last = bool(last_frame_path and str(last_frame_path).strip())
527
+ return 60 if has_last else 45
528
+
529
+ @spaces.GPU(duration=get_conditioner_duration)
530
+ def encode_h3_conditioning(
531
+ prompt: str = "",
532
+ first_frame_path: str = "",
533
+ last_frame_path: str = "",
534
+ duration: float | str = "5s",
535
+ megapixels: float = 0.4,
536
+ aspect_ratio: str = "",
537
+ ) -> str:
538
+ """
539
+ Eksekusi forward pass untuk encode Qwen3-VL 32B conditioning + Video VAE Keyframes.
540
+ Hasilnya berupa file .safetensors (Thin Wire) berisi cond_embed, token tags, & keyframe latents.
541
+ """
542
+ if not first_frame_path or not os.path.exists(first_frame_path):
543
+ raise ValueError("First Frame wajib diunggah untuk mode Image-to-Video (I2V) MiniMax-H3.")
544
+
545
+ try:
546
+ dur_val = float(str(duration).replace("s", "").strip())
547
+ except Exception:
548
+ dur_val = 5.0
549
+
550
+ wf = _load_base_workflow()
551
+
552
+ # 1. Handle Keyframe First Frame (Wajib)
553
+ first_ext = pathlib.Path(first_frame_path).suffix or ".png"
554
+ first_name = f"first_{uuid.uuid4().hex[:8]}{first_ext}"
555
+ shutil.copy2(first_frame_path, INPUT / first_name)
556
+ wf["122"]["inputs"]["image"] = first_name
557
+
558
+ # 2. Handle Keyframe Last Frame (Opsional)
559
+ if last_frame_path and os.path.exists(last_frame_path):
560
+ last_ext = pathlib.Path(last_frame_path).suffix or ".png"
561
+ last_name = f"last_{uuid.uuid4().hex[:8]}{last_ext}"
562
+ shutil.copy2(last_frame_path, INPUT / last_name)
563
+ wf["123"]["inputs"]["image"] = last_name
564
+ wf["105_104"]["inputs"]["last_frame"] = ["123", 0]
565
+ else:
566
+ if "last_frame" in wf["105_104"]["inputs"]:
567
+ del wf["105_104"]["inputs"]["last_frame"]
568
+ if "123" in wf:
569
+ del wf["123"]
570
+
571
+ # 3. Inject Megapixels (dikunci 0.4 MP), Durasi, Prompt
572
+ prefix = f"h3_{uuid.uuid4().hex[:8]}"
573
+ wf["119"]["inputs"]["megapixels"] = 0.4
574
+ wf["105_111"]["inputs"]["value"] = dur_val
575
+ wf["105_104"]["inputs"]["prompt"] = str(prompt) if prompt else ""
576
+ wf["save_h3_cond"]["inputs"]["filename_prefix"] = prefix
577
+ wf["save_h3_cond"]["inputs"]["prompt_text"] = str(prompt) if prompt else ""
578
+
579
+ print(f"[*] [GPU] Menjalankan MiniMax-H3 I2V Conditioning (0.4 MP, {dur_val}s, has_last_frame={bool(last_frame_path)})", flush=True)
580
+
581
+ target_file = _run_comfy_workflow(wf)
582
+ size_kb = os.path.getsize(target_file) / 1024.0
583
+ print(f"[*] [GPU Selesai] File conditioning: {target_file} ({size_kb:.2f} KB)", flush=True)
584
+
585
+ return target_file
586
+
587
+ def get_system_info() -> str:
588
+ info = []
589
+ info.append("### 🧠 Status Conditioner Service (Space 1 - Hot Standby)")
590
+ info.append(f"- **PyTorch Version:** `{torch.__version__}`")
591
+ info.append(f"- **CUDA Available:** `{torch.cuda.is_available()}`")
592
+ if torch.cuda.is_available():
593
+ info.append(f"- **CUDA Version:** `{torch.version.cuda}`")
594
+ info.append(f"- **Device Name:** `{torch.cuda.get_device_name(0)}`")
595
+ props = torch.cuda.get_device_properties(0)
596
+ info.append(f"- **VRAM:** `{props.total_memory / (1024**3):.2f} GB`")
597
+ info.append(f"- **Compute Capability:** `{props.major}.{props.minor}`")
598
+ info.append("- **Model Loaded:** `Qwen3-VL 32B NVFP4/AWQ` (~15.7 GB)")
599
+ info.append("- **Model VAE:** `minimax_h3_video_vae_int8_convrot` (~2.6 GB)")
600
+ info.append("- **Mode:** `Image-to-Video (I2V) Hot-Standby Pre-Warmed Engine`")
601
+ info.append("- **Dynamic Duration:** `20s (5s) | 25s (10s/15s)`")
602
+ info.append("- **Custom Nodes:** `0 External Repos (Pure ComfyUI Core Native)`")
603
+ info.append("- **Wire Output:** `.safetensors` binary format")
604
+ return "\n".join(info)
605
+
606
+ # ============================================================
607
+ # 9. ANTARMUKA GRADIO & API ENDPOINT
608
+ # ============================================================
609
+ with gr.Blocks(title="DualSpace MiniMax-H3 Conditioner Service") as demo:
610
+ gr.Markdown(
611
+ """
612
+ # 🧠 DualSpace MiniMax-H3 — Text/Vision Conditioner Service (I2V)
613
+ Space ini berfungsi sebagai **Conditioner as a Service** (Backend) untuk memproses Qwen3-VL 32B Text/Vision Encoder dan Video VAE Keyframes.
614
+ Mengembalikan file `.safetensors` (Thin Wire Protocol) untuk di-stream ke Space Generator.
615
+ """
616
+ )
617
+
618
+ with gr.Row():
619
+ with gr.Column():
620
+ first_frame_in = gr.Image(type="filepath", label="First Frame (Wajib)")
621
+ last_frame_in = gr.Image(type="filepath", label="Last Frame (Opsional)")
622
+ prompt_in = gr.Textbox(label="Prompt (Opsional)", value="A cinematic video with smooth motion, high aesthetic quality")
623
+ dur_in = gr.Radio(choices=["5s", "10s", "15s"], value="5s", label="Durasi Video")
624
+ btn_encode = gr.Button("⚡ Encode Conditioning (GPU)", variant="primary")
625
+
626
+ with gr.Column():
627
+ file_out = gr.File(label="Output .safetensors (Thin Wire)")
628
+
629
+ btn_encode.click(
630
+ fn=encode_h3_conditioning,
631
+ inputs=[prompt_in, first_frame_in, last_frame_in, dur_in],
632
+ outputs=[file_out],
633
+ api_name="encode",
634
+ )
635
+
636
+ with gr.Accordion("🛠️ Info Hardware & Status", open=False):
637
+ info_btn = gr.Button("🔍 Cek Status Sistem")
638
+ info_markdown = gr.Markdown(value=get_system_info())
639
+ info_btn.click(fn=get_system_info, outputs=[info_markdown])
640
+
641
+ if __name__ == "__main__":
642
+ demo.queue(max_size=20).launch(show_error=True)
custom_nodes/save_h3_conditioning/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
2
+
3
+ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
custom_nodes/save_h3_conditioning/nodes.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import json
4
+ import torch
5
+ import safetensors.torch as st
6
+ import folder_paths
7
+
8
+ class SaveH3ConditioningSafetensors:
9
+ """
10
+ Menyimpan tensor ComfyUI CONDITIONING hasil MiniMaxH3ImageToVideo (Qwen3-VL + Keyframes)
11
+ ke file .safetensors (Thin Wire Protocol: embedding multimodal + keyframe latents).
12
+ """
13
+ @classmethod
14
+ def INPUT_TYPES(cls):
15
+ return {
16
+ "required": {
17
+ "conditioning": ("CONDITIONING",),
18
+ "filename_prefix": ("STRING", {"default": "h3_cond"}),
19
+ "width": ("INT", {"default": 896}),
20
+ "height": ("INT", {"default": 504}),
21
+ "length": ("INT", {"default": 97}),
22
+ },
23
+ "optional": {
24
+ "prompt_text": ("STRING", {"default": ""}),
25
+ }
26
+ }
27
+
28
+ RETURN_TYPES = ()
29
+ OUTPUT_NODE = True
30
+ FUNCTION = "save"
31
+ CATEGORY = "conditioning/minimax_h3"
32
+
33
+ def save(self, conditioning, filename_prefix="h3_cond", width=896, height=504, length=97, prompt_text=""):
34
+ output_dir = folder_paths.get_output_directory()
35
+ filename = f"{filename_prefix}_{uuid.uuid4().hex[:10]}.safetensors"
36
+ filepath = os.path.join(output_dir, filename)
37
+
38
+ # ComfyUI CONDITIONING: list of tuples [[embed_tensor, extra_dict]]
39
+ cond_embed = conditioning[0][0].contiguous().cpu()
40
+ extra_dict = conditioning[0][1] if len(conditioning[0]) > 1 else {}
41
+ pooled = extra_dict.get("pooled_output", None)
42
+
43
+ tensors = {
44
+ "cond_embed": cond_embed,
45
+ }
46
+
47
+ # Simpan minimax_token_tags jika ada
48
+ token_tags = extra_dict.get("minimax_token_tags", None)
49
+ if token_tags is not None and isinstance(token_tags, torch.Tensor):
50
+ tensors["minimax_token_tags"] = token_tags.contiguous().cpu()
51
+
52
+ # Simpan minimax_keyframes latents jika ada
53
+ keyframes_meta = []
54
+ raw_keyframes = extra_dict.get("minimax_keyframes", None)
55
+ if raw_keyframes is not None and isinstance(raw_keyframes, list):
56
+ for i, kf in enumerate(raw_keyframes):
57
+ kf_entry = {
58
+ "resolved_frame_index": int(kf.get("resolved_frame_index", 0))
59
+ }
60
+ latent_val = kf.get("latent", None)
61
+ if latent_val is not None and isinstance(latent_val, torch.Tensor):
62
+ tensor_key = f"kf_{i}_latent"
63
+ tensors[tensor_key] = latent_val.contiguous().cpu()
64
+ kf_entry["tensor_key"] = tensor_key
65
+ keyframes_meta.append(kf_entry)
66
+
67
+ if pooled is not None and isinstance(pooled, torch.Tensor):
68
+ tensors["cond_pooled"] = pooled.contiguous().cpu()
69
+
70
+ metadata = {
71
+ "prompt": str(prompt_text),
72
+ "width": str(width),
73
+ "height": str(height),
74
+ "length": str(length),
75
+ "keyframes_meta": json.dumps(keyframes_meta),
76
+ "shape": str(list(cond_embed.shape)),
77
+ "format": "comfyui_h3_conditioning_v2",
78
+ }
79
+
80
+ st.save_file(tensors, filepath, metadata=metadata)
81
+ size_kb = os.path.getsize(filepath) / 1024.0
82
+ print(f"[*] [Space 1] Safetensors tersimpan: {filepath} ({size_kb:.2f} KB, keyframes: {len(keyframes_meta)})", flush=True)
83
+
84
+ return {"ui": {"safetensors_file": [filename]}}
85
+
86
+ NODE_CLASS_MAPPINGS = {
87
+ "SaveH3ConditioningSafetensors": SaveH3ConditioningSafetensors
88
+ }
89
+ NODE_DISPLAY_NAME_MAPPINGS = {
90
+ "SaveH3ConditioningSafetensors": "Save H3 Conditioning Safetensors"
91
+ }
requirements.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cu130
2
+ --extra-index-url https://download.pytorch.org/whl/cu128
3
+ torch
4
+ torchvision
5
+ torchaudio
6
+ torchsde
7
+ spaces
8
+ gradio>=5,<6
9
+ gradio_client>=1.0.0
10
+ huggingface_hub>=0.34.0
11
+ transformers>=4.48.0
12
+ accelerate>=0.26.0
13
+ safetensors
14
+ einops
15
+ scipy
16
+ numpy
17
+ pillow
18
+ psutil
19
+ websocket-client
20
+ spandrel
21
+ kornia
22
+ av
23
+ color-matcher
24
+ matplotlib
25
+ mss
26
+ opencv-python-headless
27
+ imageio
28
+ imageio-ffmpeg
workflow_clip.json ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "122": {
3
+ "inputs": {
4
+ "image": "first_frame.png"
5
+ },
6
+ "class_type": "LoadImage",
7
+ "_meta": {
8
+ "title": "Load First Frame"
9
+ }
10
+ },
11
+ "123": {
12
+ "inputs": {
13
+ "image": "last_frame.png"
14
+ },
15
+ "class_type": "LoadImage",
16
+ "_meta": {
17
+ "title": "Load Last Frame"
18
+ }
19
+ },
20
+ "119": {
21
+ "inputs": {
22
+ "upscale_method": "nearest-exact",
23
+ "megapixels": 0.4,
24
+ "resolution_steps": 32,
25
+ "image": [
26
+ "122",
27
+ 0
28
+ ]
29
+ },
30
+ "class_type": "ImageScaleToTotalPixels",
31
+ "_meta": {
32
+ "title": "Scale Image to Total Pixels"
33
+ }
34
+ },
35
+ "120": {
36
+ "inputs": {
37
+ "image": [
38
+ "119",
39
+ 0
40
+ ]
41
+ },
42
+ "class_type": "GetImageSize",
43
+ "_meta": {
44
+ "title": "Get Image Size"
45
+ }
46
+ },
47
+ "105_111": {
48
+ "inputs": {
49
+ "value": 5.0
50
+ },
51
+ "class_type": "PrimitiveFloat",
52
+ "_meta": {
53
+ "title": "Float (duration)"
54
+ }
55
+ },
56
+ "105_107": {
57
+ "inputs": {
58
+ "expression": "max(5, round(a * 24)) + (5 - (max(5, round(a * 24)) % 17)) % 17",
59
+ "values.a": [
60
+ "105_111",
61
+ 0
62
+ ]
63
+ },
64
+ "class_type": "ComfyMathExpression",
65
+ "_meta": {
66
+ "title": "Math Expression"
67
+ }
68
+ },
69
+ "105_13": {
70
+ "inputs": {
71
+ "clip_name": "qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
72
+ "type": "minimax",
73
+ "device": "default"
74
+ },
75
+ "class_type": "CLIPLoader",
76
+ "_meta": {
77
+ "title": "Load CLIP"
78
+ }
79
+ },
80
+ "105_11": {
81
+ "inputs": {
82
+ "vae_name": "minimax_h3_video_vae_int8_convrot.safetensors"
83
+ },
84
+ "class_type": "VAELoader",
85
+ "_meta": {
86
+ "title": "Load Video VAE INT8"
87
+ }
88
+ },
89
+ "105_104": {
90
+ "inputs": {
91
+ "prompt": "",
92
+ "width": [
93
+ "120",
94
+ 0
95
+ ],
96
+ "height": [
97
+ "120",
98
+ 1
99
+ ],
100
+ "length": [
101
+ "105_107",
102
+ 1
103
+ ],
104
+ "clip": [
105
+ "105_13",
106
+ 0
107
+ ],
108
+ "vae": [
109
+ "105_11",
110
+ 0
111
+ ],
112
+ "first_frame": [
113
+ "122",
114
+ 0
115
+ ],
116
+ "last_frame": [
117
+ "123",
118
+ 0
119
+ ]
120
+ },
121
+ "class_type": "MiniMaxH3ImageToVideo",
122
+ "_meta": {
123
+ "title": "MiniMax H3 Image to Video"
124
+ }
125
+ },
126
+ "save_h3_cond": {
127
+ "inputs": {
128
+ "conditioning": [
129
+ "105_104",
130
+ 0
131
+ ],
132
+ "filename_prefix": "h3_cond",
133
+ "width": [
134
+ "120",
135
+ 0
136
+ ],
137
+ "height": [
138
+ "120",
139
+ 1
140
+ ],
141
+ "length": [
142
+ "105_107",
143
+ 1
144
+ ],
145
+ "prompt_text": ""
146
+ },
147
+ "class_type": "SaveH3ConditioningSafetensors",
148
+ "_meta": {
149
+ "title": "Save H3 Conditioning Safetensors"
150
+ }
151
+ }
152
+ }