AI 生图 · Kaggle 免费 GPU 跑 ComfyUI 完整流程
- 获取链接
- X
- 电子邮件
- 其他应用
地址: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)
{
"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
}
- 获取链接
- X
- 电子邮件
- 其他应用
评论
发表评论