CivArchive
    MiniMax H3 Face Refinement Workflow ( Fix Oily Skin & Identity Drift) - v1.0
    NSFW

    Hey I have made a workflow for MiniMax H3 with dual sampling with face refinement so I can help with the oily face

    Description

    FAQ

    Comments (9)

    jafdeth2030105Oct 15, 2026
    CivitAI

    H3 Face track node error:

    # ComfyUI Error Report ## Error Details - Node ID: 401 - Node Type: H3FaceTrackCrop - Exception Type: NotImplementedError - Exception Message: NotImplementedError: Could not run 'torchvision::nms' with arguments from the 'CUDA' backend. This could be because the operator doesn't exist for this backend, or was omitted during the selective/custom build process (if using custom build). If you are a Facebook employee using PyTorch on mobile, please visit https://fburl.com/ptmfixes for possible resolutions. 'torchvision::nms' is only available for these backends: [CPU, Meta, BackendSelect, Python, FuncTorchDynamicLayerBackMode, Functionalize, Conjugate, Negative, ZeroTensor, ADInplaceOrView, AutogradOther, AutogradCPU, AutogradCUDA, AutogradXLA, AutogradMPS, AutogradXPU,

    misterpurplepillOct 17, 2026

    This could be because the operator doesn't exist for this backend, or was omitted during the selective/custom build process (if using custom build).

    dilectiogamesOct 15, 2026· 2 reactions
    CivitAI

    anyone has any idea on how to make this works? it doesn't detect the extensions even when installed. can you post a clean version without the upscaler

    misterpurplepillOct 17, 2026

    hi, I created a scritp, run it on your conda environment or your venv:


    #!/usr/bin/env python3
    """
    Idempotent installer for MiniMax H3 FaceRefine workflow setup.
    
    Installs custom nodes, pip deps, face detectors, LoRAs, and optional large models.
    
    Usage (on the ComfyUI server):
      conda activate comfy
      python install.py --comfy-root ~/ComfyUI
    
      # skip ~60GB Singularity + int8 TE (reuse what you already have):
      python install.py --comfy-root ~/ComfyUI --skip-large
    
      # only nodes + detectors + small LoRAs:
      python install.py --comfy-root ~/ComfyUI --skip-large --skip-upscaler
    
    Logs: ./install-face-refine.log (next to this script) + stdout.
    """
    
    from __future__ import annotations
    
    import argparse
    import logging
    import os
    import shutil
    import subprocess
    import sys
    import time
    import urllib.request
    from dataclasses import dataclass, field
    from pathlib import Path
    from typing import Iterable, Optional
    
    SCRIPT_DIR = Path(__file__).resolve().parent
    DEFAULT_LOG = SCRIPT_DIR / "install-face-refine.log"
    
    # ---------------------------------------------------------------------------
    # Catalog
    # ---------------------------------------------------------------------------
    
    CUSTOM_NODES: list[dict] = [
        {
            "name": "ComfyUI-H3-FaceRefine",
            "url": "https://github.com/Carasibana/ComfyUI-H3-FaceRefine.git",
            "required": True,
            "note": "Provides H3FaceTrackCrop (#401), H3FaceStitch, H3PerFrameDenoise, H3InjectVideoLatent",
        },
        {
            "name": "ComfyUI-Impact-Pack",
            "url": "https://github.com/ltdrdata/ComfyUI-Impact-Pack.git",
            "required": True,
            "note": "Registers models/ultralytics/{bbox,segm} so detector dropdown validates",
        },
        {
            "name": "Comfyui_Minimax_h3_latent_Upscaler",
            "url": "https://github.com/LBH-123-AI/Comfyui_Minimax_h3_latent_Upscaler.git",
            "required": False,
            "skip_flag": "skip_upscaler",
            "note": "Optional Latent Upscaler node (MinimaxH3LatentUpscaler3D)",
        },
    ]
    
    HF_FILES: list[dict] = [
        # LoRAs (small / medium)
        {
            "repo": "Alissonerdx/Minimax-H3-ComfyUI",
            "repo_file": "loras/minimax_h3_lms_v1.0_r64.safetensors",
            "dest_rel": "models/loras/minimax_h3_lms_v1.0_r64.safetensors",
            "group": "loras",
            "approx": "~1.24 GB",
        },
        {
            "repo": "drbaph/MiniMax-H3-Turbo-Lora-ComfyUI",
            "repo_file": "minimax_h3_ref2v_turbo_4step_v0.1_comfyui_resized_avg_rank_21_bf16.safetensors",
            "dest_rel": "models/loras/minimax_h3_ref2v_turbo_4step_v0.1_comfyui_resized_avg_rank_21_bf16.safetensors",
            "group": "loras",
            "approx": "~327 MB",
            "alt_note": "Or pick your existing lightx2v turbo 4-step full bf16 in the LoRA node dropdown",
        },
        {
            "repo": "prithivMLmods/MiniMax-H3-Facial-Realism-CloseUp",
            "repo_file": "minimax-h3-facial-realism-closeup-cp2000.safetensors",
            "dest_rel": "models/loras/minimax-h3-facial-realism-closeup-cp2000.safetensors",
            "group": "loras",
            "approx": "~75 MB",
        },
        # Large (skippable)
        {
            "repo": "Comfy-Org/MiniMax-H3",
            "repo_file": "text_encoders/qwen3vl_32b_minimax_h3_int8_convrot.safetensors",
            "dest_rel": "models/text_encoders/qwen3vl_32b_minimax_h3_int8_convrot.safetensors",
            "group": "large",
            "approx": "~27 GB",
            "alt_note": "MANUAL OK: reuse qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors in CLIPLoader",
        },
        {
            "repo": "WarmBloodAban/Minimax-h3_Singularity",
            "repo_file": "Minimax-h3_Singularity_ref2va_v1.3_int8.safetensors",
            "dest_rel": "models/diffusion_models/Minimax-h3_Singularity_ref2va_v1.3_int8.safetensors",
            "group": "large",
            "approx": "~34 GB",
            "alt_note": "MANUAL OK: reuse minimax_h3_ref2va_pruned_int8_convrot.safetensors (or your usual REF2VA) in UNETLoader",
        },
        # Upscaler v1 (V2 filename from workflow is not on public HF; install v1 + manual select)
        {
            "repo": "LBH-123-AI/Minimax_h3_latent_Upscaler",
            "repo_file": "minimax_h3_latent_upscaler_3d_conv_v1/minimax_h3_latent_upscaler_3d_conv_v1_fp16.safetensors",
            "dest_rel": "models/latent_upscale_models/minimax_h3_latent_upscaler_3d_conv_v1_fp16.safetensors",
            "group": "upscaler",
            "approx": "~691 MB",
            "alt_note": (
                "MANUAL: workflow asks for minimax_h3_latent_upscaler_3d_fp16_V2.safetensors "
                "(not found on public HF). Select this v1 file in the node, or mute Optional Latent Upscaler"
            ),
        },
    ]
    
    URL_FILES: list[dict] = [
        {
            "url": "https://huggingface.co/Bingsu/adetailer/resolve/main/face_yolov8m.pt",
            "dest_rel": "models/ultralytics/bbox/face_yolov8m.pt",
            "group": "detectors",
            "approx": "~50 MB",
        },
        {
            "url": "https://huggingface.co/Bingsu/adetailer/resolve/main/person_yolov8m-seg.pt",
            "dest_rel": "models/ultralytics/segm/person_yolov8m-seg.pt",
            "group": "detectors",
            "approx": "~50 MB",
        },
    ]
    
    # Known alternatives already on many H3 setups (for status report)
    ALT_CHECKS: list[tuple[str, str]] = [
        ("models/text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors", "TE nvfp4 (alt for int8)"),
        ("models/diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors", "REF2VA pruned (alt for Singularity)"),
        ("models/loras/minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors", "turbo 4-step full (alt for resized)"),
        ("models/vae/minimax_h3_video_vae_fp16.safetensors", "video VAE"),
        ("models/vae/minimax_h3_audio_vae_fp32.safetensors", "audio VAE"),
    ]
    
    
    # ---------------------------------------------------------------------------
    # Logging / UI
    # ---------------------------------------------------------------------------
    
    class Color:
        RESET = "\033[0m"
        BOLD = "\033[1m"
        RED = "\033[31m"
        GREEN = "\033[32m"
        YELLOW = "\033[33m"
        BLUE = "\033[34m"
        CYAN = "\033[36m"
        DIM = "\033[2m"
    
        @classmethod
        def enabled(cls) -> bool:
            return sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
    
    
    def c(text: str, color: str) -> str:
        if not Color.enabled():
            return text
        return f"{color}{text}{Color.RESET}"
    
    
    def setup_logging(log_path: Path, verbose: bool) -> logging.Logger:
        log_path.parent.mkdir(parents=True, exist_ok=True)
        logger = logging.getLogger("face_refine_install")
        logger.handlers.clear()
        logger.setLevel(logging.DEBUG)
    
        fmt = logging.Formatter(
            "%(asctime)s | %(levelname)-7s | %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S",
        )
    
        fh = logging.FileHandler(log_path, encoding="utf-8")
        fh.setLevel(logging.DEBUG)
        fh.setFormatter(fmt)
        logger.addHandler(fh)
    
        sh = logging.StreamHandler(sys.stdout)
        sh.setLevel(logging.DEBUG if verbose else logging.INFO)
        sh.setFormatter(fmt)
        logger.addHandler(sh)
    
        return logger
    
    
    @dataclass
    class Result:
        ok: int = 0
        skipped: int = 0
        failed: int = 0
        manual: list[str] = field(default_factory=list)
        errors: list[str] = field(default_factory=list)
    
        def add_manual(self, msg: str) -> None:
            self.manual.append(msg)
    
        def add_error(self, msg: str) -> None:
            self.failed += 1
            self.errors.append(msg)
    
    
    # ---------------------------------------------------------------------------
    # Helpers
    # ---------------------------------------------------------------------------
    
    def expand(path: str | Path) -> Path:
        return Path(os.path.expanduser(str(path))).resolve()
    
    
    def human_bytes(n: float) -> str:
        for unit in ("B", "KB", "MB", "GB", "TB"):
            if abs(n) < 1024:
                return f"{n:3.1f} {unit}"
            n /= 1024
        return f"{n:.1f} PB"
    
    
    def run_cmd(
        cmd: list[str],
        *,
        cwd: Optional[Path] = None,
        logger: logging.Logger,
        check: bool = True,
        env: Optional[dict] = None,
    ) -> subprocess.CompletedProcess:
        logger.debug("CMD: %s (cwd=%s)", " ".join(cmd), cwd or ".")
        merged = os.environ.copy()
        if env:
            merged.update(env)
        try:
            proc = subprocess.run(
                cmd,
                cwd=str(cwd) if cwd else None,
                env=merged,
                text=True,
                capture_output=True,
            )
        except FileNotFoundError as e:
            logger.error("Command not found: %s", cmd[0])
            raise RuntimeError(f"command not found: {cmd[0]}") from e
    
        if proc.stdout:
            for line in proc.stdout.splitlines():
                logger.debug("  out| %s", line)
        if proc.stderr:
            for line in proc.stderr.splitlines():
                logger.debug("  err| %s", line)
    
        if check and proc.returncode != 0:
            raise RuntimeError(
                f"command failed ({proc.returncode}): {' '.join(cmd)}\n"
                f"stderr:\n{proc.stderr[-2000:] if proc.stderr else '(empty)'}"
            )
        return proc
    
    
    def ensure_dir(path: Path, logger: logging.Logger) -> None:
        if not path.exists():
            logger.info("mkdir %s", path)
            path.mkdir(parents=True, exist_ok=True)
    
    
    def file_ok(path: Path, min_bytes: int = 1024) -> bool:
        return path.is_file() and path.stat().st_size >= min_bytes
    
    
    def progress_bar(done: int, total: int, width: int = 28) -> str:
        if total <= 0:
            return "[" + "?" * width + "]"
        frac = min(1.0, done / total)
        filled = int(width * frac)
        return "[" + "#" * filled + "-" * (width - filled) + f"] {frac * 100:5.1f}%"
    
    
    def download_url(
        url: str,
        dest: Path,
        logger: logging.Logger,
        *,
        force: bool = False,
    ) -> str:
        """Download URL to dest. Returns 'ok'|'skipped'|'failed'."""
        ensure_dir(dest.parent, logger)
        if file_ok(dest) and not force:
            logger.info("SKIP exists %s (%s)", dest, human_bytes(dest.stat().st_size))
            return "skipped"
    
        tmp = dest.with_suffix(dest.suffix + ".partial")
        logger.info("DOWNLOAD %s", url)
        logger.info("     -> %s", dest)
    
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "face-refine-install/1.0"})
            with urllib.request.urlopen(req, timeout=120) as resp:
                total = int(resp.headers.get("Content-Length") or 0)
                chunk = 1024 * 1024
                written = 0
                t0 = time.time()
                with open(tmp, "wb") as f:
                    while True:
                        data = resp.read(chunk)
                        if not data:
                            break
                        f.write(data)
                        written += len(data)
                        elapsed = max(0.001, time.time() - t0)
                        speed = written / elapsed
                        if total:
                            msg = (
                                f"\r  {progress_bar(written, total)} "
                                f"{human_bytes(written)}/{human_bytes(total)} "
                                f"{human_bytes(speed)}/s"
                            )
                        else:
                            msg = f"\r  {human_bytes(written)} downloaded  {human_bytes(speed)}/s"
                        sys.stdout.write(msg)
                        sys.stdout.flush()
                sys.stdout.write("\n")
                sys.stdout.flush()
    
            tmp.replace(dest)
            logger.info("OK downloaded %s (%s)", dest.name, human_bytes(dest.stat().st_size))
            return "ok"
        except Exception as e:
            logger.error("FAILED download %s: %s", url, e)
            if tmp.exists():
                try:
                    tmp.unlink()
                except OSError:
                    pass
            return "failed"
    
    
    def download_hf(
        repo: str,
        repo_file: str,
        dest: Path,
        logger: logging.Logger,
        *,
        force: bool = False,
        token: Optional[str] = None,
    ) -> str:
        if file_ok(dest) and not force:
            logger.info("SKIP exists %s (%s)", dest, human_bytes(dest.stat().st_size))
            return "skipped"
    
        ensure_dir(dest.parent, logger)
    
        # Prefer huggingface_hub
        try:
            from huggingface_hub import hf_hub_download  # type: ignore
        except ImportError:
            logger.warning("huggingface_hub not installed — falling back to resolve URL")
            url = f"https://huggingface.co/{repo}/resolve/main/{repo_file}"
            return download_url(url, dest, logger, force=force)
    
        logger.info("HF DOWNLOAD %s :: %s", repo, repo_file)
        logger.info("         -> %s", dest)
        try:
            # Download into a cache dir next to dest, then move/copy into place
            cache_dir = dest.parent / ".hf_cache"
            ensure_dir(cache_dir, logger)
            path = hf_hub_download(
                repo_id=repo,
                filename=repo_file,
                local_dir=str(cache_dir),
                local_dir_use_symlinks=False,
                token=token,
                resume_download=True,
            )
            src = Path(path)
            # hf may nest as cache_dir/repo_file
            if not src.is_file():
                nested = cache_dir / repo_file
                if nested.is_file():
                    src = nested
            if not src.is_file():
                raise FileNotFoundError(f"hf_hub_download returned missing path: {path}")
    
            if src.resolve() != dest.resolve():
                ensure_dir(dest.parent, logger)
                shutil.copy2(src, dest)
            logger.info("OK %s (%s)", dest.name, human_bytes(dest.stat().st_size))
            return "ok"
        except Exception as e:
            logger.error("FAILED HF %s/%s: %s", repo, repo_file, e)
            logger.error("  Tip: pip install -U 'huggingface_hub<2'  (or set HF_TOKEN if gated)")
            return "failed"
    
    
    def git_clone_or_update(
        url: str,
        dest: Path,
        logger: logging.Logger,
        *,
        force_reclone: bool = False,
    ) -> str:
        if dest.exists() and force_reclone:
            logger.warning("Removing existing %s (--force-reclone)", dest)
            shutil.rmtree(dest)
    
        if dest.exists():
            if not (dest / ".git").is_dir():
                logger.error("%s exists but is not a git repo — MANUAL: remove or rename it", dest)
                return "failed"
            logger.info("UPDATE %s", dest.name)
            try:
                run_cmd(["git", "-C", str(dest), "fetch", "--prune"], logger=logger)
                # stay on current branch; pull ff-only
                run_cmd(["git", "-C", str(dest), "pull", "--ff-only"], logger=logger, check=False)
                # even if pull fails (local edits), treat as skipped/ok with warning
                st = run_cmd(["git", "-C", str(dest), "status", "-sb"], logger=logger, check=False)
                logger.info("  %s", (st.stdout or "").strip().splitlines()[0] if st.stdout else "updated")
                return "skipped"
            except Exception as e:
                logger.error("git update failed for %s: %s", dest, e)
                return "failed"
    
        logger.info("CLONE %s", url)
        try:
            run_cmd(["git", "clone", "--depth", "1", url, str(dest)], logger=logger)
            logger.info("OK cloned %s", dest.name)
            return "ok"
        except Exception as e:
            logger.error("CLONE failed %s: %s", url, e)
            return "failed"
    
    
    def pip_install_requirements(
        req_file: Path,
        python: Path,
        logger: logging.Logger,
    ) -> str:
        if not req_file.is_file():
            logger.info("No requirements.txt at %s — skip pip", req_file)
            return "skipped"
        logger.info("PIP install -r %s", req_file)
        try:
            run_cmd(
                [str(python), "-m", "pip", "install", "-r", str(req_file)],
                logger=logger,
            )
            logger.info("OK pip requirements from %s", req_file.parent.name)
            return "ok"
        except Exception as e:
            logger.error("PIP failed for %s: %s", req_file, e)
            return "failed"
    
    
    def pip_ensure(packages: Iterable[str], python: Path, logger: logging.Logger) -> None:
        for pkg in packages:
            logger.info("PIP ensure %s", pkg)
            try:
                run_cmd([str(python), "-m", "pip", "install", "-U", pkg], logger=logger)
            except Exception as e:
                logger.error("Could not install %s: %s", pkg, e)
    
    
    # ---------------------------------------------------------------------------
    # Steps
    # ---------------------------------------------------------------------------
    
    def resolve_comfy_root(arg: Optional[str], logger: logging.Logger) -> Path:
        candidates: list[Path] = []
        if arg:
            candidates.append(expand(arg))
        env = os.environ.get("COMFYUI_ROOT")
        if env:
            candidates.append(expand(env))
        candidates.extend(
            [
                expand("~/ComfyUI"),
                expand("~/comfyui"),
                Path("/opt/ComfyUI"),
            ]
        )
        for p in candidates:
            if (p / "main.py").is_file() or (p / "custom_nodes").is_dir():
                logger.info("ComfyUI root: %s", p)
                return p
        raise SystemExit(
            "Could not find ComfyUI root. Pass --comfy-root ~/ComfyUI "
            "or set COMFYUI_ROOT."
        )
    
    
    def step_custom_nodes(
        comfy: Path,
        args: argparse.Namespace,
        logger: logging.Logger,
        result: Result,
    ) -> None:
        logger.info(c("=== Custom nodes ===", Color.BOLD + Color.CYAN))
        cn = comfy / "custom_nodes"
        ensure_dir(cn, logger)
    
        for item in CUSTOM_NODES:
            if item.get("skip_flag") and getattr(args, item["skip_flag"], False):
                logger.info("SKIP node %s (%s)", item["name"], item["skip_flag"])
                result.skipped += 1
                continue
    
            dest = cn / item["name"]
            logger.info("--- %s ---", item["name"])
            logger.info("  %s", item["note"])
            status = git_clone_or_update(item["url"], dest, logger, force_reclone=args.force_reclone)
            if status == "ok":
                result.ok += 1
            elif status == "skipped":
                result.skipped += 1
            else:
                result.add_error(f"custom_node {item['name']}")
                if item.get("required"):
                    result.add_manual(
                        f"REQUIRED: manually clone {item['url']} into {dest}"
                    )
    
            # pip requirements
            req = dest / "requirements.txt"
            if dest.is_dir() and not args.skip_pip:
                st = pip_install_requirements(req, Path(args.python), logger)
                if st == "failed":
                    result.add_error(f"pip {item['name']}")
                    result.add_manual(
                        f"MANUAL: cd {dest} && {args.python} -m pip install -r requirements.txt"
                    )
    
    
    def step_detectors(comfy: Path, args: argparse.Namespace, logger: logging.Logger, result: Result) -> None:
        logger.info(c("=== Face detectors ===", Color.BOLD + Color.CYAN))
        for item in URL_FILES:
            dest = comfy / item["dest_rel"]
            logger.info("--- %s (%s) ---", dest.name, item["approx"])
            st = download_url(item["url"], dest, logger, force=args.force_download)
            if st == "ok":
                result.ok += 1
            elif st == "skipped":
                result.skipped += 1
            else:
                result.add_error(item["dest_rel"])
                result.add_manual(f"MANUAL: wget -O {dest} '{item['url']}'")
    
    
    def step_hf_models(comfy: Path, args: argparse.Namespace, logger: logging.Logger, result: Result) -> None:
        logger.info(c("=== HuggingFace models ===", Color.BOLD + Color.CYAN))
        token = args.hf_token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
    
        for item in HF_FILES:
            group = item["group"]
            if group == "large" and args.skip_large:
                logger.info(
                    "SKIP large %s — %s",
                    item["dest_rel"],
                    item.get("alt_note", ""),
                )
                result.skipped += 1
                if item.get("alt_note"):
                    result.add_manual(item["alt_note"])
                continue
            if group == "upscaler" and args.skip_upscaler:
                logger.info("SKIP upscaler model (%s)", item["dest_rel"])
                result.skipped += 1
                continue
    
            dest = comfy / item["dest_rel"]
            logger.info("--- %s (%s) ---", dest.name, item["approx"])
            if item.get("alt_note"):
                logger.info("  NOTE: %s", item["alt_note"])
    
            st = download_hf(
                item["repo"],
                item["repo_file"],
                dest,
                logger,
                force=args.force_download,
                token=token,
            )
            if st == "ok":
                result.ok += 1
            elif st == "skipped":
                result.skipped += 1
            else:
                result.add_error(item["dest_rel"])
                result.add_manual(
                    f"MANUAL: huggingface-cli download {item['repo']} {item['repo_file']} "
                    f"--local-dir <tmp> && mv to {dest}"
                )
                if item.get("alt_note"):
                    result.add_manual(item["alt_note"])
    
    
    def step_sanity(
        comfy: Path,
        python: Path,
        logger: logging.Logger,
        result: Result,
    ) -> None:
        logger.info(c("=== Sanity / alternatives ===", Color.BOLD + Color.CYAN))
    
        must = [
            "custom_nodes/ComfyUI-H3-FaceRefine",
            "models/ultralytics/bbox/face_yolov8m.pt",
        ]
        for rel in must:
            p = comfy / rel
            if p.exists():
                logger.info(c("  OK  ", Color.GREEN) + rel)
            else:
                logger.error(c("  MISS", Color.RED) + " %s", rel)
                result.add_error(f"missing {rel}")
    
        for rel, label in ALT_CHECKS:
            p = comfy / rel
            if p.is_file():
                logger.info(
                    c("  ALT ", Color.BLUE) + "%s — %s (%s)",
                    label,
                    rel,
                    human_bytes(p.stat().st_size),
                )
            else:
                logger.debug("  (no alt) %s", rel)
    
        logger.info("Checking onnxruntime providers (InsightFace tip)...")
        proc = run_cmd(
            [
                str(python),
                "-c",
                "import onnxruntime as o; print(','.join(o.get_available_providers()))",
            ],
            logger=logger,
            check=False,
        )
        if proc.returncode == 0:
            providers = (proc.stdout or "").strip()
            logger.info("  onnxruntime providers: %s", providers)
            if "CUDAExecutionProvider" not in providers:
                result.add_manual(
                    "MANUAL: InsightFace may run on CPU. Install ONE of onnxruntime-gpu OR onnxruntime, "
                    f"not both. providers={providers}"
                )
        else:
            logger.warning(
                "  onnxruntime not importable in %s (ok if you skip insightface identity)",
                python,
            )
    
        result.add_manual(
            "MANUAL: Restart ComfyUI after install. Confirm no IMPORT FAILED for ComfyUI-H3-FaceRefine. "
            "Reload minimaxH3FaceRefinement_v10.json — node #401 (H3FaceTrackCrop) should resolve."
        )
        result.add_manual(
            "MANUAL: If using --skip-large, open UNETLoader / CLIPLoader and pick models you already have."
        )
    
    
    def print_summary(result: Result, log_path: Path, logger: logging.Logger) -> None:
        logger.info(c("=== Summary ===", Color.BOLD + Color.CYAN))
        logger.info("  ok=%d  skipped=%d  failed=%d", result.ok, result.skipped, result.failed)
        logger.info("  log file: %s", log_path)
    
        if result.errors:
            logger.error(c("Errors:", Color.RED))
            for e in result.errors:
                logger.error("  - %s", e)
    
        if result.manual:
            # unique preserve order
            seen = set()
            logger.warning(c("Manual / follow-up actions:", Color.YELLOW))
            for m in result.manual:
                if m in seen:
                    continue
                seen.add(m)
                logger.warning("  • %s", m)
    
        if result.failed:
            logger.error("Finished WITH ERRORS — see log and manual actions above.")
            sys.exit(1)
        logger.info(c("Finished OK (idempotent re-run is safe).", Color.GREEN))
    
    
    # ---------------------------------------------------------------------------
    # Main
    # ---------------------------------------------------------------------------
    
    def build_parser() -> argparse.ArgumentParser:
        p = argparse.ArgumentParser(
            description="Idempotent MiniMax H3 FaceRefine installer (nodes + models).",
            formatter_class=argparse.RawDescriptionHelpFormatter,
            epilog="""
    Examples:
      python install.py --comfy-root ~/ComfyUI --skip-large
      python install.py --comfy-root ~/ComfyUI --python $(which python) -v
      COMFYUI_ROOT=~/ComfyUI python install.py --skip-large --skip-upscaler
    """,
        )
        p.add_argument("--comfy-root", default=None, help="Path to ComfyUI (or set COMFYUI_ROOT)")
        p.add_argument(
            "--python",
            default=sys.executable,
            help="Python used for pip (default: this interpreter / activate conda first)",
        )
        p.add_argument("--skip-large", action="store_true", help="Skip ~27GB TE + ~34GB Singularity")
        p.add_argument("--skip-upscaler", action="store_true", help="Skip latent upscaler node + weights")
        p.add_argument("--skip-pip", action="store_true", help="Do not pip install node requirements")
        p.add_argument("--skip-nodes", action="store_true", help="Do not clone/update custom_nodes")
        p.add_argument("--skip-models", action="store_true", help="Do not download any models")
        p.add_argument("--force-download", action="store_true", help="Re-download even if file exists")
        p.add_argument("--force-reclone", action="store_true", help="Delete and re-clone custom nodes")
        p.add_argument("--hf-token", default=None, help="HF token (or HF_TOKEN env)")
        p.add_argument("--log", default=str(DEFAULT_LOG), help="Log file path")
        p.add_argument("-v", "--verbose", action="store_true", help="Debug logs on stdout")
        p.add_argument(
            "--dry-run",
            action="store_true",
            help="Print planned actions only (no git/pip/download)",
        )
        return p
    
    
    def dry_run_report(comfy: Path, args: argparse.Namespace, logger: logging.Logger) -> None:
        logger.info("DRY-RUN comfy=%s python=%s", comfy, args.python)
        logger.info("Custom nodes:")
        for item in CUSTOM_NODES:
            skip = item.get("skip_flag") and getattr(args, item["skip_flag"], False)
            logger.info("  [%s] %s %s", "SKIP" if skip else "DO", item["name"], item["url"])
        logger.info("URL files:")
        for item in URL_FILES:
            logger.info("  %s -> %s", item["url"], comfy / item["dest_rel"])
        logger.info("HF files:")
        for item in HF_FILES:
            skip = (item["group"] == "large" and args.skip_large) or (
                item["group"] == "upscaler" and args.skip_upscaler
            )
            logger.info(
                "  [%s] %s/%s -> %s",
                "SKIP" if skip else "DO",
                item["repo"],
                item["repo_file"],
                comfy / item["dest_rel"],
            )
    
    
    def main(argv: Optional[list[str]] = None) -> None:
        args = build_parser().parse_args(argv)
        log_path = expand(args.log)
        logger = setup_logging(log_path, args.verbose)
        result = Result()
    
        logger.info(c("MiniMax H3 FaceRefine installer", Color.BOLD))
        logger.info("log=%s", log_path)
        logger.info("python=%s", args.python)
    
        try:
            comfy = resolve_comfy_root(args.comfy_root, logger)
        except SystemExit as e:
            logger.error("%s", e)
            raise
    
        if args.dry_run:
            dry_run_report(comfy, args, logger)
            return
    
        # Ensure tooling for HF downloads
        if not args.skip_models and not args.skip_pip:
            pip_ensure(["huggingface_hub<2"], Path(args.python), logger)
    
        if not args.skip_nodes:
            step_custom_nodes(comfy, args, logger, result)
        else:
            logger.info("SKIP custom nodes")
    
        if not args.skip_models:
            step_detectors(comfy, args, logger, result)
            step_hf_models(comfy, args, logger, result)
        else:
            logger.info("SKIP models")
    
        step_sanity(comfy, Path(args.python), logger, result)
        print_summary(result, log_path, logger)
    
    
    if __name__ == "__main__":
        main()
    

    dilectiogamesOct 19, 2026· 1 reaction

    @misterpurplepill thank you this worked, I've struggled with how to run this thing, I'm still learning. you can see the result I've posted a video in the gallery. Do you have any idea how to make it work for small faces, I'm assuming is a setting on that node that detect faces. Thank You again, I really am feedup with all these complex setups that I can't comprehend how they work and you helped me fix the faces to be actual real.

    misterpurplepillOct 26, 2026

    @dilectiogames I recommend a node that you can put a lot of pictures to "pre train" the faces, it is not really a training, take a look on this one https://civitai.red/models/3002052/minimax-h3-refmod-the-same-face-in-every-clip-no-lora?modelVersionId=3405532

    misterpurplepillOct 26, 2026

    thanks for the buzz @dilectiogames

    dilectiogamesOct 26, 2026

    @misterpurplepill I've did some tests with that one works great, but it forces you do go either refmod route or direct at runtime reference images (is not compatible yet for both). also it really slows down your renders, using those refmods. I don't know what the reason is and I can't find a real answer on why to use these refmods over direct reference. I've noticed that if you structure the prompt like minimax actually expect it to be then you don't lose identity over time. if you go outside the guidliness you get punished. also the AIs even the Argon one doesn't have any idea on how this minimax reftovideo works and will give you bad advice. like repeating the initial tags like <Picture X> outside the subject definition section. that is a big error. anyway to get back to the link you have given me, adding a lot of pictures is a mistake as it will slow down the refmod, second there is no need to split the views separately, no need to cut them out, minimax can understand perfectly a character sheet with 4 views which if you pack them correctly it will improve the memory, also always use same image size because that extention will not work correctly with different sizes, it will crop them out.

    tupuOct 16, 2026· 1 reaction
    CivitAI

    The typical wanker!!!

    Workflows
    MiniMax H3

    Details

    Downloads
    815
    Platform
    CivitAI
    Platform Status
    Available
    Created
    10/14/2026
    Updated
    11/2/2026
    Deleted
    -

    Files

    minimaxH3FaceRefinement_v10.json

    Mirrors