MarkTechPost(RSS)
精选
72AI 编辑部评分,满分 100

用 ComfyUI API 实现 MiniMax-H3 多模态视频与音频生成流水线

2026-08-11 13:44· 13分钟前· Sana Hassan
AI 导读

本教程演示如何以 ComfyUI 为无头推理后端,构建端到端的 MiniMax-H3 视频生成工作流。通过 Python 直接构建执行图,支持文生视频、首尾帧条件生成和参考图像条件生成,并自动根据 GPU 显存选择 quality、balanced、squeeze 三种权重配置。流水线涵盖模型自动下载、节点模式校验、音视频联合解码与进度监控,无需图形界面即可复现实验。

推荐理由

用 Python 编程替代 ComfyUI 图形界面,在 Colab 上搭建 MiniMax-H3 视频生成管道,并自动适配不同 GPU 显存方案,教程提供的全流程代码可直接复用。

正文 · AI 翻译

在本教程中,我们使用 ComfyUI 作为无头推理后端,实现了一个端到端的 MiniMax-H3 视频生成工作流。我们围绕 GPU 内存、磁盘容量、模型精度、分辨率、时长、采样策略以及多种生成模式来配置环境,同时根据可用硬件动态选择合适的权重配置档。我们以编程方式安装并启动 ComfyUI,从 Hugging Face 下载所需的扩散模型、文本编码器、视频 VAE 和音频 VAE 权重,并通过其 HTTP 和 WebSocket API 与运行中的服务器通信。我们还直接在 Python 中构建 ComfyUI 执行图,对照实时的 /object_info 端点验证节点模式,并支持文生视频、首帧和末帧条件生成,以及参考图像条件生成。通过结合自动化模型设置、模式感知的图构建、视频-音频联合解码、进度监控和输出收集,我们创建了一个可复现的流水线,用于在不依赖图形化 ComfyUI 界面的情况下实验 MiniMax-H3。

import json, os, re, shutil, subprocess, sys, time, uuid, urllib.request, urllib.error
from pathlib import Path
CFG = {
  
   "MODE": "t2v",                
   "PROMPT": (
       "Realistic live-action cinematic look. A lone lighthouse keeper on a storm-lashed "
       "cliff at dusk, anamorphic lens, shallow depth of field, film grain, volumetric sea spray.\n"
       "[0s-2s] Wide shot: waves detonate against black rock, the lighthouse beam sweeps the frame.\n"
       "[2s-4s] Medium shot: the keeper braces against the wind, coat snapping, rain on his face.\n"
       "[4s-5s] Close up: he squints into the dark and says \"She's holding.\"\n"
       "Camera: hard cuts between shots, slight handheld jitter, no dissolves.\n"
       "Audio: roaring surf and howling wind throughout, low cello drone underneath, "
       "a heavy wave impact on each cut, the line delivered clearly over the storm.\n"
       "No text, subtitles, logos or watermarks."
   ),
   "ASPECT": (16, 9),            
   "MEGAPIXELS": 0.4,            
   "SECONDS": 5.0,               
   "SEED": 556589502035082,
   "STEPS": 20,                  
   "SAMPLER": "res_multistep",   
   "SCHEDULER": "simple",
  
   "FIRST_FRAME": None,          
   "LAST_FRAME": None,           
   "REF_IMAGES": [],             
   "REF_IMAGE_SIZE": "match",    
  
   "SIGMA_SHIFT": None,          
   "TURBO_LORA": False,          
   "TURBO_STEPS": 8,
   "TURBO_SAMPLER": "euler",
   "TURBO_SCHEDULER": "beta",
  
   "COMFY_DIR": "/content/ComfyUI",
   "OUT_DIR": "/content/outputs",
   "MODELS_ROOT": "/content/models",  
   "PORT": 8188,
   "HF_TOKEN": os.environ.get("HF_TOKEN", ""), 
   "SKIP_INSTALL": False,             
}
REPO = "Comfy-Org/MiniMax-H3"
API = f"http://127.0.0.1:{CFG['PORT']}"
PROFILES = [
   dict(name="quality", min_vram=70,
        unet_fl="minimax_h3_fl2va_bf16.safetensors",           
        unet_ref="minimax_h3_ref2va_bf16.safetensors",
        te="qwen3vl_32b_minimax_h3_int8_convrot.safetensors",  
        flags=["--normalvram"]),
   dict(name="balanced", min_vram=38,
        unet_fl="minimax_h3_fl2va_pruned_int8_convrot.safetensors",  
        unet_ref="minimax_h3_ref2va_pruned_int8_convrot.safetensors",
        te="qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",           
        flags=["--normalvram", "--cache-none"]),
   dict(name="squeeze", min_vram=20,
        unet_fl="minimax_h3_fl2va_pruned_fp8_scaled.safetensors",    
        unet_ref="minimax_h3_ref2va_pruned_fp8_scaled.safetensors",
        te="qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
        flags=["--lowvram", "--cache-none", "--disable-smart-memory"]),
]
VAE_VIDEO = "minimax_h3_video_vae_fp16.safetensors"
VAE_AUDIO = "minimax_h3_audio_vae_fp32.safetensors"
def sh(cmd, cwd=None, check=True, quiet=False):
   """Run a shell command, streaming output."""
   print(f"$ {cmd}")
   p = subprocess.run(cmd, shell=True, cwd=cwd,
                      stdout=subprocess.DEVNULL if quiet else None,
                      stderr=subprocess.STDOUT if quiet else None)
   if check and p.returncode != 0:
       raise RuntimeError(f"command failed ({p.returncode}): {cmd}")
def get_json(path, payload=None, timeout=30):
   url = f"{API}{path}"
   data = json.dumps(payload).encode() if payload is not None else None
   req = urllib.request.Request(url, data=data,
                                headers={"Content-Type": "application/json"})
   with urllib.request.urlopen(req, timeout=timeout) as r:
       body = r.read()
   return json.loads(body) if body else {}
def align_frames(seconds, fps=24):
   """H3 consumes frame counts on the 17k+5 grid. Snap upward."""
   n = max(5, int(round(seconds * fps)))
   while n % 17 != 5:
       n += 1
   return n
def h3_canvas(aspect=(16, 9), megapixels=0.98, multiple=32):
   """Mirror of ComfyUI's ResolutionSelector + H3's 768*1344 area cap."""
   ar = aspect[0] / aspect[1]
   total = megapixels * 1e6
   h = (total / ar) ** 0.5
   w = ar * h
   cap = 768 * 1344
   if w * h > cap:
       s = (cap / (w * h)) ** 0.5
       w, h = w * s, h * s
   r = lambda v: max(multiple, int(round(v / multiple)) * multiple)
   return r(w), r(h)
def preflight():
   try:
       import torch
   except ImportError:
       raise SystemExit("PyTorch missing — run this in a Colab GPU runtime.")
   if not torch.cuda.is_available():
       raise SystemExit("No CUDA device. Runtime > Change runtime type > GPU (A100).")
   name = torch.cuda.get_device_name(0)
   vram = torch.cuda.get_device_properties(0).total_memory / 1e9
   free_disk = shutil.disk_usage("/content").free / 1e9
   bf16 = torch.cuda.is_bf16_supported()
   print(f"GPU        : {name}  ({vram:.1f} GB VRAM, bf16={bf16})")
   print(f"Free disk  : {free_disk:.1f} GB")
   if not bf16:
       raise SystemExit(
           "This GPU has no bf16 support (T4/K80). MiniMax-H3 will not run here.\n"
           "Switch to an A100/L4/H100 runtime."
       )
   profile = next((p for p in PROFILES if vram >= p["min_vram"]), None)
   if profile is None:
       raise SystemExit(
           f"{vram:.0f} GB VRAM is below the ~20 GB floor for the smallest H3 build."
       )
   if free_disk < 45:
       print("WARNING: <45 GB free. Point MODELS_ROOT at Drive or expect a disk-full error.")
   print(f"Profile    : {profile['name']}  (unet={profile['unet_fl']}, te={profile['te']})")
   return profile

我们定义了核心的 MiniMax-H3 配置、模型档位、生成参数,以及整个工作流中使用的共享工具函数。我们在推理开始前计算有效的帧数和画布尺寸,同时检查 GPU 能力、可用显存、BF16 支持和磁盘空间。我们还会自动选择最合适的模型档位,使流水线与 Colab 运行时中可用的硬件相匹配。

def install_comfy():
   comfy = Path(CFG["COMFY_DIR"])
   if CFG["SKIP_INSTALL"] and comfy.exists():
       print("Skipping install (SKIP_INSTALL=True).")
       return
   sh("pip install -q -U 'huggingface_hub[hf_xet]' hf_transfer websocket-client")
   if not comfy.exists():
       sh(f"git clone --depth 1 https://github.com/comfyanonymous/ComfyUI {comfy}")
  
   sh(f"pip install -q -r {comfy}/requirements.txt")
   ver = (comfy / "comfyui_version.py")
   if ver.exists():
       print("ComfyUI:", ver.read_text().strip())
   if not (comfy / "comfy_extras" / "nodes_minimax_h3.py").exists():
       raise SystemExit("This ComfyUI checkout lacks native MiniMax-H3 nodes — update it.")
  
   root = Path(CFG["MODELS_ROOT"])
   for sub in ("diffusion_models", "text_encoders", "vae", "loras"):
       (root / sub).mkdir(parents=True, exist_ok=True)
   (comfy / "extra_model_paths.yaml").write_text(
       "minimax_h3:\n"
       f"    base_path: {root}\n"
       "    diffusion_models: diffusion_models\n"
       "    text_encoders: text_encoders\n"
       "    vae: vae\n"
       "    loras: loras\n"
   )
   Path(CFG["OUT_DIR"]).mkdir(parents=True, exist_ok=True)
def fetch(repo_id, filename, subdir):
   from huggingface_hub import hf_hub_download
   os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
   dest = Path(CFG["MODELS_ROOT"]) / subdir
   target = dest / Path(filename).name
   if target.exists() and target.stat().st_size > 1_000_000:
       print(f"cached  {target.name} ({target.stat().st_size/1e9:.1f} GB)")
       return target
   print(f"pulling {filename} -> {dest}")
   try:
       p = hf_hub_download(repo_id=repo_id, filename=filename, local_dir=str(dest),
                           token=CFG["HF_TOKEN"] or None)
   except Exception as e:
       if "401" in str(e) or "403" in str(e) or "gated" in str(e).lower():
           raise SystemExit(
               f"Access denied for {repo_id}. Accept the MiniMax-H3 community license on the "
               "model page, create a read token, then set CFG['HF_TOKEN']."
           ) from e
       raise
  
   p = Path(p)
   if p != target:
       target.parent.mkdir(parents=True, exist_ok=True)
       shutil.move(str(p), str(target))
   return target
def download_weights(profile, mode):
   unet = profile["unet_ref"] if mode == "r2v" else profile["unet_fl"]
   fetch(REPO, f"diffusion_models/{unet}", "diffusion_models")
   fetch(REPO, f"text_encoders/{profile['te']}", "text_encoders")
   fetch(REPO, f"vae/{VAE_VIDEO}", "vae")
   fetch(REPO, f"vae/{VAE_AUDIO}", "vae")
   lora = None
   if CFG["TURBO_LORA"]:
      
       from huggingface_hub import HfApi
       lora_repo = "drbaph/MiniMax-H3-Turbo-Lora-ComfyUI"
       files = [f for f in HfApi().list_repo_files(lora_repo)
                if f.endswith(".safetensors") and "pruned" in f]
       if not files:
           files = [f for f in HfApi().list_repo_files(lora_repo) if f.endswith(".safetensors")]
       if files:
           lora = fetch(lora_repo, sorted(files)[-1], "loras").name
           print(f"turbo LoRA: {lora}")
   return unet, profile["te"], lora

我们在 Colab 环境中安装并配置 ComfyUI,准备外部模型目录结构,并启用 MiniMax-H3 支持。我们从 Hugging Face 下载所需的扩散模型、文本编码器、视频 VAE 和音频 VAE 权重,并尽可能复用缓存文件。我们还可以选择性地获取 Turbo LoRA 配置,以便在需要时用部分生成质量换取更快的推理速度。

class ComfyServer:
   def __init__(self, flags):
       self.flags, self.proc, self.log = flags, None, Path("/content/comfyui.log")
   def start(self):
       cmd = [sys.executable, "main.py",
              "--listen", "127.0.0.1", "--port", str(CFG["PORT"]),
              "--disable-auto-launch", "--preview-method", "none",
              "--output-directory", CFG["OUT_DIR"]] + self.flags
       print("$", " ".join(cmd))
       f = open(self.log, "wb")
       self.proc = subprocess.Popen(cmd, cwd=CFG["COMFY_DIR"], stdout=f, stderr=subprocess.STDOUT)
       deadline = time.time() + 300
       while time.time() < deadline:
           if self.proc.poll() is not None:
               print(self.log.read_text()[-4000:])
               raise SystemExit("ComfyUI died during startup (log above).")
           try:
               stats = get_json("/system_stats", timeout=3)
               dev = stats.get("devices", [{}])[0]
               print(f"server up — {dev.get('name','?')} "
                     f"{dev.get('vram_total',0)/1e9:.1f} GB total, "
                     f"{dev.get('vram_free',0)/1e9:.1f} GB free")
               return
           except Exception:
               time.sleep(2)
       raise SystemExit("Server did not become ready in 300s. Check /content/comfyui.log")
   def tail(self, n=3000):
       return self.log.read_text()[-n:] if self.log.exists() else ""
   def free_vram(self):
       try:
           get_json("/free", {"unload_models": True, "free_memory": True})
       except Exception:
           pass
   def stop(self):
       if self.proc and self.proc.poll() is None:
           self.proc.terminate()
           try:
               self.proc.wait(30)
           except subprocess.TimeoutExpired:
               self.proc.kill()
class Schema:
   """Reads /object_info so the graph is validated against the *running* node set
   instead of whatever the docs said last week."""
   def __init__(self):
       self.info = get_json("/object_info", timeout=120)
   def require(self, *classes):
       missing = [c for c in classes if c not in self.info]
       if missing:
           raise SystemExit(f"Missing node classes: {missing}. Update ComfyUI to >= 0.30.0.")
   def inputs_of(self, cls):
       spec = self.info[cls]["input"]
       return list(spec.get("required", {})) + list(spec.get("optional", {}))
   def check(self, cls, payload):
       known = set(self.inputs_of(cls))
       unknown = [k for k in payload if k not in known]
       if unknown:
           print(f"  note: {cls} does not declare {unknown} — declared: {sorted(known)}")
   def autogrow(self, cls, prefix, n):
       """Autogrow slots (ref_image_1, ref_video_1, ...) are dynamic; discover the
       real names if the server exposes them, otherwise fall back to 1-based."""
       found = sorted([k for k in self.inputs_of(cls) if k.startswith(prefix)])
       if len(found) >= n:
           return found[:n]
       return [f"{prefix}{i+1}" for i in range(n)]

我们构建了一个服务器管理层,将 ComfyUI 作为后台子进程启动,并通过其 API 验证其可用性。我们监控服务器启动过程、检查 GPU 内存统计信息、在必要时释放显存,并在执行完毕后安全地终止服务器。我们还构建了一个模式检查工具,用于读取实时的 ComfyUI 节点定义,以便验证图输入并动态发现支持的节点插槽。

class H3Graph:
   def __init__(self, schema, unet, te, lora=None):
       self.s, self.g, self._id = schema, {}, 0
       self.unet, self.te, self.lora = unet, te, lora
   def node(self, cls, **inputs):
       self.s.check(cls, inputs)
       self._id += 1
       nid = str(self._id)
       self.g[nid] = {"class_type": cls, "inputs": inputs}
       return nid
  
   def _backbone(self):
       model = self.node("UNETLoader", unet_name=self.unet, weight_dtype="default")
       if self.lora:
           model = self.node("LoraLoaderModelOnly", model=[model, 0],
                             lora_name=self.lora, strength_model=1.0)
       if CFG["SIGMA_SHIFT"]:
           sv, sa = CFG["SIGMA_SHIFT"]
           model = self.node("MiniMaxH3SigmaShift", model=[model, 0],
                             shift_video=float(sv), shift_audio=float(sa))
       clip = self.node("CLIPLoader", clip_name=self.te, type="minimax", device="default")
       vvae = self.node("VAELoader", vae_name=VAE_VIDEO)
       avae = self.node("VAELoader", vae_name=VAE_AUDIO)
       return model, clip, vvae, avae
   def _tail(self, model, cond, latent, vvae, avae):
       turbo = bool(self.lora)
       steps = CFG["TURBO_STEPS"] if turbo else CFG["STEPS"]
       sampler_name = CFG["TURBO_SAMPLER"] if turbo else CFG["SAMPLER"]
       sched = CFG["TURBO_SCHEDULER"] if turbo else CFG["SCHEDULER"]
       noise = self.node("RandomNoise", noise_seed=int(CFG["SEED"]))
       samp = self.node("KSamplerSelect", sampler_name=sampler_name)
       sig = self.node("BasicScheduler", model=[model, 0], scheduler=sched,
                       steps=steps, denoise=1.0)
       guider = self.node("BasicGuider", model=[model, 0], conditioning=[cond[0], cond[1]])
       out = self.node("SamplerCustomAdvanced", noise=[noise, 0], guider=[guider, 0],
                       sampler=[samp, 0], sigmas=[sig, 0], latent_image=[latent[0], latent[1]])
      
       frames = self.node("VAEDecode", samples=[out, 0], vae=[vvae, 0])
       audio = self.node("VAEDecodeAudio", samples=[out, 0], vae=[avae, 0])
       vid = self.node("CreateVideo", images=[frames, 0], audio=[audio, 0], fps=24)
       self.node("SaveVideo", video=[vid, 0], filename_prefix="MiniMaxH3/h3",
                 format="auto", codec="auto")
       print(f"  sampling: {steps} steps, {sampler_name}/{sched}")
       return self.g
   def _load_image(self, uploaded_name):
       return self.node("LoadImage", image=uploaded_name, upload="image")
  
   def t2v_or_flf2v(self, w, h, length, first=None, last=None):
       self.s.require("MiniMaxH3ImageToVideo", "SamplerCustomAdvanced", "SaveVideo")
       model, clip, vvae, avae = self._backbone()
       kw = {}
       if first:
           kw["first_frame"] = [self._load_image(first), 0]  
       if last:
           kw["last_frame"] = [self._load_image(last), 0]    
       n = self.node("MiniMaxH3ImageToVideo", clip=[clip, 0], vae=[vvae, 0],
                     prompt=CFG["PROMPT"], width=w, height=h, length=length, **kw)
       return self._tail(model, (n, 0), (n, 1), vvae, avae)
   def r2v(self, w, h, length, ref_names):
       self.s.require("MiniMaxH3ReferenceToVideo")
       model, clip, vvae, avae = self._backbone()
       slots = self.s.autogrow("MiniMaxH3ReferenceToVideo", "ref_image_", len(ref_names))
       refs = {slot: [self._load_image(nm), 0] for slot, nm in zip(slots, ref_names)}
       print(f"  reference slots: {list(refs)}")
       n = self.node("MiniMaxH3ReferenceToVideo", clip=[clip, 0], vae=[vvae, 0],
                     audio_vae=[avae, 0], prompt=CFG["PROMPT"], width=w, height=h,
                     length=length, ref_image_size=CFG["REF_IMAGE_SIZE"], **refs)
       return self._tail(model, (n, 0), (n, 1), vvae, avae)

我们完全使用 Python 通过可复用的节点构建方法来构建 MiniMax-H3 ComfyUI 工作流图。我们为标准和 Turbo 两种配置组装了模型主干、条件处理流水线、采样器、调度器、联合潜空间解码、视频创建和输出保存等阶段。我们还通过相同的可编程图架构支持文本生成视频、首帧和末帧条件视频,以及参考图像条件视频生成。

def upload_image(path):
   """Multipart POST to /upload/image; returns the name LoadImage expects."""
   path = Path(path)
   if not path.exists():
       raise FileNotFoundError(path)
   boundary = uuid.uuid4().hex
   body = (
       f"--{boundary}\r\nContent-Disposition: form-data; name=\"image\"; "
       f"filename=\"{path.name}\"\r\nContent-Type: application/octet-stream\r\n\r\n"
   ).encode() + path.read_bytes() + (
       f"\r\n--{boundary}\r\nContent-Disposition: form-data; name=\"overwrite\"\r\n\r\ntrue"
       f"\r\n--{boundary}--\r\n"
   ).encode()
   req = urllib.request.Request(f"{API}/upload/image", data=body,
                                headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
   with urllib.request.urlopen(req, timeout=120) as r:
       info = json.loads(r.read())
   sub = info.get("subfolder") or ""
   print(f"  uploaded {path.name}")
   return f"{sub}/{info['name']}" if sub else info["name"]
def run_graph(graph, server, timeout=7200):
   """Submit, then follow the WebSocket for per-step progress."""
   import websocket
   cid = uuid.uuid4().hex
   Path("/content/last_workflow_api.json").write_text(json.dumps(graph, indent=2))
   try:
       res = get_json("/prompt", {"prompt": graph, "client_id": cid})
   except urllib.error.HTTPError as e:
       detail = e.read().decode()[:3000]
       raise SystemExit(f"Graph rejected by ComfyUI:\n{detail}")
   pid = res["prompt_id"]
   print(f"queued {pid} — first run loads ~37 GB of weights, be patient")
   ws = websocket.WebSocket()
   ws.connect(f"ws://127.0.0.1:{CFG['PORT']}/ws?clientId={cid}", timeout=60)
   t0, last = time.time(), ""
   try:
       while time.time() - t0 < timeout:
           try:
               msg = ws.recv()
           except Exception:
               time.sleep(1)
               continue
           if isinstance(msg, bytes):
               continue
           d = json.loads(msg)
           t, data = d.get("type"), d.get("data", {})
           if t == "executing" and data.get("prompt_id") == pid:
               if data.get("node") is None:
                   print(f"\ndone in {time.time()-t0:.0f}s")
                   break
               cls = graph.get(data["node"], {}).get("class_type", data["node"])
               if cls != last:
                   print(f"\n  -> {cls}", end="", flush=True)
                   last = cls
           elif t == "progress":
               v, m = data.get("value", 0), data.get("max", 1)
               print(f"\r  -> {last}  {v}/{m}   ", end="", flush=True)
           elif t == "execution_error":
               print("\n--- execution error ---")
               print(json.dumps(data, indent=2)[:4000])
               print(server.tail())
               raise SystemExit("Generation failed.")
   finally:
       ws.close()
  
   files = []
   try:
       hist = get_json(f"/history/{pid}")
       for out in hist.get(pid, {}).get("outputs", {}).values():
           for items in out.values():
               if isinstance(items, list):
                   for it in items:
                       if isinstance(it, dict) and "filename" in it:
                           p = Path(CFG["OUT_DIR"]) / (it.get("subfolder") or "") / it["filename"]
                           if p.exists():
                               files.append(p)
   except Exception:
       pass
   if not files:
       cands = [p for p in Path(CFG["OUT_DIR"]).rglob("*")
                if p.suffix.lower() in (".mp4", ".webm", ".mkv") and p.stat().st_mtime > t0]
       files = sorted(cands, key=lambda p: p.stat().st_mtime)
   return files
def main():
   profile = preflight()
   install_comfy()
   mode = CFG["MODE"]
   unet, te, lora = download_weights(profile, mode)
   w, h = h3_canvas(CFG["ASPECT"], CFG["MEGAPIXELS"])
   length = align_frames(CFG["SECONDS"])
   print(f"\ncanvas {w}x{h}, {length} frames "
         f"({length/24:.2f}s @24fps, grid check {length % 17 == 5})")
   server = ComfyServer(profile["flags"])
   server.start()
   try:
       schema = Schema()
       builder = H3Graph(schema, unet, te, lora)
       if mode == "r2v":
           if not CFG["REF_IMAGES"]:
               raise SystemExit("MODE='r2v' needs CFG['REF_IMAGES'] and <Picture N> tags "
                                "in the prompt.")
           names = [upload_image(p) for p in CFG["REF_IMAGES"][:9]]
           graph = builder.r2v(w, h, length, names)
       else:
           first = upload_image(CFG["FIRST_FRAME"]) if CFG["FIRST_FRAME"] else None
           last = upload_image(CFG["LAST_FRAME"]) if CFG["LAST_FRAME"] else None
           if mode == "flf2v" and not (first or last):
               raise SystemExit("MODE='flf2v' needs FIRST_FRAME and/or LAST_FRAME.")
           graph = builder.t2v_or_flf2v(w, h, length, first, last)
       print(f"graph: {len(graph)} nodes "
             f"({', '.join(sorted({n['class_type'] for n in graph.values()}))})")
       files = run_graph(graph, server)
       server.free_vram()
   finally:
       server.stop()
   if not files:
       print("No output file found. Log tail:\n", server.tail())
       return
   for f in files:
       print(f"\noutput: {f}  ({f.stat().st_size/1e6:.1f} MB)")
   try:
       from IPython.display import Video, display
       vid = files[-1]
       if vid.stat().st_size < 60e6:
           display(Video(str(vid), embed=True, width=720))
       else:
           print("Too large to embed — use files.download() or check /content/outputs")
   except Exception:
       pass
main()

我们处理图像上传、图提交、WebSocket 进度跟踪、输出发现以及教程的完整执行流程。我们将生成的图提交给 ComfyUI,监控各个节点的执行和采样进度,收集生成的视频文件,并直接在 Colab 中展示可管理的输出。最后,我们通过主函数协调所有前述组件,使工作流从硬件预检和模型加载,一直到同步的 MiniMax-H3 视频和音频生成。

总而言之,我们实现了一条完整的、可编程的 MiniMax-H3 推理流水线,涵盖从硬件验证与模型获取,到图执行以及最终的同步视频-音频生成。我们将 ComfyUI 用作无头服务器,同时从 Python 控制整个工作流,这让我们能够直接访问配置、模型加载、条件设定、采样、解码、服务器生命周期管理以及生成输出。我们还通过动态检查 ComfyUI 节点模式、根据可用 VRAM 调整模型配置、将帧数与 MiniMax-H3 要求对齐,以及通过同一套可复用架构支持多种条件模式,使该流水线更加稳健。到工作流结束时,我们拥有了一个灵活的基础,可以在此基础上扩展不同的提示词、随机种子、参考图像、帧约束、LoRA 加速、分辨率和采样策略,同时保持一个一致且自动化的 MiniMax-H3 生成流程。


来源:MarkTechPost(RSS) · marktechpost.com

用 ComfyUI API 实现 MiniMax-H3 多模态视频与音频生成流水线

MarkTechPost(RSS)·2026-08-11 13:44·13分钟前·Sana Hassan
AI 导读

本教程演示如何以 ComfyUI 为无头推理后端,构建端到端的 MiniMax-H3 视频生成工作流。通过 Python 直接构建执行图,支持文生视频、首尾帧条件生成和参考图像条件生成,并自动根据 GPU 显存选择 quality、balanced、squeeze 三种权重配置。流水线涵盖模型自动下载、节点模式校验、音视频联合解码与进度监控,无需图形界面即可复现实验。

正文 · AI 翻译

在本教程中,我们使用 ComfyUI 作为无头推理后端,实现了一个端到端的 MiniMax-H3 视频生成工作流。我们围绕 GPU 内存、磁盘容量、模型精度、分辨率、时长、采样策略以及多种生成模式来配置环境,同时根据可用硬件动态选择合适的权重配置档。我们以编程方式安装并启动 ComfyUI,从 Hugging Face 下载所需的扩散模型、文本编码器、视频 VAE 和音频 VAE 权重,并通过其 HTTP 和 WebSocket API 与运行中的服务器通信。我们还直接在 Python 中构建 ComfyUI 执行图,对照实时的 /object_info 端点验证节点模式,并支持文生视频、首帧和末帧条件生成,以及参考图像条件生成。通过结合自动化模型设置、模式感知的图构建、视频-音频联合解码、进度监控和输出收集,我们创建了一个可复现的流水线,用于在不依赖图形化 ComfyUI 界面的情况下实验 MiniMax-H3。

import json, os, re, shutil, subprocess, sys, time, uuid, urllib.request, urllib.error
from pathlib import Path
CFG = {
  
   "MODE": "t2v",                
   "PROMPT": (
       "Realistic live-action cinematic look. A lone lighthouse keeper on a storm-lashed "
       "cliff at dusk, anamorphic lens, shallow depth of field, film grain, volumetric sea spray.\n"
       "[0s-2s] Wide shot: waves detonate against black rock, the lighthouse beam sweeps the frame.\n"
       "[2s-4s] Medium shot: the keeper braces against the wind, coat snapping, rain on his face.\n"
       "[4s-5s] Close up: he squints into the dark and says \"She's holding.\"\n"
       "Camera: hard cuts between shots, slight handheld jitter, no dissolves.\n"
       "Audio: roaring surf and howling wind throughout, low cello drone underneath, "
       "a heavy wave impact on each cut, the line delivered clearly over the storm.\n"
       "No text, subtitles, logos or watermarks."
   ),
   "ASPECT": (16, 9),            
   "MEGAPIXELS": 0.4,            
   "SECONDS": 5.0,               
   "SEED": 556589502035082,
   "STEPS": 20,                  
   "SAMPLER": "res_multistep",   
   "SCHEDULER": "simple",
  
   "FIRST_FRAME": None,          
   "LAST_FRAME": None,           
   "REF_IMAGES": [],             
   "REF_IMAGE_SIZE": "match",    
  
   "SIGMA_SHIFT": None,          
   "TURBO_LORA": False,          
   "TURBO_STEPS": 8,
   "TURBO_SAMPLER": "euler",
   "TURBO_SCHEDULER": "beta",
  
   "COMFY_DIR": "/content/ComfyUI",
   "OUT_DIR": "/content/outputs",
   "MODELS_ROOT": "/content/models",  
   "PORT": 8188,
   "HF_TOKEN": os.environ.get("HF_TOKEN", ""), 
   "SKIP_INSTALL": False,             
}
REPO = "Comfy-Org/MiniMax-H3"
API = f"http://127.0.0.1:{CFG['PORT']}"
PROFILES = [
   dict(name="quality", min_vram=70,
        unet_fl="minimax_h3_fl2va_bf16.safetensors",           
        unet_ref="minimax_h3_ref2va_bf16.safetensors",
        te="qwen3vl_32b_minimax_h3_int8_convrot.safetensors",  
        flags=["--normalvram"]),
   dict(name="balanced", min_vram=38,
        unet_fl="minimax_h3_fl2va_pruned_int8_convrot.safetensors",  
        unet_ref="minimax_h3_ref2va_pruned_int8_convrot.safetensors",
        te="qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",           
        flags=["--normalvram", "--cache-none"]),
   dict(name="squeeze", min_vram=20,
        unet_fl="minimax_h3_fl2va_pruned_fp8_scaled.safetensors",    
        unet_ref="minimax_h3_ref2va_pruned_fp8_scaled.safetensors",
        te="qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
        flags=["--lowvram", "--cache-none", "--disable-smart-memory"]),
]
VAE_VIDEO = "minimax_h3_video_vae_fp16.safetensors"
VAE_AUDIO = "minimax_h3_audio_vae_fp32.safetensors"
def sh(cmd, cwd=None, check=True, quiet=False):
   """Run a shell command, streaming output."""
   print(f"$ {cmd}")
   p = subprocess.run(cmd, shell=True, cwd=cwd,
                      stdout=subprocess.DEVNULL if quiet else None,
                      stderr=subprocess.STDOUT if quiet else None)
   if check and p.returncode != 0:
       raise RuntimeError(f"command failed ({p.returncode}): {cmd}")
def get_json(path, payload=None, timeout=30):
   url = f"{API}{path}"
   data = json.dumps(payload).encode() if payload is not None else None
   req = urllib.request.Request(url, data=data,
                                headers={"Content-Type": "application/json"})
   with urllib.request.urlopen(req, timeout=timeout) as r:
       body = r.read()
   return json.loads(body) if body else {}
def align_frames(seconds, fps=24):
   """H3 consumes frame counts on the 17k+5 grid. Snap upward."""
   n = max(5, int(round(seconds * fps)))
   while n % 17 != 5:
       n += 1
   return n
def h3_canvas(aspect=(16, 9), megapixels=0.98, multiple=32):
   """Mirror of ComfyUI's ResolutionSelector + H3's 768*1344 area cap."""
   ar = aspect[0] / aspect[1]
   total = megapixels * 1e6
   h = (total / ar) ** 0.5
   w = ar * h
   cap = 768 * 1344
   if w * h > cap:
       s = (cap / (w * h)) ** 0.5
       w, h = w * s, h * s
   r = lambda v: max(multiple, int(round(v / multiple)) * multiple)
   return r(w), r(h)
def preflight():
   try:
       import torch
   except ImportError:
       raise SystemExit("PyTorch missing — run this in a Colab GPU runtime.")
   if not torch.cuda.is_available():
       raise SystemExit("No CUDA device. Runtime > Change runtime type > GPU (A100).")
   name = torch.cuda.get_device_name(0)
   vram = torch.cuda.get_device_properties(0).total_memory / 1e9
   free_disk = shutil.disk_usage("/content").free / 1e9
   bf16 = torch.cuda.is_bf16_supported()
   print(f"GPU        : {name}  ({vram:.1f} GB VRAM, bf16={bf16})")
   print(f"Free disk  : {free_disk:.1f} GB")
   if not bf16:
       raise SystemExit(
           "This GPU has no bf16 support (T4/K80). MiniMax-H3 will not run here.\n"
           "Switch to an A100/L4/H100 runtime."
       )
   profile = next((p for p in PROFILES if vram >= p["min_vram"]), None)
   if profile is None:
       raise SystemExit(
           f"{vram:.0f} GB VRAM is below the ~20 GB floor for the smallest H3 build."
       )
   if free_disk < 45:
       print("WARNING: <45 GB free. Point MODELS_ROOT at Drive or expect a disk-full error.")
   print(f"Profile    : {profile['name']}  (unet={profile['unet_fl']}, te={profile['te']})")
   return profile

我们定义了核心的 MiniMax-H3 配置、模型档位、生成参数,以及整个工作流中使用的共享工具函数。我们在推理开始前计算有效的帧数和画布尺寸,同时检查 GPU 能力、可用显存、BF16 支持和磁盘空间。我们还会自动选择最合适的模型档位,使流水线与 Colab 运行时中可用的硬件相匹配。

def install_comfy():
   comfy = Path(CFG["COMFY_DIR"])
   if CFG["SKIP_INSTALL"] and comfy.exists():
       print("Skipping install (SKIP_INSTALL=True).")
       return
   sh("pip install -q -U 'huggingface_hub[hf_xet]' hf_transfer websocket-client")
   if not comfy.exists():
       sh(f"git clone --depth 1 https://github.com/comfyanonymous/ComfyUI {comfy}")
  
   sh(f"pip install -q -r {comfy}/requirements.txt")
   ver = (comfy / "comfyui_version.py")
   if ver.exists():
       print("ComfyUI:", ver.read_text().strip())
   if not (comfy / "comfy_extras" / "nodes_minimax_h3.py").exists():
       raise SystemExit("This ComfyUI checkout lacks native MiniMax-H3 nodes — update it.")
  
   root = Path(CFG["MODELS_ROOT"])
   for sub in ("diffusion_models", "text_encoders", "vae", "loras"):
       (root / sub).mkdir(parents=True, exist_ok=True)
   (comfy / "extra_model_paths.yaml").write_text(
       "minimax_h3:\n"
       f"    base_path: {root}\n"
       "    diffusion_models: diffusion_models\n"
       "    text_encoders: text_encoders\n"
       "    vae: vae\n"
       "    loras: loras\n"
   )
   Path(CFG["OUT_DIR"]).mkdir(parents=True, exist_ok=True)
def fetch(repo_id, filename, subdir):
   from huggingface_hub import hf_hub_download
   os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
   dest = Path(CFG["MODELS_ROOT"]) / subdir
   target = dest / Path(filename).name
   if target.exists() and target.stat().st_size > 1_000_000:
       print(f"cached  {target.name} ({target.stat().st_size/1e9:.1f} GB)")
       return target
   print(f"pulling {filename} -> {dest}")
   try:
       p = hf_hub_download(repo_id=repo_id, filename=filename, local_dir=str(dest),
                           token=CFG["HF_TOKEN"] or None)
   except Exception as e:
       if "401" in str(e) or "403" in str(e) or "gated" in str(e).lower():
           raise SystemExit(
               f"Access denied for {repo_id}. Accept the MiniMax-H3 community license on the "
               "model page, create a read token, then set CFG['HF_TOKEN']."
           ) from e
       raise
  
   p = Path(p)
   if p != target:
       target.parent.mkdir(parents=True, exist_ok=True)
       shutil.move(str(p), str(target))
   return target
def download_weights(profile, mode):
   unet = profile["unet_ref"] if mode == "r2v" else profile["unet_fl"]
   fetch(REPO, f"diffusion_models/{unet}", "diffusion_models")
   fetch(REPO, f"text_encoders/{profile['te']}", "text_encoders")
   fetch(REPO, f"vae/{VAE_VIDEO}", "vae")
   fetch(REPO, f"vae/{VAE_AUDIO}", "vae")
   lora = None
   if CFG["TURBO_LORA"]:
      
       from huggingface_hub import HfApi
       lora_repo = "drbaph/MiniMax-H3-Turbo-Lora-ComfyUI"
       files = [f for f in HfApi().list_repo_files(lora_repo)
                if f.endswith(".safetensors") and "pruned" in f]
       if not files:
           files = [f for f in HfApi().list_repo_files(lora_repo) if f.endswith(".safetensors")]
       if files:
           lora = fetch(lora_repo, sorted(files)[-1], "loras").name
           print(f"turbo LoRA: {lora}")
   return unet, profile["te"], lora

我们在 Colab 环境中安装并配置 ComfyUI,准备外部模型目录结构,并启用 MiniMax-H3 支持。我们从 Hugging Face 下载所需的扩散模型、文本编码器、视频 VAE 和音频 VAE 权重,并尽可能复用缓存文件。我们还可以选择性地获取 Turbo LoRA 配置,以便在需要时用部分生成质量换取更快的推理速度。

class ComfyServer:
   def __init__(self, flags):
       self.flags, self.proc, self.log = flags, None, Path("/content/comfyui.log")
   def start(self):
       cmd = [sys.executable, "main.py",
              "--listen", "127.0.0.1", "--port", str(CFG["PORT"]),
              "--disable-auto-launch", "--preview-method", "none",
              "--output-directory", CFG["OUT_DIR"]] + self.flags
       print("$", " ".join(cmd))
       f = open(self.log, "wb")
       self.proc = subprocess.Popen(cmd, cwd=CFG["COMFY_DIR"], stdout=f, stderr=subprocess.STDOUT)
       deadline = time.time() + 300
       while time.time() < deadline:
           if self.proc.poll() is not None:
               print(self.log.read_text()[-4000:])
               raise SystemExit("ComfyUI died during startup (log above).")
           try:
               stats = get_json("/system_stats", timeout=3)
               dev = stats.get("devices", [{}])[0]
               print(f"server up — {dev.get('name','?')} "
                     f"{dev.get('vram_total',0)/1e9:.1f} GB total, "
                     f"{dev.get('vram_free',0)/1e9:.1f} GB free")
               return
           except Exception:
               time.sleep(2)
       raise SystemExit("Server did not become ready in 300s. Check /content/comfyui.log")
   def tail(self, n=3000):
       return self.log.read_text()[-n:] if self.log.exists() else ""
   def free_vram(self):
       try:
           get_json("/free", {"unload_models": True, "free_memory": True})
       except Exception:
           pass
   def stop(self):
       if self.proc and self.proc.poll() is None:
           self.proc.terminate()
           try:
               self.proc.wait(30)
           except subprocess.TimeoutExpired:
               self.proc.kill()
class Schema:
   """Reads /object_info so the graph is validated against the *running* node set
   instead of whatever the docs said last week."""
   def __init__(self):
       self.info = get_json("/object_info", timeout=120)
   def require(self, *classes):
       missing = [c for c in classes if c not in self.info]
       if missing:
           raise SystemExit(f"Missing node classes: {missing}. Update ComfyUI to >= 0.30.0.")
   def inputs_of(self, cls):
       spec = self.info[cls]["input"]
       return list(spec.get("required", {})) + list(spec.get("optional", {}))
   def check(self, cls, payload):
       known = set(self.inputs_of(cls))
       unknown = [k for k in payload if k not in known]
       if unknown:
           print(f"  note: {cls} does not declare {unknown} — declared: {sorted(known)}")
   def autogrow(self, cls, prefix, n):
       """Autogrow slots (ref_image_1, ref_video_1, ...) are dynamic; discover the
       real names if the server exposes them, otherwise fall back to 1-based."""
       found = sorted([k for k in self.inputs_of(cls) if k.startswith(prefix)])
       if len(found) >= n:
           return found[:n]
       return [f"{prefix}{i+1}" for i in range(n)]

我们构建了一个服务器管理层,将 ComfyUI 作为后台子进程启动,并通过其 API 验证其可用性。我们监控服务器启动过程、检查 GPU 内存统计信息、在必要时释放显存,并在执行完毕后安全地终止服务器。我们还构建了一个模式检查工具,用于读取实时的 ComfyUI 节点定义,以便验证图输入并动态发现支持的节点插槽。

class H3Graph:
   def __init__(self, schema, unet, te, lora=None):
       self.s, self.g, self._id = schema, {}, 0
       self.unet, self.te, self.lora = unet, te, lora
   def node(self, cls, **inputs):
       self.s.check(cls, inputs)
       self._id += 1
       nid = str(self._id)
       self.g[nid] = {"class_type": cls, "inputs": inputs}
       return nid
  
   def _backbone(self):
       model = self.node("UNETLoader", unet_name=self.unet, weight_dtype="default")
       if self.lora:
           model = self.node("LoraLoaderModelOnly", model=[model, 0],
                             lora_name=self.lora, strength_model=1.0)
       if CFG["SIGMA_SHIFT"]:
           sv, sa = CFG["SIGMA_SHIFT"]
           model = self.node("MiniMaxH3SigmaShift", model=[model, 0],
                             shift_video=float(sv), shift_audio=float(sa))
       clip = self.node("CLIPLoader", clip_name=self.te, type="minimax", device="default")
       vvae = self.node("VAELoader", vae_name=VAE_VIDEO)
       avae = self.node("VAELoader", vae_name=VAE_AUDIO)
       return model, clip, vvae, avae
   def _tail(self, model, cond, latent, vvae, avae):
       turbo = bool(self.lora)
       steps = CFG["TURBO_STEPS"] if turbo else CFG["STEPS"]
       sampler_name = CFG["TURBO_SAMPLER"] if turbo else CFG["SAMPLER"]
       sched = CFG["TURBO_SCHEDULER"] if turbo else CFG["SCHEDULER"]
       noise = self.node("RandomNoise", noise_seed=int(CFG["SEED"]))
       samp = self.node("KSamplerSelect", sampler_name=sampler_name)
       sig = self.node("BasicScheduler", model=[model, 0], scheduler=sched,
                       steps=steps, denoise=1.0)
       guider = self.node("BasicGuider", model=[model, 0], conditioning=[cond[0], cond[1]])
       out = self.node("SamplerCustomAdvanced", noise=[noise, 0], guider=[guider, 0],
                       sampler=[samp, 0], sigmas=[sig, 0], latent_image=[latent[0], latent[1]])
      
       frames = self.node("VAEDecode", samples=[out, 0], vae=[vvae, 0])
       audio = self.node("VAEDecodeAudio", samples=[out, 0], vae=[avae, 0])
       vid = self.node("CreateVideo", images=[frames, 0], audio=[audio, 0], fps=24)
       self.node("SaveVideo", video=[vid, 0], filename_prefix="MiniMaxH3/h3",
                 format="auto", codec="auto")
       print(f"  sampling: {steps} steps, {sampler_name}/{sched}")
       return self.g
   def _load_image(self, uploaded_name):
       return self.node("LoadImage", image=uploaded_name, upload="image")
  
   def t2v_or_flf2v(self, w, h, length, first=None, last=None):
       self.s.require("MiniMaxH3ImageToVideo", "SamplerCustomAdvanced", "SaveVideo")
       model, clip, vvae, avae = self._backbone()
       kw = {}
       if first:
           kw["first_frame"] = [self._load_image(first), 0]  
       if last:
           kw["last_frame"] = [self._load_image(last), 0]    
       n = self.node("MiniMaxH3ImageToVideo", clip=[clip, 0], vae=[vvae, 0],
                     prompt=CFG["PROMPT"], width=w, height=h, length=length, **kw)
       return self._tail(model, (n, 0), (n, 1), vvae, avae)
   def r2v(self, w, h, length, ref_names):
       self.s.require("MiniMaxH3ReferenceToVideo")
       model, clip, vvae, avae = self._backbone()
       slots = self.s.autogrow("MiniMaxH3ReferenceToVideo", "ref_image_", len(ref_names))
       refs = {slot: [self._load_image(nm), 0] for slot, nm in zip(slots, ref_names)}
       print(f"  reference slots: {list(refs)}")
       n = self.node("MiniMaxH3ReferenceToVideo", clip=[clip, 0], vae=[vvae, 0],
                     audio_vae=[avae, 0], prompt=CFG["PROMPT"], width=w, height=h,
                     length=length, ref_image_size=CFG["REF_IMAGE_SIZE"], **refs)
       return self._tail(model, (n, 0), (n, 1), vvae, avae)

我们完全使用 Python 通过可复用的节点构建方法来构建 MiniMax-H3 ComfyUI 工作流图。我们为标准和 Turbo 两种配置组装了模型主干、条件处理流水线、采样器、调度器、联合潜空间解码、视频创建和输出保存等阶段。我们还通过相同的可编程图架构支持文本生成视频、首帧和末帧条件视频,以及参考图像条件视频生成。

def upload_image(path):
   """Multipart POST to /upload/image; returns the name LoadImage expects."""
   path = Path(path)
   if not path.exists():
       raise FileNotFoundError(path)
   boundary = uuid.uuid4().hex
   body = (
       f"--{boundary}\r\nContent-Disposition: form-data; name=\"image\"; "
       f"filename=\"{path.name}\"\r\nContent-Type: application/octet-stream\r\n\r\n"
   ).encode() + path.read_bytes() + (
       f"\r\n--{boundary}\r\nContent-Disposition: form-data; name=\"overwrite\"\r\n\r\ntrue"
       f"\r\n--{boundary}--\r\n"
   ).encode()
   req = urllib.request.Request(f"{API}/upload/image", data=body,
                                headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
   with urllib.request.urlopen(req, timeout=120) as r:
       info = json.loads(r.read())
   sub = info.get("subfolder") or ""
   print(f"  uploaded {path.name}")
   return f"{sub}/{info['name']}" if sub else info["name"]
def run_graph(graph, server, timeout=7200):
   """Submit, then follow the WebSocket for per-step progress."""
   import websocket
   cid = uuid.uuid4().hex
   Path("/content/last_workflow_api.json").write_text(json.dumps(graph, indent=2))
   try:
       res = get_json("/prompt", {"prompt": graph, "client_id": cid})
   except urllib.error.HTTPError as e:
       detail = e.read().decode()[:3000]
       raise SystemExit(f"Graph rejected by ComfyUI:\n{detail}")
   pid = res["prompt_id"]
   print(f"queued {pid} — first run loads ~37 GB of weights, be patient")
   ws = websocket.WebSocket()
   ws.connect(f"ws://127.0.0.1:{CFG['PORT']}/ws?clientId={cid}", timeout=60)
   t0, last = time.time(), ""
   try:
       while time.time() - t0 < timeout:
           try:
               msg = ws.recv()
           except Exception:
               time.sleep(1)
               continue
           if isinstance(msg, bytes):
               continue
           d = json.loads(msg)
           t, data = d.get("type"), d.get("data", {})
           if t == "executing" and data.get("prompt_id") == pid:
               if data.get("node") is None:
                   print(f"\ndone in {time.time()-t0:.0f}s")
                   break
               cls = graph.get(data["node"], {}).get("class_type", data["node"])
               if cls != last:
                   print(f"\n  -> {cls}", end="", flush=True)
                   last = cls
           elif t == "progress":
               v, m = data.get("value", 0), data.get("max", 1)
               print(f"\r  -> {last}  {v}/{m}   ", end="", flush=True)
           elif t == "execution_error":
               print("\n--- execution error ---")
               print(json.dumps(data, indent=2)[:4000])
               print(server.tail())
               raise SystemExit("Generation failed.")
   finally:
       ws.close()
  
   files = []
   try:
       hist = get_json(f"/history/{pid}")
       for out in hist.get(pid, {}).get("outputs", {}).values():
           for items in out.values():
               if isinstance(items, list):
                   for it in items:
                       if isinstance(it, dict) and "filename" in it:
                           p = Path(CFG["OUT_DIR"]) / (it.get("subfolder") or "") / it["filename"]
                           if p.exists():
                               files.append(p)
   except Exception:
       pass
   if not files:
       cands = [p for p in Path(CFG["OUT_DIR"]).rglob("*")
                if p.suffix.lower() in (".mp4", ".webm", ".mkv") and p.stat().st_mtime > t0]
       files = sorted(cands, key=lambda p: p.stat().st_mtime)
   return files
def main():
   profile = preflight()
   install_comfy()
   mode = CFG["MODE"]
   unet, te, lora = download_weights(profile, mode)
   w, h = h3_canvas(CFG["ASPECT"], CFG["MEGAPIXELS"])
   length = align_frames(CFG["SECONDS"])
   print(f"\ncanvas {w}x{h}, {length} frames "
         f"({length/24:.2f}s @24fps, grid check {length % 17 == 5})")
   server = ComfyServer(profile["flags"])
   server.start()
   try:
       schema = Schema()
       builder = H3Graph(schema, unet, te, lora)
       if mode == "r2v":
           if not CFG["REF_IMAGES"]:
               raise SystemExit("MODE='r2v' needs CFG['REF_IMAGES'] and <Picture N> tags "
                                "in the prompt.")
           names = [upload_image(p) for p in CFG["REF_IMAGES"][:9]]
           graph = builder.r2v(w, h, length, names)
       else:
           first = upload_image(CFG["FIRST_FRAME"]) if CFG["FIRST_FRAME"] else None
           last = upload_image(CFG["LAST_FRAME"]) if CFG["LAST_FRAME"] else None
           if mode == "flf2v" and not (first or last):
               raise SystemExit("MODE='flf2v' needs FIRST_FRAME and/or LAST_FRAME.")
           graph = builder.t2v_or_flf2v(w, h, length, first, last)
       print(f"graph: {len(graph)} nodes "
             f"({', '.join(sorted({n['class_type'] for n in graph.values()}))})")
       files = run_graph(graph, server)
       server.free_vram()
   finally:
       server.stop()
   if not files:
       print("No output file found. Log tail:\n", server.tail())
       return
   for f in files:
       print(f"\noutput: {f}  ({f.stat().st_size/1e6:.1f} MB)")
   try:
       from IPython.display import Video, display
       vid = files[-1]
       if vid.stat().st_size < 60e6:
           display(Video(str(vid), embed=True, width=720))
       else:
           print("Too large to embed — use files.download() or check /content/outputs")
   except Exception:
       pass
main()

我们处理图像上传、图提交、WebSocket 进度跟踪、输出发现以及教程的完整执行流程。我们将生成的图提交给 ComfyUI,监控各个节点的执行和采样进度,收集生成的视频文件,并直接在 Colab 中展示可管理的输出。最后,我们通过主函数协调所有前述组件,使工作流从硬件预检和模型加载,一直到同步的 MiniMax-H3 视频和音频生成。

总而言之,我们实现了一条完整的、可编程的 MiniMax-H3 推理流水线,涵盖从硬件验证与模型获取,到图执行以及最终的同步视频-音频生成。我们将 ComfyUI 用作无头服务器,同时从 Python 控制整个工作流,这让我们能够直接访问配置、模型加载、条件设定、采样、解码、服务器生命周期管理以及生成输出。我们还通过动态检查 ComfyUI 节点模式、根据可用 VRAM 调整模型配置、将帧数与 MiniMax-H3 要求对齐,以及通过同一套可复用架构支持多种条件模式,使该流水线更加稳健。到工作流结束时,我们拥有了一个灵活的基础,可以在此基础上扩展不同的提示词、随机种子、参考图像、帧约束、LoRA 加速、分辨率和采样策略,同时保持一个一致且自动化的 MiniMax-H3 生成流程。


来源:MarkTechPost(RSS)· marktechpost.com