Spaces:
Running on Zero
Running on Zero
Download app.py from alibaybay/dualspace-h3-generator: direct link, hf CLI and curl.
- Browser
- Download file 30.3 kB
-
https://huggingface.co/spaces/alibaybay/dualspace-h3-generator/resolve/main/app.py
- Command line
-
hf download hf://spaces/alibaybay/dualspace-h3-generator/app.py
-
curl -L -o app.py https://huggingface.co/spaces/alibaybay/dualspace-h3-generator/resolve/main/app.py
30.3 kB
| from __future__ import annotations | |
| import os | |
| import sys | |
| import subprocess | |
| import pathlib | |
| import shutil | |
| import re | |
| import uuid | |
| import json | |
| import glob | |
| import random | |
| import time | |
| import asyncio | |
| from functools import lru_cache | |
| from typing import Any | |
| # ============================================================ | |
| # 1. HUGGINGFACE_HUB SELF-HEALING REPAIR | |
| # ============================================================ | |
| def _hf_hub_version() -> str: | |
| try: | |
| from importlib.metadata import version as _pkg_version | |
| return _pkg_version("huggingface_hub") | |
| except Exception: | |
| return "" | |
| def _hf_hub_is_broken() -> bool: | |
| import importlib | |
| import importlib.util | |
| for module_name in ("huggingface_hub._snapshot_download", "huggingface_hub._tree_cache"): | |
| try: | |
| if importlib.util.find_spec(module_name) is None: | |
| continue | |
| except Exception: | |
| return True | |
| try: | |
| importlib.import_module(module_name) | |
| except ImportError: | |
| return True | |
| except Exception: | |
| continue | |
| return False | |
| def _hf_hub_reinstall(upgrade: bool) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "pip", | |
| "install", | |
| "--no-cache-dir", | |
| "--force-reinstall", | |
| "--no-deps", | |
| ] | |
| if upgrade: | |
| cmd += ["--upgrade", "huggingface_hub"] | |
| else: | |
| pinned = _hf_hub_version() | |
| cmd.append(f"huggingface_hub=={pinned}" if pinned else "huggingface_hub") | |
| print(f"[hf-repair] {' '.join(cmd)}", flush=True) | |
| subprocess.run(cmd, check=False) | |
| def _repair_huggingface_hub_and_restart() -> None: | |
| stage = int(os.environ.get("_HF_HUB_REPAIR_STAGE", "0") or "0") | |
| if stage >= 2 or not _hf_hub_is_broken(): | |
| return | |
| _hf_hub_reinstall(upgrade=stage == 1) | |
| os.environ["_HF_HUB_REPAIR_STAGE"] = str(stage + 1) | |
| os.execv(sys.executable, [sys.executable, *sys.argv]) | |
| _repair_huggingface_hub_and_restart() | |
| # ============================================================ | |
| # 2. IMPORTS UTAMA (SPACES WAJIB PERTAMA SEBELUM TORCH) | |
| # ============================================================ | |
| import spaces | |
| import torch | |
| import gradio as gr | |
| import gradio_client.utils | |
| from gradio_client import Client, handle_file | |
| from huggingface_hub import hf_hub_download | |
| # ============================================================ | |
| # 2.1 MONKEY-PATCH GRADIO_CLIENT OPENAPI SCHEMA BUG | |
| # ============================================================ | |
| _orig_get_type = gradio_client.utils.get_type | |
| def _safe_get_type(schema): | |
| if isinstance(schema, bool): | |
| return "boolean" | |
| if not isinstance(schema, dict): | |
| return "str" | |
| return _orig_get_type(schema) | |
| gradio_client.utils.get_type = _safe_get_type | |
| _orig_json_schema = gradio_client.utils._json_schema_to_python_type | |
| def _safe_json_schema(schema, defs=None): | |
| if isinstance(schema, bool): | |
| return "bool" | |
| if not isinstance(schema, dict): | |
| return "str" | |
| return _orig_json_schema(schema, defs) | |
| gradio_client.utils._json_schema_to_python_type = _safe_json_schema | |
| # ============================================================ | |
| # 3. KONFIGURASI PATH & DIREKTORI | |
| # ============================================================ | |
| ROOT = pathlib.Path(__file__).resolve().parent | |
| COMFY = ROOT / "ComfyUI" | |
| MODELS = COMFY / "models" | |
| INPUT = COMFY / "input" | |
| OUTPUT = COMFY / "output" | |
| LOCAL_CUSTOM_NODES = ROOT / "custom_nodes" | |
| WORKFLOW_FILE = ROOT / "workflow_generator.json" | |
| NODE_OUTPUT_ID = "92" | |
| CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER_SPACE", "alibaybay/dualspace-h3-clip") | |
| # ============================================================ | |
| # 4. DAFTAR MODEL GENERATOR MINIMAX-H3 (INT8 + TAOMATE 3-STEP) | |
| # ============================================================ | |
| DOWNLOADS = [ | |
| { | |
| "repo": "Comfy-Org/MiniMax-H3", | |
| "file": "diffusion_models/minimax_h3_fl2va_pruned_int8_convrot.safetensors", | |
| "dest": MODELS / "diffusion_models" / "minimax_h3_fl2va_pruned_int8_convrot.safetensors", | |
| "alt_dest": MODELS / "unet" / "minimax_h3_fl2va_pruned_int8_convrot.safetensors", | |
| "label": "Diffusion Model (FL2VA Pruned INT8 ~21.0GB)", | |
| }, | |
| { | |
| "repo": "Kijai/MiniMax-H3_comfy", | |
| "file": "loras/minimax_h3_taomate_3step_lora_avg_rank_19_bf16.safetensors", | |
| "dest": MODELS / "loras" / "minimax_h3_taomate_3step_lora_avg_rank_19_bf16.safetensors", | |
| "alt_dest": None, | |
| "label": "TaoMate 3-Step LoRA BF16 (~1.96GB)", | |
| }, | |
| { | |
| "repo": "Comfy-Org/MiniMax-H3", | |
| "file": "vae/minimax_h3_video_vae_int8_convrot.safetensors", | |
| "dest": MODELS / "vae" / "minimax_h3_video_vae_int8_convrot.safetensors", | |
| "alt_dest": None, | |
| "label": "Video VAE INT8 ConvRot (~2.6GB)", | |
| }, | |
| { | |
| "repo": "Comfy-Org/MiniMax-H3", | |
| "file": "vae/minimax_h3_audio_vae_fp32.safetensors", | |
| "dest": MODELS / "vae" / "minimax_h3_audio_vae_fp32.safetensors", | |
| "alt_dest": None, | |
| "label": "Audio VAE FP32 (~605MB)", | |
| }, | |
| ] | |
| # ZERO EXTERNAL CUSTOM NODES: Mengandalkan node native ComfyUI core | |
| CUSTOM_NODES: list[tuple[str, str]] = [] | |
| _comfy_ready = False | |
| _nodes_ready = False | |
| server_instance = None | |
| # ============================================================ | |
| # 5. HELPER & MODEL DOWNLOADER | |
| # ============================================================ | |
| def _run_cmd(cmd: list[str], cwd: pathlib.Path = ROOT, check: bool = True) -> None: | |
| print(f"[*] Menjalankan: {' '.join(cmd)} di {cwd}", flush=True) | |
| subprocess.run(cmd, cwd=cwd, check=check) | |
| def _link_or_copy(src: pathlib.Path, dest: pathlib.Path) -> None: | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| if dest.is_symlink(): | |
| dest.unlink() | |
| if dest.exists() and dest.stat().st_size > 1000: | |
| return | |
| try: | |
| os.link(src, dest) | |
| return | |
| except OSError: | |
| pass | |
| shutil.copy2(src, dest) | |
| def _download_to_dest(repo: str, file_path: str, dest: pathlib.Path, token: str | None) -> None: | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| if dest.is_symlink(): | |
| dest.unlink() | |
| if dest.exists() and dest.stat().st_size > 1000: | |
| return | |
| p = pathlib.Path(file_path) | |
| filename = p.name | |
| subfolder = str(p.parent) if str(p.parent) != "." else None | |
| print(f"[*] Mengunduh {filename} dari {repo} ke {dest.parent}...", flush=True) | |
| downloaded_str = hf_hub_download( | |
| repo_id=repo, | |
| filename=filename, | |
| subfolder=subfolder, | |
| local_dir=str(dest.parent), | |
| token=token, | |
| ) | |
| downloaded = pathlib.Path(downloaded_str) | |
| if downloaded.resolve() == dest.resolve(): | |
| return | |
| if dest.exists() or dest.is_symlink(): | |
| dest.unlink() | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| try: | |
| os.replace(downloaded, dest) | |
| except OSError: | |
| shutil.copy2(downloaded, dest) | |
| if downloaded.exists(): | |
| downloaded.unlink() | |
| sub_dir = dest.parent / "split_files" | |
| if sub_dir.exists(): | |
| shutil.rmtree(sub_dir, ignore_errors=True) | |
| def _install_filtered_requirements(req_path: pathlib.Path, cwd: pathlib.Path) -> None: | |
| if not req_path.exists(): | |
| return | |
| blocked = {"torch", "torchvision", "torchaudio", "transformers", "huggingface-hub", "accelerate", "xformers"} | |
| safe: list[str] = [] | |
| for line in req_path.read_text(encoding="utf-8", errors="ignore").splitlines(): | |
| item = line.strip() | |
| if not item or item.startswith("#"): | |
| continue | |
| low = item.lower().replace("_", "-") | |
| package = re.split(r"[<>=!~;\[\s]", low, maxsplit=1)[0] | |
| if package in blocked: | |
| continue | |
| safe.append(item) | |
| if safe: | |
| filtered_file = cwd / "requirements_filtered.txt" | |
| filtered_file.write_text("\n".join(safe) + "\n", encoding="utf-8") | |
| _run_cmd([sys.executable, "-m", "pip", "install", "-r", "requirements_filtered.txt", "--no-cache-dir"], cwd=cwd, check=False) | |
| def _apply_comfy_utils_namespace_fix() -> None: | |
| utils_path = COMFY / "utils" | |
| utilities_path = COMFY / "utilities" | |
| if utils_path.exists() and not utilities_path.exists(): | |
| try: | |
| utils_path.rename(utilities_path) | |
| except OSError: | |
| pass | |
| replacements = [ | |
| (re.compile(r"(^|\n)(\s*)from utils(\s|\.)"), r"\1\2from utilities\3"), | |
| (re.compile(r"(^|\n)(\s*)import utils(\s|\.|$)"), r"\1\2import utilities\3"), | |
| ] | |
| for path in COMFY.rglob("*.py"): | |
| if "__pycache__" in path.parts: | |
| continue | |
| try: | |
| text = path.read_text(encoding="utf-8") | |
| except UnicodeDecodeError: | |
| continue | |
| updated = text | |
| for pattern, repl in replacements: | |
| updated = pattern.sub(repl, updated) | |
| updated = updated.replace("from utils import", "from utilities import") | |
| if updated != text: | |
| path.write_text(updated, encoding="utf-8") | |
| def _ensure_comfy() -> None: | |
| global _comfy_ready | |
| if _comfy_ready: | |
| return | |
| print("[1/3] Menyiapkan ComfyUI Runtime untuk H3 Generator Hub...", flush=True) | |
| if not COMFY.exists(): | |
| _run_cmd(["git", "clone", "--depth", "1", "https://github.com/comfyanonymous/ComfyUI.git", str(COMFY)]) | |
| _install_filtered_requirements(COMFY / "requirements.txt", COMFY) | |
| custom_root = COMFY / "custom_nodes" | |
| custom_root.mkdir(parents=True, exist_ok=True) | |
| # Pasang local thin wire loader: external_h3_conditioning | |
| if LOCAL_CUSTOM_NODES.exists(): | |
| for src_node in LOCAL_CUSTOM_NODES.iterdir(): | |
| if src_node.is_dir() and not src_node.name.startswith("."): | |
| target_node = custom_root / src_node.name | |
| if target_node.exists(): | |
| shutil.rmtree(target_node, ignore_errors=True) | |
| shutil.copytree(src_node, target_node) | |
| print(f"[*] Terpasang local custom node: {src_node.name}", flush=True) | |
| _apply_comfy_utils_namespace_fix() | |
| for folder in ("diffusion_models", "unet", "loras", "vae"): | |
| (MODELS / folder).mkdir(parents=True, exist_ok=True) | |
| INPUT.mkdir(parents=True, exist_ok=True) | |
| OUTPUT.mkdir(parents=True, exist_ok=True) | |
| _comfy_ready = True | |
| print("[1/3] ComfyUI Runtime H3 Generator Siap.", flush=True) | |
| def _ensure_models(progress=None) -> None: | |
| print("[2/3] Memeriksa & Mengunduh Model Generator MiniMax-H3...", flush=True) | |
| token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN") | |
| for row in DOWNLOADS: | |
| dest = pathlib.Path(row["dest"]) | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| if dest.is_symlink(): | |
| dest.unlink() | |
| if not (dest.exists() and dest.stat().st_size > 1000): | |
| print(f"[*] Mengunduh {row['label']}...", flush=True) | |
| _download_to_dest(row["repo"], row["file"], dest, token) | |
| alt = row.get("alt_dest") | |
| if alt is not None: | |
| alt_path = pathlib.Path(alt) | |
| if dest.exists() and dest.stat().st_size > 1000: | |
| _link_or_copy(dest, alt_path) | |
| print("[2/3] Semua model generator MiniMax-H3 telah siap.", flush=True) | |
| def _init_comfy_nodes() -> None: | |
| global _nodes_ready, server_instance | |
| if _nodes_ready: | |
| return | |
| print("[3/3] Menginisialisasi Engine ComfyUI Generator (Full Standby)...", flush=True) | |
| comfy_path = str(COMFY) | |
| sys.path = [p for p in sys.path if p != comfy_path] | |
| sys.path.insert(0, comfy_path) | |
| for module_name in list(sys.modules): | |
| if module_name in ("utils", "app") or module_name.startswith(("utils.", "app.")): | |
| del sys.modules[module_name] | |
| os.chdir(COMFY) | |
| import execution | |
| import nodes | |
| import server | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| import inspect | |
| sig = inspect.signature(server.PromptServer.__init__) | |
| if "asset_manager" in sig.parameters: | |
| try: | |
| from app.assets.manager import default_asset_manager | |
| asset_mgr = default_asset_manager() | |
| except Exception: | |
| class DummyAssetManager: | |
| enabled = False | |
| def startup(self): pass | |
| def shutdown(self): pass | |
| def register_routes(self, app, user_manager=None): pass | |
| def ensure_scan_started(self): pass | |
| def pause_background_scan(self): pass | |
| def queue_output_scan(self): pass | |
| def resume_background_scan(self): pass | |
| def register_upload(self, *args, **kwargs): return None | |
| def register_executed_output(self, *args, **kwargs): return None | |
| def register_cached_output(self, *args, **kwargs): return None | |
| def set_event_sink(self, sink): pass | |
| asset_mgr = DummyAssetManager() | |
| server_instance = server.PromptServer(loop, asset_mgr) | |
| else: | |
| server_instance = server.PromptServer(loop) | |
| try: | |
| execution.PromptQueue(server_instance) | |
| except Exception: | |
| pass | |
| res = nodes.init_extra_nodes() | |
| if asyncio.iscoroutine(res): | |
| loop.run_until_complete(res) | |
| _nodes_ready = True | |
| print("[3/3] Engine ComfyUI Generator Siap & Berada dalam Mode Hot-Standby.", flush=True) | |
| executor_instance = None | |
| def _get_or_create_executor(): | |
| global executor_instance | |
| if executor_instance is None: | |
| import execution | |
| executor_instance = execution.PromptExecutor( | |
| server_instance, | |
| cache_type=execution.CacheType.RAM_PRESSURE, | |
| cache_args={"lru": 32, "ram": 60.0, "ram_inactive": 60.0}, | |
| ) | |
| return executor_instance | |
| def _preload_models_to_ram(): | |
| """Me-load UNet INT8, LoRA TaoMate 3-Step, Video VAE INT8, dan Audio VAE ke RAM saat boot.""" | |
| print("[*] Pre-loading bobot model (UNet INT8 ~21GB, LoRA, VAE INT8) ke RAM...", flush=True) | |
| try: | |
| import nodes | |
| unet_loader = nodes.UNETLoader() | |
| vae_loader = nodes.VAELoader() | |
| lora_loader = nodes.LoraLoaderModelOnly() | |
| # 1. Load UNet INT8 | |
| print("[*] Pre-loading UNet INT8...", flush=True) | |
| unet_res = unet_loader.load_unet("minimax_h3_fl2va_pruned_int8_convrot.safetensors", weight_dtype="default") | |
| unet_model = unet_res[0] | |
| # 2. Patch LoRA TaoMate 3-Step | |
| print("[*] Pre-patching LoRA TaoMate 3-Step...", flush=True) | |
| lora_loader.load_lora_model_only(unet_model, "minimax_h3_taomate_3step_lora_avg_rank_19_bf16.safetensors", 1.0) | |
| # 3. Load Video VAE INT8 & Audio VAE | |
| print("[*] Pre-loading Video VAE INT8 & Audio VAE...", flush=True) | |
| vae_loader.load_vae("minimax_h3_video_vae_int8_convrot.safetensors") | |
| vae_loader.load_vae("minimax_h3_audio_vae_fp32.safetensors") | |
| print("[*] Pre-load model ke RAM berhasil! Semua model telah siap di memory (Zero Disk Reload).", flush=True) | |
| except Exception as e: | |
| print(f"[!] Warning saat pre-load model: {e}", flush=True) | |
| # ============================================================ | |
| # 6. ROOT STARTUP PRE-WARMING (HOT-STANDBY OPTIMIZATION) | |
| # ============================================================ | |
| def _startup_prewarm(): | |
| print("=" * 60, flush=True) | |
| print("[startup] Memulai Pre-Warming Engine H3 Generator Hub...", flush=True) | |
| _ensure_comfy() | |
| _ensure_models() | |
| _init_comfy_nodes() | |
| _get_or_create_executor() | |
| _preload_models_to_ram() | |
| print("[startup] Pre-Warming Selesai. Siap Melayani Permintaan Instan.", flush=True) | |
| print("=" * 60, flush=True) | |
| _startup_prewarm() | |
| # ============================================================ | |
| # 7. PERSISTENT LRU CLIENT POOL KE SPACE KONDISIONER | |
| # ============================================================ | |
| def _get_conditioner_client(space_id: str, ip_token: str | None) -> Client: | |
| """Membuka dan mempertahankan sesi koneksi HTTP / WebSocket ke Space 1.""" | |
| headers = {"x-ip-token": ip_token} if ip_token else {} | |
| print(f"[*] [LRU Pool] Membuka persistent connection ke Conditioner: {space_id}", flush=True) | |
| return Client(space_id, headers=headers) | |
| def fetch_remote_conditioning( | |
| prompt: str, | |
| first_frame_path: str, | |
| last_frame_path: str | None, | |
| duration: float, | |
| ip_token: str | None, | |
| ) -> str: | |
| """Mengirim parameter ke Space 1 via Thin Wire API dan menerima file .safetensors.""" | |
| client = _get_conditioner_client(CONDITIONER_SPACE, ip_token) | |
| print(f"[*] [Space 2] Meminta conditioning dari Space 1 ({CONDITIONER_SPACE})...", flush=True) | |
| res = client.predict( | |
| prompt=prompt, | |
| first_frame_path=handle_file(first_frame_path), | |
| last_frame_path=handle_file(last_frame_path) if last_frame_path else None, | |
| duration=f"{duration:.0f}s", | |
| api_name="/encode", | |
| ) | |
| if isinstance(res, (tuple, list)): | |
| remote_file = res[0] | |
| elif isinstance(res, dict) and "path" in res: | |
| remote_file = res["path"] | |
| else: | |
| remote_file = str(res) | |
| size_kb = os.path.getsize(remote_file) / 1024.0 if os.path.exists(remote_file) else 0 | |
| print(f"[*] [Space 2] Berhasil menerima Thin Wire safetensors: {remote_file} ({size_kb:.2f} KB)", flush=True) | |
| return remote_file | |
| # ============================================================ | |
| # 8. LOGIKA RUNNER ZERO-OVERHEAD @SPACES.GPU (DYNAMIC DURATION) | |
| # ============================================================ | |
| DURATION_GPU_MAP = { | |
| 5.0: 55, # Waktu riil ~35s -> Minta 55s (ZeroGPU reserve: 82.5s) | |
| 10.0: 75, # Waktu riil ~55s -> Minta 75s (ZeroGPU reserve: 112.5s) | |
| 15.0: 100, # Waktu riil ~74.6s -> Minta 100s (ZeroGPU reserve: 150.0s) | |
| } | |
| def get_generator_duration(safetensors_path: str, video_duration: float | str = 5.0, seed: int = 0) -> int: | |
| """Kalkulasi alokasi waktu GPU ZeroGPU dinamis dan aman sesuai opsi durasi diskrit.""" | |
| try: | |
| dur_val = float(str(video_duration).replace("s", "").strip()) | |
| except Exception: | |
| dur_val = 5.0 | |
| return DURATION_GPU_MAP.get(dur_val, int(min(max(50, 25 + dur_val * 4.8), 105))) | |
| def _load_base_workflow() -> dict[str, Any]: | |
| with open(WORKFLOW_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| def _run_comfy_generator_workflow(workflow: dict[str, Any]) -> str: | |
| """Eksekusi workflow generator ComfyUI menggunakan persistent PromptExecutor dengan Profiler.""" | |
| import execution | |
| executor = _get_or_create_executor() | |
| prompt_id = str(uuid.uuid4()) | |
| node_durations: list[tuple[str, str, float]] = [] | |
| orig_get_output_data = execution.get_output_data | |
| def _profiling_get_output_data(obj, input_data_all, *args, **kwargs): | |
| if isinstance(obj, str): | |
| node_id = obj | |
| node_info = workflow.get(node_id, {}) | |
| class_type = node_info.get("class_type", "UnknownNode") | |
| node_title = node_info.get("_meta", {}).get("title", class_type) | |
| label = f"[Node {node_id}: {node_title}]" | |
| else: | |
| node_id = "?" | |
| class_type = obj.__class__.__name__ | |
| label = f"[{class_type}]" | |
| t0 = time.time() | |
| print(f"🚀 {label} Mulai dieksekusi...", flush=True) | |
| try: | |
| res = orig_get_output_data(obj, input_data_all, *args, **kwargs) | |
| dur = time.time() - t0 | |
| node_durations.append((node_id, label, dur)) | |
| print(f"⏱️ {label} Selesai dalam: {dur:.2f}s", flush=True) | |
| return res | |
| except Exception as e: | |
| dur = time.time() - t0 | |
| print(f"❌ {label} Gagal setelah: {dur:.2f}s ({e})", flush=True) | |
| raise | |
| execution.get_output_data = _profiling_get_output_data | |
| t_workflow_start = time.time() | |
| try: | |
| executor.execute( | |
| workflow, | |
| prompt_id, | |
| extra_data={}, | |
| execute_outputs=[NODE_OUTPUT_ID], | |
| ) | |
| finally: | |
| execution.get_output_data = orig_get_output_data | |
| t_workflow_total = time.time() - t_workflow_start | |
| print("\n" + "=" * 70, flush=True) | |
| print("📊 REKAPITULASI PROFILING WAKTU EKSEKUSI SPACE 2 (PER NODE):", flush=True) | |
| print("=" * 70, flush=True) | |
| sorted_nodes = sorted(node_durations, key=lambda x: x[2], reverse=True) | |
| for nid, label, dur in sorted_nodes: | |
| pct = (dur / t_workflow_total * 100) if t_workflow_total > 0 else 0 | |
| bar = "█" * int(pct // 5) | |
| print(f" {label:<45} : {dur:>6.2f}s ({pct:>5.1f}%) {bar}", flush=True) | |
| print("-" * 70, flush=True) | |
| print(f" ⏱️ TOTAL DURASI SAMPLING & DECODE : {t_workflow_total:.2f} detik", flush=True) | |
| print("=" * 70 + "\n", flush=True) | |
| if not executor.success: | |
| err = ( | |
| executor.status_messages[-1] | |
| if hasattr(executor, "status_messages") and executor.status_messages | |
| else "ComfyUI execution gagal" | |
| ) | |
| raise RuntimeError(str(err)) | |
| files = [ | |
| pathlib.Path(p) | |
| for p in glob.glob(str(OUTPUT / "**" / "*.mp4"), recursive=True) | |
| ] | |
| if not files: | |
| files = [ | |
| pathlib.Path(p) | |
| for p in glob.glob(str(OUTPUT / "**" / "*.*"), recursive=True) | |
| if p.endswith((".mp4", ".webm", ".mkv", ".mov")) | |
| ] | |
| if not files: | |
| raise RuntimeError("Generation selesai tetapi file video output (.mp4) tidak ditemukan di folder output.") | |
| latest_video = sorted(files, key=lambda p: p.stat().st_mtime, reverse=True)[0] | |
| return str(latest_video) | |
| def run_generator_gpu( | |
| safetensors_path: str, | |
| video_duration: float | str = 5.0, | |
| seed: int = 0, | |
| ) -> str: | |
| """Eksekusi murni GPU forward: Denoising TaoMate 3-Step LoRA + Video & Audio VAE Decode.""" | |
| wf = _load_base_workflow() | |
| try: | |
| dur_val = float(str(video_duration).replace("s", "").strip()) | |
| except Exception: | |
| dur_val = 5.0 | |
| prefix = f"H3_vid_{uuid.uuid4().hex[:8]}" | |
| wf["ext_h3_cond"]["inputs"]["safetensors_path"] = safetensors_path | |
| wf["105_15"]["inputs"]["noise_seed"] = int(seed) | |
| wf["105_9"]["inputs"]["steps"] = 3 # Kunci standar TaoMate 3-Step LoRA | |
| wf["92"]["inputs"]["filename_prefix"] = f"video/{prefix}" | |
| print(f"[*] [GPU Space 2] Menjalankan MiniMax-H3 Denoising (TaoMate 3-Step, Durasi={dur_val}s, Seed={seed})...", flush=True) | |
| target_video = _run_comfy_generator_workflow(wf) | |
| print(f"[*] [GPU Selesai] Video MP4 berhasil dibuat: {target_video}", flush=True) | |
| return target_video | |
| # ============================================================ | |
| # 9. PIPELINE LENGKAP DUAL-SPACE (I2V) | |
| # ============================================================ | |
| def generate_video_pipeline( | |
| first_frame: str | None, | |
| last_frame: str | None, | |
| prompt: str, | |
| duration_choice: str, | |
| seed: float, | |
| randomize_seed: bool, | |
| request: gr.Request = None, | |
| progress=gr.Progress(track_tqdm=True), | |
| ) -> tuple[str | None, str, int]: | |
| if not first_frame or not os.path.exists(first_frame): | |
| raise gr.Error("First Frame (gambar keyframe awal) wajib diunggah untuk mode Image-to-Video!") | |
| seed_int = int(seed) | |
| if randomize_seed or seed_int == 0: | |
| seed_int = random.randint(1, 1000000000000000) | |
| try: | |
| dur_val = float(str(duration_choice).replace("s", "").strip()) | |
| except Exception: | |
| dur_val = 5.0 | |
| # Ekstraksi ZeroGPU token | |
| ip_token = None | |
| if request and hasattr(request, "headers"): | |
| ip_token = request.headers.get("x-ip-token") | |
| t_total_start = time.time() | |
| # Step 1: Conditioning via Space 1 (Thin Wire) | |
| progress(0.1, desc="⚡ [Space 1] Menghubungi Qwen3-VL Conditioner & Keyframes Encoder...") | |
| t_cond_start = time.time() | |
| try: | |
| safetensors_file = fetch_remote_conditioning( | |
| prompt=prompt or "", | |
| first_frame_path=first_frame, | |
| last_frame_path=last_frame, | |
| duration=dur_val, | |
| ip_token=ip_token, | |
| ) | |
| except Exception as e: | |
| raise gr.Error(f"Gagal memproses conditioning di Space 1 ({CONDITIONER_SPACE}): {e}") | |
| t_cond_elapsed = time.time() - t_cond_start | |
| # Step 2: Denoising & Video Decode via Space 2 GPU | |
| progress(0.4, desc=f"🎬 [Space 2] Denoising TaoMate 3-Step & Decoding Video ({dur_val:.0f}s)...") | |
| t_gen_start = time.time() | |
| try: | |
| video_path = run_generator_gpu( | |
| safetensors_path=safetensors_file, | |
| video_duration=dur_val, | |
| seed=seed_int, | |
| ) | |
| except Exception as e: | |
| raise gr.Error(f"Gagal saat proses denoising/video generation: {e}") | |
| t_gen_elapsed = time.time() - t_gen_start | |
| t_total_elapsed = time.time() - t_total_start | |
| report = ( | |
| f"✅ Video Berhasil Dibuat!\n" | |
| f"⏱️ Total Waktu: {t_total_elapsed:.2f}s | " | |
| f"🧠 Space 1 (Conditioner): {t_cond_elapsed:.2f}s | " | |
| f"🎬 Space 2 (Denoise & Decode): {t_gen_elapsed:.2f}s\n" | |
| f"⚙️ Resolusi: Otomatis (0.4 MP) | Durasi: {dur_val:.0f}s | Steps: 3 (TaoMate LoRA) | Seed: {seed_int}" | |
| ) | |
| return video_path, report, seed_int | |
| # ============================================================ | |
| # 9.1 FUNGSI DEBUG SYSTEM INFO (DI DALAM GPU) | |
| # ============================================================ | |
| def get_system_info(*args, **kwargs) -> str: | |
| import torch | |
| import sys | |
| lines = [] | |
| lines.append("### 💻 Informasi Sistem & Lingkungan GPU:") | |
| lines.append(f"- **PyTorch Version:** `{torch.__version__}`") | |
| lines.append(f"- **CUDA Available:** `{torch.cuda.is_available()}`") | |
| lines.append(f"- **CUDA Version:** `{torch.version.cuda}`") | |
| if torch.cuda.is_available(): | |
| try: | |
| device_name = torch.cuda.get_device_name(0) | |
| props = torch.cuda.get_device_properties(0) | |
| vram_gb = round(props.total_memory / (1024**3), 2) | |
| lines.append(f"- **Device Name:** `{device_name}`") | |
| lines.append(f"- **VRAM (GPU Memory):** `{vram_gb} GB`") | |
| lines.append(f"- **Compute Capability:** `{props.major}.{props.minor}`") | |
| lines.append(f"- **cuDNN Version:** `{torch.backends.cudnn.version()}`") | |
| except Exception as e: | |
| lines.append(f"- **GPU Query Error:** `{e}`") | |
| else: | |
| lines.append("- **Device:** `CPU / Emulated`") | |
| lines.append(f"- **Python Version:** `{sys.version.split()[0]}`") | |
| lines.append("- **Model UNet:** `minimax_h3_fl2va_pruned_int8_convrot` (~21 GB)") | |
| lines.append("- **Model LoRA:** `minimax_h3_taomate_3step_lora_avg_rank_19_bf16` (~1.96 GB)") | |
| lines.append("- **Model Video VAE:** `minimax_h3_video_vae_int8_convrot` (~2.6 GB)") | |
| lines.append("- **Model Audio VAE:** `minimax_h3_audio_vae_fp32` (~605 MB)") | |
| lines.append("- **Resolution Default:** `0.4 MP (Auto Ratio)`") | |
| lines.append("- **Sampling Steps:** `3 Steps (TaoMate Fixed)`") | |
| lines.append("- **Dynamic Duration:** `45s (5s) | 65s (10s) | 85s (15s)`") | |
| lines.append("- **External Custom Nodes:** `0 (ComfyUI Core Native)`") | |
| return "\n".join(lines) | |
| # ============================================================ | |
| # 10. ANTARMUKA GRADIO STUDIO MINIMALIS & ELEGAN | |
| # ============================================================ | |
| custom_css = """ | |
| .container { max-width: 1200px; margin: auto; } | |
| .generate-btn { font-size: 1.15rem !important; padding: 12px !important; font-weight: bold !important; } | |
| """ | |
| with gr.Blocks(title="MiniMax-H3 Studio — Dual-Space ComfyUI", css=custom_css, theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| """ | |
| # ⚡ MiniMax-H3 FL2VA — Dual-Space Video Studio (TaoMate 3-Step) | |
| Aplikasi Video Generative canggih multimodal berbasis **MiniMax-H3 (FL2VA)** dengan arsitektur **Dual-Space ZeroGPU**. | |
| - **Space 1 (`Conditioner`)**: Memproses Qwen3-VL 32B + Video VAE Keyframes via Thin Wire Protocol. | |
| - **Space 2 (`Generator Hub`)**: Melakukan Denoising INT8 FL2VA + TaoMate 3-Step LoRA + Decode Video & Audio. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=5): | |
| with gr.Row(): | |
| first_frame_input = gr.Image(type="filepath", label="First Frame (Keyframe Awal - Wajib)") | |
| last_frame_input = gr.Image(type="filepath", label="Last Frame (Keyframe Akhir - Opsional)") | |
| prompt_input = gr.Textbox( | |
| label="Prompt Teks (Opsional)", | |
| placeholder="Deskripsikan aksi atau suasana video sinematik...", | |
| lines=2, | |
| value="cinematic camera movement, natural motion, hyper-detailed", | |
| ) | |
| with gr.Group(): | |
| duration_input = gr.Radio( | |
| choices=["5s", "10s", "15s"], | |
| value="5s", | |
| label="Durasi Video", | |
| ) | |
| with gr.Row(): | |
| seed_input = gr.Number(value=0, label="Seed (0 = Random)", precision=0) | |
| randomize_seed_input = gr.Checkbox(label="🎲 Randomize Seed Setiap Generate", value=True) | |
| btn_generate = gr.Button("🚀 Generate Video (Dual-Space I2V)", variant="primary", elem_classes=["generate-btn"]) | |
| with gr.Column(scale=5): | |
| video_output = gr.Video(label="Hasil Video MiniMax-H3 (dengan Audio)", autoplay=True) | |
| report_output = gr.Markdown(label="Status & Benchmark Log") | |
| with gr.Accordion("🛠️ Debug Info Sistem (PyTorch & CUDA)", open=False): | |
| debug_btn = gr.Button("🔍 Cek Status Model & Hardware GPU", size="sm") | |
| debug_info_md = gr.Markdown() | |
| debug_btn.click(fn=get_system_info, inputs=[], outputs=[debug_info_md]) | |
| btn_generate.click( | |
| fn=generate_video_pipeline, | |
| inputs=[ | |
| first_frame_input, | |
| last_frame_input, | |
| prompt_input, | |
| duration_input, | |
| seed_input, | |
| randomize_seed_input, | |
| ], | |
| outputs=[video_output, report_output, seed_input], | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=20).launch(show_error=True) | |