No ComfyUI, no node graph — just a .py file and the diffusers library. The same models and weights ComfyUI runs, called directly. Everything below works locally on CPU; a GPU only makes it faster.不用 ComfyUI,没有节点图——只要一个 .py 文件和 diffusers 库。和 ComfyUI 跑的是同样的模型与权重,只是直接调用。下面的一切都能在本地 CPU 上运行;GPU 只是让它更快。
ComfyUI looks like magic because it paints a graph of nodes. But under the hood every node is calling Hugging Face diffusers + PyTorch — the same library you can import yourself. A diffusers pipeline is one Python object that bundles every node ComfyUI wires by hand:ComfyUI 看起来很神奇,因为它画出一张节点图。但底层每个节点都在调用 Hugging Face 的 diffusers + PyTorch——也就是你自己可以 import 的那个库。一个 diffusers pipeline 就是一个 Python 对象,把 ComfyUI 手工连接的每个节点都打包在一起:
| ComfyUI nodeComfyUI 节点 | diffusers equivalentdiffusers 中的对应 |
|---|---|
| Load Checkpoint | AutoPipelineForText2Image.from_pretrained(...) |
| CLIP Text Encode (positive / negative) | prompt= / negative_prompt= arguments参数 |
| KSampler | num_inference_steps, guidance_scale, generator |
| VAE Decode | automatic — happens inside the pipeline自动——在 pipeline 内部完成 |
| Save Image | image.save("out.png") |
You need Python 3.10+ and these packages. torch CPU wheels are large but install with one command; no CUDA, no drivers required.你需要 Python 3.10+ 和以下这些包。torch 的 CPU 版本体积较大,但一条命令即可安装;无需 CUDA、无需驱动。
# a clean virtual environment keeps this off your system Python
python -m venv .venv
# Windows: .venv\Scripts\activate · macOS/Linux: source .venv/bin/activate
pip install diffusers transformers accelerate safetensors
pip install torch # CPU build is the default
pip install imageio imageio-ffmpeg pillow # for saving the video file
from_pretrained() downloads weights once and caches them. You never manage .safetensors files by hand like in ComfyUI — diffusers fetches and stores them under:~/.cache/huggingface/hub (macOS/Linux) · C:\Users\<you>\.cache\huggingface\hub (Windows). Delete that folder to free space.from_pretrained() 会只下载一次权重并缓存。你不必像在 ComfyUI 里那样手动管理 .safetensors 文件——diffusers 自动获取并存放在:~/.cache/huggingface/hub(macOS/Linux)· C:\Users\<你>\.cache\huggingface\hub(Windows)。删掉这个目录即可释放空间。
If you'd rather not install torch and the rest, everything above is pre-baked into ddtraveller/diffusers-cpu — CPU-only, multi-arch (Intel + Apple Silicon/ARM). The two tools below, t2i and i2v, are exactly the scripts from sections 2 and 3, already on the image's PATH.如果你不想安装 torch 等一堆依赖,上面这一切都已经打包进 ddtraveller/diffusers-cpu 镜像——纯 CPU、多架构(Intel + Apple Silicon/ARM)。下面的两个工具 t2i 和 i2v 就是第 2、3 节里的脚本,已经放在镜像的 PATH 上。
# 1 — pull it
docker pull ddtraveller/diffusers-cpu
# 2 — make two host folders: one for outputs, one to cache downloaded models
mkdir -p ~/diffusers/work ~/diffusers/cache
# text -> image (first run downloads sd-turbo into the cache, then it's instant)
docker run --rm \
-v ~/diffusers/work:/work \
-v ~/diffusers/cache:/cache \
ddtraveller/diffusers-cpu \
t2i --prompt "misty Doi Suthep temple at dawn, lanna style" --out /work/temple.png
# image -> video (SVD is gated — pass your Hugging Face token)
docker run --rm -e HF_TOKEN=hf_xxx \
-v ~/diffusers/work:/work \
-v ~/diffusers/cache:/cache \
ddtraveller/diffusers-cpu \
i2v --image /work/temple.png --out /work/temple.mp4
Your temple.png / temple.mp4 land in ~/diffusers/work on your host. The /cache mount means models download once and are reused next run. Run docker run --rm ddtraveller/diffusers-cpu t2i --help to see every flag (--model, --steps, --seed, …).生成的 temple.png / temple.mp4 会出现在宿主机的 ~/diffusers/work 里。挂载 /cache 意味着模型只下载一次,下次运行直接复用。运行 docker run --rm ddtraveller/diffusers-cpu t2i --help 可查看所有参数(--model、--steps、--seed……)。
On Windows, replace ~/diffusers/... with a full path and use ${PWD} (PowerShell) or %cd% (cmd) for the current folder.在 Windows 上,把 ~/diffusers/... 换成完整路径,当前目录用 ${PWD}(PowerShell)或 %cd%(cmd)。
Using stabilityai/sd-turbo as the example: it is ungated — no Hugging Face login, no license to accept — and the download is about 2.5 GB. Both methods below write to the same cache, so your script finds the weights either way. You only need one.以 stabilityai/sd-turbo 为例:它是非受限模型——不需要登录 Hugging Face、不需要接受许可——下载约 2.5 GB。下面两种方法写入的是同一个缓存,所以脚本都能找到权重。你只需选其一。
The first time from_pretrained() runs, it fetches the weights and caches them; every run after that loads from disk. This is the whole "download step" — there isn't a separate one.第一次执行 from_pretrained() 时会拉取权重并缓存;之后每次运行都从磁盘加载。这就是全部的“下载步骤”——并没有单独的一步。
# run the t2i.py from section 2 — the download happens on this line, once:
# pipe = AutoPipelineForText2Image.from_pretrained("stabilityai/sd-turbo", ...)
python t2i.py # first run pulls ~2.5 GB into the cache, then a few seconds per image
hf CLI方法 B——用 hf 命令行提前下载Use this to grab the weights ahead of time (offline prep, or to inspect the files). It installs nothing your script doesn't already need.用它可以提前把权重拉下来(离线准备,或想查看文件)。它安装的东西脚本本来也需要。
pip install -U "huggingface_hub[cli]"
# downloads the repo into ~/.cache/huggingface/hub — your script finds it automatically
hf download stabilityai/sd-turbo
# …or put the files in a folder you can see, instead of the hidden cache:
hf download stabilityai/sd-turbo --local-dir ./sd-turbo
# confirm what landed (size + on-disk path):
hf cache scan
hf download is the current command; the older huggingface-cli download still works but is deprecated. No hf auth login is needed for sd-turbo — that step is only for gated models like SVD in section 3.hf download 是现在的命令;旧的 huggingface-cli download 仍可用但已弃用。sd-turbo 不需要 hf auth login——那一步只对第 3 节里 SVD 这类受限模型才需要。
"cpu" on any non-CUDA box. On a Mac, swap in Apple's Metal backend for a big speed-up — set device = "mps" and dtype = torch.float16. With "mps" the variant="fp16" argument also halves the download:第 2 节的脚本在非 CUDA 机器上会退回 "cpu"。在 Mac 上换成 Apple 的 Metal 后端能大幅提速——设 device = "mps"、dtype = torch.float16。配合 "mps" 时,variant="fp16" 还能把下载体积减半:
pipe = AutoPipelineForText2Image.from_pretrained(
"stabilityai/sd-turbo", torch_dtype=torch.float16, variant="fp16",
).to("mps")
Cache location on macOS: ~/.cache/huggingface/hub. Delete that folder (or run hf cache delete) to free the space. To pull only the smaller fp16 weights via the CLI: hf download stabilityai/sd-turbo --include "*.json" "*.txt" "*fp16*".macOS 上的缓存位置:~/.cache/huggingface/hub。删除该目录(或运行 hf cache delete)即可释放空间。只想用命令行拉取较小的 fp16 权重:hf download stabilityai/sd-turbo --include "*.json" "*.txt" "*fp16*"。
The whole program is four lines of real work. We use SD-Turbo: a distilled model that produces a 512×512 image in 1–4 steps (regular Stable Diffusion needs ~25), so it finishes in seconds even on CPU. Turbo models are trained without classifier-free guidance, so set guidance_scale=0.0.真正干活的代码就四行。我们用 SD-Turbo:一个蒸馏模型,1–4 步就能生成 512×512 的图(普通 Stable Diffusion 需要约 25 步),所以即便在 CPU 上也几秒完成。Turbo 模型训练时没有使用 classifier-free guidance,所以要设 guidance_scale=0.0。
# t2i.py — text to image, runs on CPU
import torch
from diffusers import AutoPipelineForText2Image
# pick the best device available; CPU needs float32 (float16 is GPU-only)
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float16 if device == "cuda" else torch.float32
# 1 — Load Checkpoint (downloads + caches on first run)
pipe = AutoPipelineForText2Image.from_pretrained(
"stabilityai/sd-turbo", torch_dtype=dtype,
).to(device)
pipe.enable_attention_slicing() # less RAM on small machines
# 2 — CLIP Text Encode + KSampler + VAE Decode, all in one call
image = pipe(
prompt="misty Doi Suthep temple at dawn, golden light, lanna architecture, highly detailed",
num_inference_steps=4, # sd-turbo: 1 is fastest, 4 is a touch nicer
guidance_scale=0.0, # turbo models: CFG OFF
generator=torch.Generator(device).manual_seed(42), # reproducible
).images[0]
# 3 — Save Image
image.save("temple.png")
print("saved temple.png")
python t2i.py # first run downloads ~2.5 GB, then a few seconds per image on CPU
The only thing that changes between models is the repo name in from_pretrained() and the step/guidance settings. The pipeline class auto-detects the architecture.在不同模型之间,唯一要改的就是 from_pretrained() 里的仓库名,以及步数 / guidance 设置。pipeline 类会自动识别模型结构。
| Model (repo id)模型(仓库 id) | Settings设置 | Use it for适用于 |
|---|---|---|
stabilityai/sd-turbo | 1–4 steps, CFG 0, 512²1–4 步,CFG 0,512² | Fastest — the default above.最快——上面的默认。 |
stabilityai/sdxl-turbo | 1–4 steps, CFG 0, 512–10241–4 步,CFG 0,512–1024 | Higher quality, slower on CPU.质量更高,CPU 上更慢。 |
stable-diffusion-v1-5/stable-diffusion-v1-5 | ~25 steps, CFG 7, 512²约 25 步,CFG 7,512² | The classic SD1.5; needs a negative_prompt.经典 SD1.5;需要 negative_prompt。 |
Same shape, different pipeline. Stable Video Diffusion (SVD) takes one still image and predicts a short clip of motion from it. The base model makes 14 frames; ...-xt makes 25. Feed it the temple.png you just generated.套路一样,换个 pipeline。Stable Video Diffusion(SVD)接收一张静态图,并据此预测一小段运动画面。基础模型生成 14 帧;...-xt 生成 25 帧。把你刚生成的 temple.png 喂给它。
# i2v.py — image to video
import torch
from diffusers import StableVideoDiffusionPipeline
from diffusers.utils import load_image, export_to_video
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float16 if device == "cuda" else torch.float32
pipe = StableVideoDiffusionPipeline.from_pretrained(
"stabilityai/stable-video-diffusion-img2vid", torch_dtype=dtype,
).to(device)
pipe.enable_attention_slicing()
# SVD wants a specific size; resize the still first
image = load_image("temple.png").resize((1024, 576))
frames = pipe(
image,
num_frames=14, # base = 14, the -xt model = 25
decode_chunk_size=2, # lower = less memory (decode fewer frames at once)
generator=torch.Generator(device).manual_seed(42),
).frames[0]
export_to_video(frames, "temple.mp4", fps=7)
print("saved temple.mp4")
num_frames=14 and decode_chunk_size=2.用 turbo 模型做文生图在 CPU 上是几秒。视频则完全不同:SVD 一次要对 14–25 帧去噪,所以在 CPU 上一段短视频可能要 几十分钟到一个多小时。代码是对的、也能跑——只是没有 GPU 就得有耐心。先从 num_frames=14、decode_chunk_size=2 开始。
huggingface-cli login # paste a token from huggingface.co/settings/tokens
.md files & images in one render在一次渲染中引用多个 .md 文件和图片Real projects don't hard-code the prompt string. A cartoon series keeps its look, its characters, and each shot in separate .md files, and locks the actual appearance in reference .pngs — a "show bible". At render time a script reads them all, concatenates the text into one prompt, and feeds the images into the pipeline. That is the whole trick to keeping frame #800 on-model with frame #1. (See the companion guide Prepare Once, Animate Forever for how the bible is authored.)真实项目不会把提示词写死。一部卡通剧集把画风、角色和每个镜头分别放在不同的 .md 文件里,并用参考 .png 锁定真实外观——一本「设定集」。渲染时,脚本会把它们全部读入、把文本拼成一条提示词、并把图片喂进 pipeline。这就是让第 800 帧与第 1 帧保持一致的全部诀窍。(设定集如何撰写见姊妹篇 准备一次,永远开拍。)
.md filesA——把多个 .md 文件的文本拼接起来Each bible file is plain YAML after its # title line, so one tiny loader reads any of them. The scene names which characters and location to use; the script resolves those names to the locked tokens and joins everything into a single prompt. One extra dependency: pip install pyyaml.每个设定集文件在 # 标题 行之后就是普通 YAML,所以一个极小的加载函数就能读取任意一个。场景文件指明用哪些角色和地点;脚本把这些名字解析成锁定的 token,再把一切拼成一条 prompt。多一个依赖:pip install pyyaml。
# render_scene.py — assemble ONE prompt from many .md files, then render
import yaml, torch
from pathlib import Path
from diffusers import AutoPipelineForText2Image
def fields(path):
# a .md bible file is YAML once you drop its '# title' line
body = "".join(l for l in open(path, encoding="utf-8") if not l.startswith("#"))
return yaml.safe_load(body)
style = fields("style.md") # render_tokens, negative, model
chars = {p.stem: fields(p) for p in Path("characters").glob("*.md")}
scene = fields("scenes/scene_01.md") # cast, action, camera, motion
# resolve every character named in the scene -> their locked look_tokens
looks = [chars[name]["look_tokens"] for name in scene["cast"]]
prompt = ", ".join(looks + [scene["action"], scene["camera"], style["render_tokens"]])
pipe = AutoPipelineForText2Image.from_pretrained(style["model"], torch_dtype=torch.float32)
image = pipe(
prompt=prompt,
negative_prompt=style["negative"],
num_inference_steps=28, guidance_scale=6.5, # animagine-xl (a normal SDXL model, not turbo)
).images[0]
Path("renders").mkdir(exist_ok=True)
image.save("renders/scene_01.png")
cast: [ploy], not a 40-word description. Change Ploy's hairpin once in characters/ploy.md and every scene that lists her inherits it on the next render — no find-and-replace across hundreds of prompts.场景文件写的是 cast: [ploy],而不是 40 个词的描述。在 characters/ploy.md 里改一次 Ploy 的发夹,下次渲染时每个用到她的场景都会继承——不必在上百条提示词里查找替换。
Words drift; pictures don't. To pin the actual drawing — the character turnaround, the location backdrop — pass them as pixels. IP-Adapter is the standard way, and it takes more than one image: load a list of adapters, set a list of scales, and pass a list to ip_adapter_image — one slot per adapter. Here adapter 1 holds the character, adapter 2 holds the place.文字会走样,图片不会。要锁定真实画作——角色转面图、场景背景——就把它们作为像素传入。IP-Adapter 是标准做法,而且能接收不止一张图:加载一个列表的适配器、设置一个列表的权重、并把一个列表传给 ip_adapter_image——每个适配器对应一个槽位。这里适配器 1 装角色,适配器 2 装地点。
# reference the actual PICTURES, not just words — several at once
from diffusers.utils import load_image
pipe.load_ip_adapter(
"h94/IP-Adapter", subfolder="sdxl_models",
weight_name=[
"ip-adapter-plus-face_sdxl_vit-h.safetensors", # adapter 1 -> the character
"ip-adapter_sdxl_vit-h.safetensors", # adapter 2 -> the place / style
],
)
pipe.set_ip_adapter_scale([0.7, 0.4]) # per-adapter strength (a list, one per adapter)
image = pipe(
prompt=prompt,
negative_prompt=style["negative"],
ip_adapter_image=[ # a LIST -> one entry per loaded adapter
load_image(chars["ploy"]["refs"][0]), # characters/ploy_turnaround.png
load_image("locations/village_lane_wide.png"),
],
num_inference_steps=30, guidance_scale=6.5,
).images[0]
The refs: list comes straight from characters/ploy.md — the YAML loader already parsed it, so chars["ploy"]["refs"][0] is the turnaround path. A single still you want to lightly redraw instead goes through AutoPipelineForImage2Image with image= + a strength= (0.3 keeps it close, 0.8 reimagines it).refs: 列表直接来自 characters/ploy.md——YAML 加载器已经解析好了,所以 chars["ploy"]["refs"][0] 就是转面图路径。若只想对单张图做轻度重绘,则用 AutoPipelineForImage2Image 配 image= 和 strength=(0.3 贴近原图,0.8 大幅重构)。
Because the still you just rendered already is every reference composed into one frame, animating it needs no new references — the composed PNG becomes the video's first frame.因为你刚渲染的静帧本身就是所有参考合成的一帧,让它动起来无需新的参考——这张合成 PNG 直接成为视频的第一帧。
# the composed still IS the video's reference: feed it straight to SVD as frame 0
from diffusers.utils import load_image, export_to_video
seed = load_image("renders/scene_01.png").resize((1024, 576))
frames = pipe_i2v(seed, num_frames=14, decode_chunk_size=2).frames[0]
export_to_video(frames, "renders/scene_01.mp4", fps=7)
# SVD takes no text prompt. For a text-driven i2v model (e.g. Wan 2.2), the
# scene's `motion:` line becomes the motion prompt, with this still as frame 0.
| Source file来源文件 | Field字段 | Goes into进入 |
|---|---|---|
style.md | model | from_pretrained(...) repo id仓库 id |
style.md | render_tokens | appended to every附加到每条 prompt |
style.md | negative | negative_prompt= |
characters/*.md | look_tokens | resolved from由 scene.cast into解析进 prompt |
characters/*.md | refs (png) | ip_adapter_image= / init初始图 image= |
locations/*.png | — | ip_adapter_image= / i2v seed framei2v 种子帧 |
scenes/scene_NN.md | action, camera | prompt |
scenes/scene_NN.md | motion | motion prompt (Wan-style i2v)运动提示词(Wan 类 i2v) |
scenes/scene_NN.md | seconds | clip length /片段时长 / num_frames |
scene_NN.md, run the script. No prompt is ever retyped, no reference re-described — the .md files and .pngs are the inputs, and the pipeline call is the same every time. That's the line between "exploring in a UI" and "shipping a series".一旦加载器就位,渲染一个新镜头就是:写一个简短的 scene_NN.md,运行脚本。提示词永不重打、参考永不重述——.md 文件和 .png 就是输入,pipeline 调用每次都一样。这正是「在界面里探索」与「交付一部剧集」的分界线。
On a 16–24 GB machine, two flags do most of the work. They trade a little speed for a lot less peak memory — the difference between "runs" and "killed: out of memory".在 16–24 GB 内存的机器上,两个开关就能解决大部分问题。它们用一点速度换取大幅降低的峰值内存——这正是“能跑”与“被系统杀掉:内存不足”的区别。
| Call调用 | What it does作用 |
|---|---|
pipe.enable_attention_slicing() | Computes attention in chunks — biggest single RAM saver.分块计算注意力——单项最省内存。 |
pipe.enable_vae_slicing() | Decodes the image/frames in slices (helps SVD most).分片解码图像 / 帧(对 SVD 帮助最大)。 |
pipe.enable_model_cpu_offload() | GPU only — keeps idle sub-models on CPU, moving each to GPU just in time. Frees VRAM.仅 GPU——把空闲的子模型留在 CPU,需要时才搬到 GPU。释放显存。 |
num_inference_steps ↓ | Fewer steps = faster. Turbo models are built for this.步数越少越快。Turbo 模型正是为此而生。 |
generator=torch.Generator(device).manual_seed(N) and the same prompt + settings produce the exact same image every run — across machines. This is the part a UI makes fiddly and a script makes trivial.传入固定的 generator=torch.Generator(device).manual_seed(N),相同的提示词和设置每次都会生成完全相同的图——跨机器也一样。这正是界面里麻烦、而脚本里轻而易举的地方。
| Text → image文生图 | Image → video图生视频 | |
|---|---|---|
| Pipeline classPipeline 类 | AutoPipelineForText2Image | StableVideoDiffusionPipeline |
| Default model默认模型 | stabilityai/sd-turbo | stabilityai/stable-video-diffusion-img2vid |
| Input输入 | a text prompt一段文本提示词 | a still image一张静态图 |
| Output输出 | image.save("out.png") | export_to_video(...,"out.mp4") |
| CPU speedCPU 速度 | seconds (turbo)几秒(turbo) | minutes–hours几十分钟–数小时 |
| Gated?是否受限? | No否 | Yes — accept license + login是——需接受许可并登录 |
diffusers pipelines. Learn from_pretrained(...) → pipe(...) → .save() and you can drive all of them from plain Python, locally, with no UI in the way.每一个扩散模型工具——ComfyUI、Automatic1111、网页 demo、还有这个脚本——调用的都是同一小撮 diffusers pipeline。掌握 from_pretrained(...) → pipe(...) → .save(),你就能用纯 Python 在本地驱动它们全部,中间没有任何界面挡路。