* feat(tutorials): in-depth Context Loop, Context Injector and Image Filter videos N05 walks the whole Context Loop (frames vs batch size vs Run count, Edit frame chips, Override Context seed, strategies, the seeds panel, Ctrl/Alt click, values, a rule on $iteration, Seed List, Hold, Bypass loop). N04 takes the Context Injector apart and N12 the Image Filter (labels, prompt edits with clip, the mask painter, Compare, Zoom, Send to Loop, the node's settings). The old Nodes videos move to their new numbers (Debug N03, Label & Grid N13, Note N14, Bookmark N15); N12 replaces the old N01. * fix(tutorials): tick both rows in T03's bulk edit The selection bar slides in after the first tick and moves the rows, so the second click could land where its row used to be. Tick one row at a time and check each one took. * fix(tutorials): scroll T05's rules into view before boxing them scrollTo now falls back to scrolling the part's own scrolling box when the wheel doesn't move it, and T05 scrolls each branch (and the second action) to the upper screen before its callout, so the boxes are on screen. * fix(tutorials): narrate T06's canvas build so it plays at normal speed The two builds had no line, so the render squeezed them to 4.5x and the hand looked rushed. A line over each keeps them at normal speed. * fix(tutorials): a hand at work plays at most 2x in the render The Tutor now records when the hand clicks, drags, types or presses a key (cues.json acts), and render.mjs speeds a silent stretch with hand work up by at most 2x instead of up to 8x, so builds don't look rushed. The ten longest silent builds (T04, A04, A05, A06, A09, A10, A12, L03 and two in N14) get a line, so they play at normal speed. * fix(tutorials): give N04's Injector room beside the stand-in nodes The Injector landed on the first text box and covered its output, so the first wire never connected. The stand-ins sit further right, the Injector further right still, and the canvas slides left before the Context step. * fix(tutorials): keep both ends of N12's new wires on screen The mask wire was dragged from the filter to a node that had scrolled off the top, so it dropped on empty canvas. The canvas now fits the nodes of each wire into the free part of the screen first, and the new Set Latent Noise Mask goes under the refine sampler. * fix(tutorials): N05 and N04 fixes from the first takes N05: zoom in on the Seed List while it's talked about (it sat under the caption), and drop the callout labels that covered the frame chips and the template. N04: narrate the Context / Assembler / preview build (36 s of silence) and move the hand off the rows so the globe tooltip closes. * fix(tutorials): paint N12's mask on the painter's canvas In paint mode the mask painter replaces the detail image, so the strokes aimed at an image that was no longer there. * fix(tutorials): a single click doesn't hold a long wait at 2x A Run click followed by a long wait counted as hand work, so the wait played at 2x instead of being squeezed. A stretch now needs two or more hand actions to be capped. * fix(tutorials): tuck N12's picker away with its minus button Escape in Detail goes back to the grid instead of tucking the picker away, so the waiting pill never showed. * fix(tutorials): box N12's two extra slots as a pair The callout took one locator matching both slot dots, which strict mode refuses. * fix(image-filter): keep the picker's keys after a view change Switching mode or leaving the painter removed the focused control and dropped focus to body, so the picker's keys went to the canvas bookmarks. Focus goes back to the picker. N12 hands the keys back before Compare and Zoom. * fix(tutorials): L05 waits for Role's save to store a history entry * fix(tutorials): a few clicks across a long wait don't cap the squeeze * fix(tutorials): review 2026-11-11 first half (lines over empty canvas, late boxes, N12 framing) * fix(tutorials): review 2026-11-11 A09-A19 findings * fix(tutorials): review 2026-11-11 L03, L05, N14, E4 findings; jumpTo keeps tall notes above the caption * fix(tutorials): A06 and T05 build their Context before the line * fix(tutorials): N14 keeps the label's peek open through its line * fix(tutorials): N12 boxes the extra slot rows; callouts fall back to the raw box * fix(tutorials): N14 hovers the label before its line * fix(tutorials): N14 keeps the hover card up while its line plays
129 lines
7.1 KiB
TypeScript
129 lines
7.1 KiB
TypeScript
// Advanced 15: model-aware prompts. The showcase model's loader (Flux 3 Klein by default, or the
|
|
// `checkpoint` param) feeds WP Model Info, which writes $model_family, $model_variant and $model_name,
|
|
// all internal, so the Assembler never prints them. A variant rule of our own names Klein. A Context
|
|
// after it rolls Role, Setting and Quality per model, whose Pool is filtered by $model_variant, so only
|
|
// the loaded model's quality words roll. Run with Preview as Text. Needs the model files, so it only
|
|
// records where they exist.
|
|
import { modelFile, tutorial } from "../tutorial.js";
|
|
|
|
tutorial(
|
|
"A15",
|
|
{
|
|
title: "Model-aware prompts",
|
|
description: "Let WP Model Info read the loaded model, and make a list keep only the options for that model with a Pool filtered by $model_variant.",
|
|
learn: "$model_family, $model_variant and $model_name, your own variant rules, and a Pool filtered by a variable.",
|
|
},
|
|
async (t) => {
|
|
const page = t.page;
|
|
|
|
const nodes = await t.h.step("Read the model", async () => {
|
|
await t.go("/");
|
|
await t.say("One pack rarely suits every model. Pony wants its score tags, Illustrious its own quality words, Flux plain sentences. WP Model Info tells the pipeline which model is loaded.");
|
|
const unet = String(t.params.unet ?? "");
|
|
const load = unet ? await t.addNode("Load Diffusion Model", { x: 200, y: 200 }) : await t.addNode("Load Checkpoint", { x: 200, y: 200 });
|
|
const [widget, file] = unet
|
|
? ["unet_name", await modelFile(t, "UNETLoader", "unet_name", unet, "diffusion_models")]
|
|
: ["ckpt_name", await modelFile(t, "CheckpointLoaderSimple", "ckpt_name", String(t.params.checkpoint ?? ""), "checkpoints")];
|
|
await t.page.evaluate(
|
|
([id, w, n]) => {
|
|
const node = window.app?.graph?.getNodeById(Number(id)) as { widgets?: { name: string; value: unknown }[] } | null | undefined;
|
|
const x = node?.widgets?.find((v) => v.name === w);
|
|
if (x) x.value = n;
|
|
window.app?.canvas?.setDirty(true, true);
|
|
},
|
|
[load, widget, file] as const,
|
|
);
|
|
await t.beat(600);
|
|
const mi = await t.addNode("WP Model Info", { x: 640, y: 200 });
|
|
await t.wire(load, "MODEL", mi, "model");
|
|
await t.beat(800);
|
|
await t.waitVoice();
|
|
await t.callout("mi", t.node(mi), "Family, variant and file name", 4);
|
|
await t.say("Wire the model into it. It writes three variables: model family, from the model itself, model variant, from the file name, and model name.");
|
|
await t.waitVoice();
|
|
await t.uncallout("mi");
|
|
const internal = await t.node(mi).locator('[data-test^="mi-internal-"]').all();
|
|
await t.callout("int", internal, "Internal: rules read them, prompts don't", 4);
|
|
await t.say("All three are internal: rules, constraints and pools read them, but the Assembler never prints them, so they stay out of your prompt.");
|
|
await t.waitVoice();
|
|
await t.uncallout("int");
|
|
return { load, mi };
|
|
});
|
|
|
|
await t.h.step("Your own variant", async () => {
|
|
await t.say("The variant comes from rules that look for words in the file name. Pony and Illustrious are built in. Add one for Klein: the variant klein, when the name says klein.");
|
|
await t.click(t.node(nodes.mi).locator('[data-test="mi-add-rule"]'));
|
|
await t.beat(500);
|
|
const rule = t.node(nodes.mi).locator('[data-test^="mi-rule-"]').filter({ has: page.locator('input[aria-label="variant"]') }).last();
|
|
await t.fill(rule.locator('input[aria-label="variant"]'), "klein", { cps: 12 });
|
|
await t.press("Tab");
|
|
await t.fill(rule.locator('input[aria-label="pattern"]'), "klein", { cps: 12 });
|
|
await t.press("Tab");
|
|
await t.beat(700);
|
|
await t.callout("variant", t.node(nodes.mi).locator('[data-test="mi-value-variant"]'), "Read from the file name", 5);
|
|
await t.waitVoice();
|
|
await t.uncallout("variant");
|
|
});
|
|
|
|
const ctx = await t.h.step("A list that follows it", async () => {
|
|
await t.say("Quality per model has one line of quality words for each model, tagged pony, illustrious or klein.");
|
|
const ctx = await t.addNode("WP Context", { x: 1330, y: 200 });
|
|
await t.wire(nodes.mi, "context", ctx, "upstream");
|
|
await t.say("Put it in a Context after Model Info, with Role and Setting.");
|
|
await t.pickModules(ctx, ["Role", "Setting", "Quality per model"]);
|
|
await t.waitVoice();
|
|
await t.say("Right-click it, press Edit, and in its Pool, under By variable, pick model variant.");
|
|
await t.click(t.moduleRow(ctx, "Quality per model").locator(".wp-module-name"), { right: true });
|
|
await t.beat(600);
|
|
await t.click(page.getByRole("menuitem", { name: "Edit" }));
|
|
await t.beat(1000);
|
|
const modal = page.getByRole("dialog").last();
|
|
const collapse = modal.locator('[data-test="pool-collapse"]');
|
|
if ((await collapse.getAttribute("aria-expanded")) !== "true") {
|
|
await t.click(collapse);
|
|
await t.beat(500);
|
|
}
|
|
const vars = modal.locator('[data-test="pool-vars"]');
|
|
await t.scrollTo(vars, { at: 0.45 });
|
|
await t.click(vars.getByRole("combobox").first());
|
|
await t.beat(600);
|
|
await t.click(page.getByRole("option", { name: /\$model_variant/ }).first());
|
|
await t.beat(600);
|
|
await t.press("Escape");
|
|
await t.beat(400);
|
|
await t.callout("hint", modal.locator('[data-test="pool-vars-hint"]'), "Decided when the prompt runs", 6);
|
|
await t.waitVoice();
|
|
await t.say("When the prompt runs, the Pool keeps the options tagged with the variant's value. Options with no model tag would stay for every model. If the variant is empty, or no option carries it, the filter steps aside and WP Debug says so.");
|
|
await t.waitVoice();
|
|
await t.uncallout("hint");
|
|
await t.click(modal.locator('[data-test="wcm-save"]'));
|
|
await t.beat(900);
|
|
return ctx;
|
|
});
|
|
|
|
await t.h.step("Run it", async () => {
|
|
await t.say("Write the template, and run it. The model loads, and the prompt gets Klein's words.");
|
|
const asm = await t.addNode("WP Prompt Assembler", { x: 1330, y: 700 });
|
|
await t.wire(ctx, "context", asm, "context");
|
|
await t.click(t.node(asm).locator("textarea, [contenteditable='true']").first());
|
|
await t.type("A painting of $role $setting. $quality", { cps: 16 });
|
|
await t.beat(600);
|
|
const text = await t.addNode("Preview as Text", { x: 1740, y: 700 });
|
|
await t.wire(asm, "prompt", text, "source");
|
|
await t.run();
|
|
await t.beat(600);
|
|
// Both sit low on the screen, under the caption: bring them up to the middle.
|
|
await t.zoomOn(asm);
|
|
await t.callout("out", t.node(text), "Klein's quality words", 6);
|
|
await t.waitVoice();
|
|
await t.uncallout("out");
|
|
await t.say("With a Pony checkpoint loaded instead, the same Context would roll score tags. A reference can filter the same way: at Quality per model, colon, dollar model variant.");
|
|
await t.waitVoice();
|
|
});
|
|
|
|
await t.h.step("Outro", async () => {
|
|
await t.say("WP Model Info names the model, and a Pool filtered by its variant keeps the right words for it. Next: loops and seeds.");
|
|
});
|
|
},
|
|
);
|