The preview dropped 11 instance keys the engine reads (constraint reach, cell/exception overrides, derivation branch edits, hold seeds), so it could show picks the run never makes. A test now checks the whitelist against the engine handlers.
Video tutorials
The tutorial videos, recorded by Watchwright. Each test in tests/ is one tutorial and one video. A test clicks through the real ComfyUI canvas and manager like a person would: it adds nodes from the search box, drags wires, types templates and presses Run. While it does, the video shows:
- a steady pointer that moves like a hand, with a ring where it clicks;
- a caption for each narration line;
- callout boxes around the part being talked about.
After the take, render.mjs adds a text-to-speech voice and writes the finished MP4, with subtitles and chapters.
| File | What it does |
|---|---|
pack.ts |
The Watchwright pack. It records in Chromium at 1920 × 1080 in dark mode, keeps a video of every take, and puts your library back afterwards (../bench/lifecycle.ts). |
tutorial.ts |
The presenter (Tutor): say, click, drag, type, fill, scrollTo, go, callout, addNode, wire, pickModules, run, fastForward, plus showcase for rendering real images. |
cursor.js |
The page script that draws the pointer, captions, callouts and the sync mark. |
library.json |
The library every take starts from (the example workflows' rows). Viewers get the same file as WP Tutorials library.json next to the videos; T01 shows how to import it into an empty library. |
voice/speak.py |
Speaks every t.say("…") line with Kokoro-82M into voice/cache/ and writes voice/lines.json. |
render.mjs |
Turns a take into out/<id>-<title>.mp4, .srt and .chapters.txt. |
Make a video
- Voice. Run
pip install kokoro-onnx soundfileonce, thenpython tutorials/voice/speak.py. The first run downloads the model (~350 MB) intovoice/models/. It speaks only lines that are new or changed.--voicepicks another voice (af_heartis the default; others areaf_bella,am_michael,bf_emmaandbm_george). Run it before recording, because each line's length decides how long the hands wait for the voice. - Record. Start ComfyUI with this extension on port 8189, or a clean one (see below). Add this folder as a pack in Watchwright, then start a run with the tutorials you want. Each take's video and
files/cues.jsonland in its attempt folder. - Render. Run
node tutorials/render.mjs <attempt folder>. It needs a full ffmpeg with libx264 and aac onPATH(or pass--ffmpeg <path>); Playwright's own ffmpeg can't do it. The finished files go totutorials/out/.
Record on a clean ComfyUI
Other custom nodes add their own buttons, menus, badges and node colours to the canvas, and some change how it behaves, so the videos look different for everyone. Record on a second ComfyUI that loads only this extension. It uses the same install and models, but its own user folder, so its settings, open tabs and library are separate from the ComfyUI you work in. Your own ComfyUI keeps running.
# from the ComfyUI folder; the user folder must already exist; any empty folder will do
python main.py --port 8190 --disable-all-custom-nodes --whitelist-custom-nodes ComfyUI-Wildcard-Pipeline ComfyUI-Easy-Use --user-directory <empty folder>
The whitelist names are folder names in custom_nodes. ComfyUI-Easy-Use is only there for its Show Any node, which lists every frame of a loop (A05, A16); ComfyUI's Preview as Text shows one. Then start Watchwright with WP_TUTORIALS_URL=http://127.0.0.1:8190/wp/ in its environment, so the pack records that ComfyUI instead of the one on 8189.
The pack's reset setting (on by default) loads library.json before the first tutorial, so every take shows the same items. Your library is backed up first and put back when the run ends, the same way the bench packs do it (see ../bench/README.md › Putting your library back).
Write a tutorial
import { tutorial } from "../tutorial.js";
tutorial("T07", { title: "Your next tutorial" }, async (t) => {
await t.h.step("Add a Context node", async () => {
await t.say("Double-click the canvas and search for WP Context.");
const ctx = await t.addNode("WP Context", { x: 520, y: 170 });
await t.callout("ctx", t.node(ctx), "Your lists live here");
await t.waitVoice();
await t.uncallout("ctx");
});
});
t.say(line)shows the caption and returns right away, so the hands work while the voice speaks. The nextsaywaits until the line has been spoken. Uset.waitVoice()to wait on purpose, for example to keep a callout up until its line ends. Each line must be a plain string literal sospeak.pycan find it. In a line,$herois read as "hero" and "WP" as "W P".- Every
t.h.stepbecomes a chapter. t.addNode(search, {x, y})drops the node centred on that point. Leave the bottom 140 px free for the caption.- Wrap a long wait in
t.fastForward(() => …), such as a model run. The video plays that part 6× faster. addNodesets new seeds to fixed, so what the video shows matches what the narration says.
Pack settings
| Setting | Default | What it does |
|---|---|---|
reset |
on | Loads library.json before the first tutorial. |
showcase |
off | Ends each tutorial by rendering real images from its prompt (needs a GPU and the checkpoint below). Leave it off where no model is installed. |
checkpoint |
waiIllustriousSDXL_v170 |
The checkpoint the showcase uses, without the extension. A missing one stops the take and lists the ones ComfyUI has. |
steps, cfg |
24, 5 | The showcase sampler's steps and CFG. |
More helpers
t.go("/wp/wildcards")opens a manager page, ort.go("/")for an empty canvas. The load plays fast and the caption stays up.t.fill(locator, text)clicks a field, clears it and types, the way a person would.t.scrollTo(locator)wheel-scrolls the page until the target is near the top.t.clickscrolls by itself when its target is off screen or under the caption.t.pickModules(contextId, ["Hero", "Setting"])opens a Context node's picker, scrolls to each row and ticks it (in that order), then adds them.if (showcasing(t)) await showcase(t, { from: assembler, preview, chain })builds a folded checkpoint → sampler chain fed by the Assembler, adds a preview and runs it a few times, fast-forwarded.between(i)runs before each run after the first (change a seed, flip a toggle).
Notes
- The screencast records CSS pixels, and the device scale factor doesn't change that. So the video is the size of the viewport: 1920 × 1080.
- Playwright's own action titles and fading pointer are off (
showActions: false), becausecursor.jsdraws a pointer that stays on screen. - Record in Chromium. Firefox's screencast drops frames.