AI 生图 · Kaggle 免费 GPU 跑 ComfyUI 完整流程


地址:https://www.kaggle.com/


 完整脚本:

"""在 Kaggle GPU Notebook 的一个 Python 代码单元中运行。


首次使用请启用 GPU 和 Internet。重复运行会保留已有环境和通过校验的模型。

"""


import hashlib

import json

import os

from pathlib import Path

import re

import shutil

import subprocess

import sys

import time

import urllib.request



ROOT = Path("/kaggle/working/ComfyUI")

PORT = 8188

LOCAL_URL = f"http://127.0.0.1:{PORT}"

VAE_URL = (

    "https://huggingface.co/Comfy-Org/Lumina_Image_2.0_Repackaged/"

    "resolve/main/split_files/vae/ae.safetensors"

)

VAE_SHA256 = "afc8e28272cd15db3919bacdb6918ce9c1ed22e96cb12c4d5ed0fba823529e38"



def run_checked(*args, cwd=None):

    subprocess.run(list(args), cwd=cwd, check=True)



def server_ready():

    try:

        with urllib.request.urlopen(f"{LOCAL_URL}/system_stats", timeout=2) as r:

            data = json.load(r)

            return r.status == 200 and "system" in data

    except Exception:

        return False



def log_tail(path):

    if not path.exists():

        return "尚无日志"

    with path.open("rb") as f:

        f.seek(max(0, path.stat().st_size - 6000))

        return f.read().decode("utf-8", errors="replace")



# ==========================================

# 1. 准备环境:保留已有模型和工作流

# ==========================================

print("📦 1/5 准备 ComfyUI 与 GGUF 插件...", flush=True)

ROOT.parent.mkdir(parents=True, exist_ok=True)

if not (ROOT / "main.py").is_file():

    run_checked("git", "clone", "https://github.com/comfyanonymous/ComfyUI.git", str(ROOT))


plugin_dir = ROOT / "custom_nodes" / "ComfyUI-GGUF"

if not plugin_dir.exists():

    run_checked("git", "clone", "https://github.com/city96/ComfyUI-GGUF.git", str(plugin_dir))



# ==========================================

# 2. 安装依赖:使用 Kaggle 自带的 CUDA PyTorch

# ==========================================

print("⚡ 2/5 检查 Python 依赖...", flush=True)

was_running = server_ready()

if was_running:

    print("✅ ComfyUI 已在运行,保留当前依赖环境。", flush=True)

else:

    import torch


    if not torch.cuda.is_available():

        raise RuntimeError("未检测到 CUDA GPU,请先在 Kaggle Settings 中启用 GPU。")

    print(f"✅ PyTorch {torch.__version__},GPU:{torch.cuda.get_device_name(0)}", flush=True)

    run_checked(sys.executable, "-m", "pip", "install", "-q", "-r", str(ROOT / "requirements.txt"))

    if (plugin_dir / "requirements.txt").is_file():

        run_checked(sys.executable, "-m", "pip", "install", "-q", "-r", str(plugin_dir / "requirements.txt"))


from safetensors import safe_open



def verify_model(path, expected_size, expected_sha256=None):

    path = Path(path)

    if not path.is_file() or path.stat().st_size != expected_size:

        return False

    try:

        if path.suffix == ".gguf":

            with path.open("rb") as f:

                if f.read(4) != b"GGUF":

                    return False

        else:

            with safe_open(str(path), framework="pt", device="cpu") as f:

                if not f.keys():

                    return False

        if expected_sha256:

            digest = hashlib.sha256()

            with path.open("rb") as f:

                for chunk in iter(lambda: f.read(4 * 1024 * 1024), b""):

                    digest.update(chunk)

            if digest.hexdigest() != expected_sha256:

                return False

        return True

    except Exception:

        return False



def download_checked(source_url, target, expected_size, expected_sha256=None):

    target = Path(target)

    target.parent.mkdir(parents=True, exist_ok=True)

    if verify_model(target, expected_size, expected_sha256):

        print(f"✅ 校验通过,保留:{target.name}", flush=True)

        return


    # 保留正式文件,下载到临时文件;通过校验后再原子替换。

    temporary = target.with_name(target.stem + ".download" + target.suffix)

    for attempt in range(1, 4):

        try:

            print(f"📥 下载 {target.name},第 {attempt}/3 次...", flush=True)

            request = urllib.request.Request(

                source_url, headers={"User-Agent": "ComfyUI-Kaggle-Setup/1.0"}

            )

            with urllib.request.urlopen(request, timeout=60) as response:

                if response.status != 200:

                    raise RuntimeError(f"HTTP {response.status},未返回完整文件")

                with temporary.open("wb") as output:

                    shutil.copyfileobj(response, output, 4 * 1024 * 1024)


            if not verify_model(temporary, expected_size, expected_sha256):

                actual_size = temporary.stat().st_size if temporary.exists() else 0

                raise RuntimeError(

                    f"大小、结构或 SHA-256 校验失败:实际 {actual_size:,} 字节,"

                    f"预期 {expected_size:,} 字节"

                )

            temporary.replace(target)

            print(f"✅ 下载及校验通过:{target.name},{expected_size:,} 字节", flush=True)

            return

        except Exception as error:

            print(f"⚠️ 下载失败:{error}", flush=True)

            if attempt == 3:

                raise RuntimeError(f"{target.name} 下载失败,脚本已停止。") from error

            time.sleep(2 * attempt)



# ==========================================

# 3. 下载并校验全套 FLUX.1 schnell 模型

# ==========================================

print("📥 3/5 下载并校验 FLUX.1 模型...", flush=True)

download_checked(

    "https://huggingface.co/city96/FLUX.1-schnell-gguf/resolve/main/flux1-schnell-Q4_K_S.gguf",

    ROOT / "models/unet/flux1-schnell-Q4_K_S.gguf",

    6783943712,

)

download_checked(

    "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors",

    ROOT / "models/clip/t5xxl_fp8_e4m3fn.safetensors",

    4893934904,

)

download_checked(

    "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors",

    ROOT / "models/clip/clip_l.safetensors",

    246144152,

)

# 此公开 VAE 地址已在本次 Kaggle 环境实际下载并通过 SHA-256 校验。

download_checked(VAE_URL, ROOT / "models/vae/ae.safetensors", 335304388, VAE_SHA256)

print("✅ 全部模型校验通过!", flush=True)



# ==========================================

# 4. 启动 ComfyUI:等待实际就绪,避免重复启动

# ==========================================

print("🚀 4/5 检查 ComfyUI 服务...", flush=True)

if not server_ready():

    server_log = ROOT / "comfyui.log"

    with server_log.open("a", encoding="utf-8") as log_file:

        comfy_process = subprocess.Popen(

            [sys.executable, "-u", "main.py", "--listen", "0.0.0.0", "--port", str(PORT)],

            cwd=ROOT,

            stdout=log_file,

            stderr=subprocess.STDOUT,

            start_new_session=True,

        )

    # 使用 ComfyUI 默认显存管理,允许将暂时不用的权重卸载到内存。

    deadline = time.monotonic() + 180

    while not server_ready():

        if comfy_process.poll() is not None:

            raise RuntimeError("ComfyUI 启动失败:\n" + log_tail(server_log))

        if time.monotonic() >= deadline:

            raise TimeoutError("ComfyUI 启动等待超时:\n" + log_tail(server_log))

        time.sleep(1)

print(f"✅ ComfyUI 服务已就绪:{LOCAL_URL}", flush=True)



# ==========================================

# 5. 复用现有临时隧道,或启动 Cloudflare 临时隧道

# ==========================================

print("🔗 5/5 准备临时访问链接...", flush=True)

old_tunnel = globals().get("tunnel_process")

old_url = globals().get("url", "")

reuse_tunnel = (

    old_tunnel is not None

    and old_tunnel.poll() is None

    and isinstance(old_url, str)

    and re.fullmatch(r"https://[a-zA-Z0-9-]+\.trycloudflare\.com", old_url)

)


if reuse_tunnel:

    print(f"🎉 临时生图网址:{old_url}", flush=True)

else:

    cloudflared = shutil.which("cloudflared")

    if cloudflared is None:

        binary = ROOT.parent / "cloudflared"

        if not binary.is_file():

            binary_tmp = binary.with_suffix(".download")

            binary_url = (

                "https://github.com/cloudflare/cloudflared/releases/latest/"

                "download/cloudflared-linux-amd64"

            )

            with urllib.request.urlopen(binary_url, timeout=60) as response:

                if response.status != 200:

                    raise RuntimeError(f"cloudflared 下载失败:HTTP {response.status}")

                with binary_tmp.open("wb") as output:

                    shutil.copyfileobj(response, output)

            with binary_tmp.open("rb") as f:

                if f.read(4) != b"\x7fELF":

                    raise RuntimeError("cloudflared 下载内容不是有效的 Linux 可执行文件。")

            binary_tmp.replace(binary)

        binary.chmod(binary.stat().st_mode | 0o111)

        cloudflared = str(binary)


    tunnel_log = ROOT / "cloudflared.log"

    with tunnel_log.open("w", encoding="utf-8") as log_file:

        tunnel_process = subprocess.Popen(

            [cloudflared, "tunnel", "--url", LOCAL_URL, "--no-autoupdate"],

            stdout=log_file,

            stderr=subprocess.STDOUT,

            start_new_session=True,

        )


    # 日志直接写文件,避免读取到网址后停止读 stderr,导致管道塞满。

    deadline = time.monotonic() + 90

    while True:

        tunnel_text = log_tail(tunnel_log)

        match = re.search(r"https://[a-zA-Z0-9-]+\.trycloudflare\.com", tunnel_text)

        if match:

            url = match.group(0)

            print(f"🎉 临时生图网址:{url}", flush=True)

            break

        if tunnel_process.poll() is not None:

            raise RuntimeError("临时隧道启动失败:\n" + tunnel_text)

        if time.monotonic() >= deadline:

            raise TimeoutError("等待临时网址超时:\n" + tunnel_text)

        time.sleep(0.5)


ComfyUI配置json 

{

  "last_node_id": 9,

  "last_link_id": 9,

  "nodes": [

    {

      "id": 1,

      "type": "UnetLoaderGGUF",

      "pos": [100, 100],

      "size": [315, 82],

      "flags": {},

      "order": 0,

      "mode": 0,

      "inputs": [],

      "outputs": [

        { "name": "MODEL", "type": "MODEL", "links": [1], "shape": 3 }

      ],

      "properties": { "Node name for S&R": "UnetLoaderGGUF" },

      "widgets_values": ["flux1-schnell-Q4_K_S.gguf"]

    },

    {

      "id": 2,

      "type": "DualCLIPLoader",

      "pos": [100, 220],

      "size": [315, 130],

      "flags": {},

      "order": 1,

      "mode": 0,

      "inputs": [],

      "outputs": [

        { "name": "CLIP", "type": "CLIP", "links": [2, 3], "shape": 3 }

      ],

      "properties": { "Node name for S&R": "DualCLIPLoader" },

      "widgets_values": ["clip_l.safetensors", "t5xxl_fp8_e4m3fn.safetensors", "flux", "default"]

    },

    {

      "id": 3,

      "type": "VAELoader",

      "pos": [100, 400],

      "size": [315, 58],

      "flags": {},

      "order": 2,

      "mode": 0,

      "inputs": [],

      "outputs": [

        { "name": "VAE", "type": "VAE", "links": [4], "shape": 3 }

      ],

      "properties": { "Node name for S&R": "VAELoader" },

      "widgets_values": ["ae.safetensors"]

    },

    {

      "id": 4,

      "type": "CLIPTextEncode",

      "pos": [460, 100],

      "size": [400, 200],

      "flags": {},

      "order": 4,

      "mode": 0,

      "inputs": [

        { "name": "clip", "type": "CLIP", "link": 2 }

      ],

      "outputs": [

        { "name": "CONDITIONING", "type": "CONDITIONING", "links": [5], "shape": 3 }

      ],

      "title": "正向提示词 (Positive Prompt)",

      "properties": { "Node name for S&R": "CLIPTextEncode" },

      "widgets_values": ["High-fashion editorial photography of a sophisticated corporate woman sipping coffee next to a huge floor-to-ceiling glass window. Bright golden sunlight illuminate the luxurious interior. She wears a minimalist designer suit with a stylish watch, effortlessly chic. Crystal clear reflection on the window, rich details, flawless composition, Vogue magazine style, dramatic natural lighting, 8k resolution, masterpiece."]

    },

    {

      "id": 5,

      "type": "CLIPTextEncode",

      "pos": [460, 340],

      "size": [400, 150],

      "flags": {},

      "order": 5,

      "mode": 0,

      "inputs": [

        { "name": "clip", "type": "CLIP", "link": 3 }

      ],

      "outputs": [

        { "name": "CONDITIONING", "type": "CONDITIONING", "links": [6], "shape": 3 }

      ],

      "title": "负向提示词 (Negative Prompt)",

      "properties": { "Node name for S&R": "CLIPTextEncode" },

      "widgets_values": [""]

    },

    {

      "id": 6,

      "type": "EmptyLatentImage",

      "pos": [460, 530],

      "size": [315, 106],

      "flags": {},

      "order": 3,

      "mode": 0,

      "inputs": [],

      "outputs": [

        { "name": "LATENT", "type": "LATENT", "links": [7], "shape": 3 }

      ],

      "properties": { "Node name for S&R": "EmptyLatentImage" },

      "widgets_values": [1024, 1024, 1]

    },

    {

      "id": 7,

      "type": "KSampler",

      "pos": [900, 100],

      "size": [315, 262],

      "flags": {},

      "order": 6,

      "mode": 0,

      "inputs": [

        { "name": "model", "type": "MODEL", "link": 1 },

        { "name": "positive", "type": "CONDITIONING", "link": 5 },

        { "name": "negative", "type": "CONDITIONING", "link": 6 },

        { "name": "latent_image", "type": "LATENT", "link": 7 }

      ],

      "outputs": [

        { "name": "LATENT", "type": "LATENT", "links": [8], "shape": 3 }

      ],

      "properties": { "Node name for S&R": "KSampler" },

      "widgets_values": [0, "fixed", 4, 1.0, "euler", "simple", 1.0]

    },

    {

      "id": 8,

      "type": "VAEDecode",

      "pos": [1250, 100],

      "size": [210, 46],

      "flags": {},

      "order": 7,

      "mode": 0,

      "inputs": [

        { "name": "samples", "type": "LATENT", "link": 8 },

        { "name": "vae", "type": "VAE", "link": 4 }

      ],

      "outputs": [

        { "name": "IMAGE", "type": "IMAGE", "links": [9], "shape": 3 }

      ],

      "properties": { "Node name for S&R": "VAEDecode" },

      "widgets_values": []

    },

    {

      "id": 9,

      "type": "SaveImage",

      "pos": [1250, 190],

      "size": [400, 450],

      "flags": {},

      "order": 8,

      "mode": 0,

      "inputs": [

        { "name": "images", "type": "IMAGE", "link": 9 }

      ],

      "properties": { "Node name for S&R": "SaveImage" },

      "widgets_values": ["ComfyUI"]

    }

  ],

  "links": [

    [1, 1, 0, 7, 0, "MODEL"],

    [2, 2, 0, 4, 0, "CLIP"],

    [3, 2, 0, 5, 0, "CLIP"],

    [4, 3, 0, 8, 1, "VAE"],

    [5, 4, 0, 7, 1, "CONDITIONING"],

    [6, 5, 0, 7, 2, "CONDITIONING"],

    [7, 6, 0, 7, 3, "LATENT"],

    [8, 7, 0, 8, 0, "LATENT"],

    [9, 8, 0, 9, 0, "IMAGE"]

  ],

  "config": {},

  "extra": {},

  "version": 0.4

}

评论

此博客中的热门博文

ChatGPT Team/Business 买一送一持续48个月!订购教程