While the models load, an empty picture area says the run is going instead of asking for a run. With steps on, the last step stays up until the saved picture has loaded, so it no longer blinks out. Between a tiled upscale's split and stitch (and for Ultimate SD Upscale) the step pictures are tiles: the whole picture stays and the chip says "upscaling in tiles".
329 lines
13 KiB
JavaScript
329 lines
13 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
IDLE,
|
|
consumersOf,
|
|
drawsTiles,
|
|
feedsInto,
|
|
nextRunState,
|
|
resultRank,
|
|
stepFraction,
|
|
stepText,
|
|
stoppedText,
|
|
upstreamIds,
|
|
} from "../js/shared/step_preview_math.mjs";
|
|
|
|
function play(events, state = IDLE) {
|
|
return events.reduce((run, event) => nextRunState(run, event), state);
|
|
}
|
|
|
|
// A small graph: nodes by id, links as a Map, each node's inputs naming a link.
|
|
function makeGraph(nodes, links) {
|
|
const graph = {
|
|
links: new Map(links.map(([id, origin, target]) => [id, { id, origin_id: origin, target_id: target }])),
|
|
getNodeById: (id) => graph.nodes.find((node) => String(node.id) === String(id)) ?? null,
|
|
nodes: [],
|
|
};
|
|
graph.nodes = nodes.map(({ id, title, inputs = [], subgraph = null }) => ({
|
|
id,
|
|
title,
|
|
graph,
|
|
subgraph,
|
|
inputs: inputs.map((link) => ({ link })),
|
|
}));
|
|
return graph;
|
|
}
|
|
|
|
test("a run starts, shows its steps and counts them", () => {
|
|
const run = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
{ type: "progress", promptId: "p1", node: "7", value: 1, max: 8 },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:2" },
|
|
{ type: "progress", promptId: "p1", node: "7", value: 2, max: 8 },
|
|
]);
|
|
assert.equal(run.status, "running");
|
|
assert.equal(run.maker, "7");
|
|
assert.equal(run.url, "blob:2");
|
|
assert.equal(stepText(run), "step 2 of 8");
|
|
assert.equal(stepFraction(run), 0.25);
|
|
});
|
|
|
|
test("the count belongs to the node making the picture", () => {
|
|
const run = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "progress", promptId: "p1", node: "3", value: 5, max: 20 },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
{ type: "progress", promptId: "p1", node: "3", value: 6, max: 20 },
|
|
]);
|
|
assert.equal(run.maker, "7");
|
|
assert.equal(stepText(run), "starting");
|
|
});
|
|
|
|
test("a second sampler in the same run starts its own count", () => {
|
|
const run = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
{ type: "progress", promptId: "p1", node: "7", value: 8, max: 8 },
|
|
{ type: "picture", promptId: "p1", maker: "12:4", url: "blob:2" },
|
|
]);
|
|
assert.equal(run.maker, "12:4");
|
|
assert.equal(run.value, 0);
|
|
assert.equal(run.max, 0);
|
|
});
|
|
|
|
test("a stop keeps the last picture and says where it stopped", () => {
|
|
const run = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
{ type: "progress", promptId: "p1", node: "7", value: 3, max: 8 },
|
|
{ type: "end", promptId: "p1", kind: "interrupted" },
|
|
]);
|
|
assert.equal(run.status, "stopped");
|
|
assert.equal(run.url, "blob:1");
|
|
assert.equal(stoppedText(run), "stopped at step 3 of 8");
|
|
assert.equal(stoppedText(run, { saves: true }), "stopped at step 3 of 8 · nothing saved");
|
|
assert.equal(stoppedText({ ...run, max: 0 }), "stopped");
|
|
});
|
|
|
|
test("a run ends as done or failed", () => {
|
|
const started = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
]);
|
|
assert.equal(nextRunState(started, { type: "end", promptId: "p1", kind: "success" }).status, "done");
|
|
assert.equal(nextRunState(started, { type: "end", promptId: "p1", kind: "error" }).status, "failed");
|
|
});
|
|
|
|
test("another run's end leaves this one running", () => {
|
|
const started = play([{ type: "start", promptId: "p1" }]);
|
|
assert.equal(nextRunState(started, { type: "end", promptId: "p2", kind: "success" }), started);
|
|
});
|
|
|
|
test("a late picture after the run ended changes nothing", () => {
|
|
const stopped = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
{ type: "end", promptId: "p1", kind: "interrupted" },
|
|
]);
|
|
assert.equal(nextRunState(stopped, { type: "picture", promptId: "p1", maker: "7", url: "blob:9" }), stopped);
|
|
});
|
|
|
|
test("a picture from a new run starts that run without a start event", () => {
|
|
const done = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
{ type: "end", promptId: "p1", kind: "success" },
|
|
]);
|
|
const next = nextRunState(done, { type: "picture", promptId: "p2", maker: "7", url: "blob:2" });
|
|
assert.equal(next.status, "running");
|
|
assert.equal(next.promptId, "p2");
|
|
assert.equal(next.value, 0);
|
|
});
|
|
|
|
test("events that carry nothing leave the state alone", () => {
|
|
const started = play([{ type: "start", promptId: "p1" }]);
|
|
assert.equal(nextRunState(started, { type: "picture", promptId: "p1", maker: "7" }), started);
|
|
assert.equal(nextRunState(started, { type: "progress", promptId: "p1", node: "7", value: "x", max: 8 }), started);
|
|
assert.equal(nextRunState(started, { type: "unknown" }), started);
|
|
assert.equal(nextRunState(undefined, null), IDLE);
|
|
assert.equal(nextRunState(IDLE, { type: "progress", node: "7", value: 1, max: 2 }), IDLE);
|
|
});
|
|
|
|
test("the step text never runs past the end", () => {
|
|
assert.equal(stepText({ value: 9, max: 8 }), "step 8 of 8");
|
|
assert.equal(stepText({ value: -1, max: 8 }), "step 0 of 8");
|
|
assert.equal(stepFraction({ value: 9, max: 8 }), 1);
|
|
assert.equal(stepFraction(IDLE), 0);
|
|
});
|
|
|
|
test("the finished picture replaces the last step once the run saves it", () => {
|
|
const steps = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:8" },
|
|
{ type: "progress", promptId: "p1", node: "7", value: 8, max: 8 },
|
|
]);
|
|
const saved = nextRunState(steps, { type: "result", promptId: "p1", url: "/view?saved", rank: 2 });
|
|
assert.equal(saved.result, "/view?saved");
|
|
assert.equal(stepText(saved), "step 8 of 8");
|
|
// It stays when the run ends.
|
|
assert.equal(nextRunState(saved, { type: "end", promptId: "p1", kind: "success" }).result, "/view?saved");
|
|
});
|
|
|
|
test("a picture that stands less for the result never replaces a better one", () => {
|
|
const saved = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
{ type: "result", promptId: "p1", url: "/view?save", rank: 2 },
|
|
]);
|
|
assert.equal(nextRunState(saved, { type: "result", promptId: "p1", url: "/view?compare", rank: 0 }), saved);
|
|
assert.equal(nextRunState(saved, { type: "result", promptId: "p1", url: "/view?again", rank: 2 }).result, "/view?again");
|
|
});
|
|
|
|
test("new steps after a result show the steps again", () => {
|
|
const run = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:1" },
|
|
{ type: "result", promptId: "p1", url: "/view?first", rank: 1 },
|
|
{ type: "picture", promptId: "p1", maker: "12:4", url: "blob:2" },
|
|
]);
|
|
assert.equal(run.result, null);
|
|
assert.equal(run.url, "blob:2");
|
|
});
|
|
|
|
test("a result from another run, or with no picture, is left out", () => {
|
|
const started = play([{ type: "start", promptId: "p1" }]);
|
|
assert.equal(nextRunState(started, { type: "result", promptId: "p2", url: "/view?x", rank: 2 }), started);
|
|
assert.equal(nextRunState(started, { type: "result", promptId: "p1", rank: 2 }), started);
|
|
assert.equal(nextRunState(IDLE, { type: "result", promptId: "p1", url: "/view?x", rank: 2 }), IDLE);
|
|
});
|
|
|
|
test("save and preview nodes fed by the sampler stand best for the result", () => {
|
|
assert.equal(resultRank("AUSBOSS_NODES_SaveImage", true), 2);
|
|
assert.equal(resultRank("SaveImage", false), 1);
|
|
assert.equal(resultRank("PreviewImage", true), 2);
|
|
assert.equal(resultRank("AUSBOSS_NODES_ImageCompare", true), 0);
|
|
assert.equal(resultRank(undefined, true), 0);
|
|
});
|
|
|
|
test("the walk back through the inputs finds every node upstream, loops included", () => {
|
|
// 1 -> 2 -> 3 (save), 2 also reads 3's output back (a loop must not hang)
|
|
const graph = makeGraph(
|
|
[
|
|
{ id: 1, title: "loader" },
|
|
{ id: 2, title: "sampler", inputs: [10, 30] },
|
|
{ id: 3, title: "save", inputs: [20] },
|
|
],
|
|
[
|
|
[10, 1, 2],
|
|
[20, 2, 3],
|
|
[30, 3, 2],
|
|
],
|
|
);
|
|
const { ids, outside } = upstreamIds(graph, graph.getNodeById(3));
|
|
assert.deepEqual([...ids].sort(), ["1", "2", "3"]);
|
|
assert.equal(outside, false);
|
|
});
|
|
|
|
test("a save node shows the steps of the sampler that feeds it", () => {
|
|
// Two branches: 1 -> 2 -> 3 (save A) and 4 -> 5 (save B)
|
|
const root = makeGraph(
|
|
[
|
|
{ id: 1, title: "sampler A" },
|
|
{ id: 2, title: "decode A", inputs: [10] },
|
|
{ id: 3, title: "Save A", inputs: [20] },
|
|
{ id: 4, title: "sampler B" },
|
|
{ id: 5, title: "Save B", inputs: [40] },
|
|
],
|
|
[
|
|
[10, 1, 2],
|
|
[20, 2, 3],
|
|
[40, 4, 5],
|
|
],
|
|
);
|
|
const saveA = root.getNodeById(3);
|
|
const saveB = root.getNodeById(5);
|
|
assert.equal(feedsInto(root, saveA, "1"), true);
|
|
assert.equal(feedsInto(root, saveB, "1"), false);
|
|
assert.deepEqual(consumersOf(root, [saveA, saveB], "4"), [saveB]);
|
|
// A maker that feeds neither (a wireless set/get pair) shows on both.
|
|
assert.deepEqual(consumersOf(root, [saveA, saveB], "9"), [saveA, saveB]);
|
|
assert.deepEqual(consumersOf(root, [], "1"), []);
|
|
});
|
|
|
|
test("a sampler inside a box counts as the box", () => {
|
|
const inner = makeGraph([{ id: 4, title: "KSampler" }], []);
|
|
const root = makeGraph(
|
|
[
|
|
{ id: 12, title: "Generate", subgraph: inner },
|
|
{ id: 13, title: "Upscale", subgraph: makeGraph([], []) },
|
|
{ id: 3, title: "Save", inputs: [20] },
|
|
],
|
|
[[20, 12, 3]],
|
|
);
|
|
const save = root.getNodeById(3);
|
|
assert.equal(feedsInto(root, save, "12:4"), true);
|
|
assert.equal(feedsInto(root, save, "13:4"), false);
|
|
});
|
|
|
|
test("a save node in a box the sampler is outside of shows the steps", () => {
|
|
const inner = makeGraph([{ id: 2, title: "Save" }], []);
|
|
const root = makeGraph(
|
|
[
|
|
{ id: 7, title: "KSampler" },
|
|
{ id: 12, title: "Finish", subgraph: inner },
|
|
],
|
|
[],
|
|
);
|
|
const save = inner.getNodeById(2);
|
|
assert.equal(feedsInto(root, save, "7"), true);
|
|
assert.equal(feedsInto(root, null, "7"), false);
|
|
assert.equal(feedsInto(root, save, null), false);
|
|
});
|
|
|
|
test("a tiled upscale's tiles never replace the whole picture", () => {
|
|
const before = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:whole" },
|
|
{ type: "progress", promptId: "p1", node: "7", value: 8, max: 8 },
|
|
]);
|
|
assert.equal(before.whole, true);
|
|
// The split runs: the whole picture stays, and the chip says what is going on.
|
|
const split = nextRunState(before, { type: "tiles", promptId: "p1", on: true });
|
|
assert.equal(split.url, "blob:whole");
|
|
assert.equal(stepText(split), "upscaling in tiles");
|
|
const tile = play([
|
|
{ type: "picture", promptId: "p1", maker: "20:5", url: "blob:tile1" },
|
|
{ type: "progress", promptId: "p1", node: "20:5", value: 3, max: 8 },
|
|
], split);
|
|
assert.equal(tile.url, "blob:whole");
|
|
assert.equal(tile.maker, "20:5");
|
|
assert.equal(stepText(tile), "upscaling in tiles · step 3 of 8");
|
|
// The stitch closes the window; a later sampler's picture shows again.
|
|
const stitched = nextRunState(tile, { type: "tiles", promptId: "p1", on: false });
|
|
assert.equal(stitched.tiling, false);
|
|
const after = nextRunState(stitched, { type: "picture", promptId: "p1", maker: "30", url: "blob:after" });
|
|
assert.equal(after.url, "blob:after");
|
|
assert.equal(after.tilePass, false);
|
|
assert.equal(stepText(after), "starting");
|
|
});
|
|
|
|
test("tiles show when there is no whole picture yet, and say they are tiles", () => {
|
|
const run = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "tiles", promptId: "p1", on: true },
|
|
{ type: "picture", promptId: "p1", maker: "5", url: "blob:tile1" },
|
|
{ type: "picture", promptId: "p1", maker: "5", url: "blob:tile2" },
|
|
{ type: "progress", promptId: "p1", node: "5", value: 2, max: 4 },
|
|
]);
|
|
assert.equal(run.url, "blob:tile2");
|
|
assert.equal(run.whole, false);
|
|
assert.equal(stepText(run), "upscaling in tiles · step 2 of 4");
|
|
});
|
|
|
|
test("a node that samples its own tiles counts as tiles", () => {
|
|
const run = play([
|
|
{ type: "start", promptId: "p1" },
|
|
{ type: "picture", promptId: "p1", maker: "7", url: "blob:whole" },
|
|
{ type: "picture", promptId: "p1", maker: "9", url: "blob:tile", tile: true },
|
|
]);
|
|
assert.equal(run.url, "blob:whole");
|
|
assert.equal(run.tilePass, true);
|
|
assert.equal(drawsTiles("UltimateSDUpscale"), true);
|
|
assert.equal(drawsTiles("UltimateSDUpscaleNoUpscale"), true);
|
|
assert.equal(drawsTiles("KSampler"), false);
|
|
assert.equal(drawsTiles(undefined), false);
|
|
});
|
|
|
|
test("tile events from another run or after the end change nothing", () => {
|
|
const started = play([{ type: "start", promptId: "p1" }]);
|
|
assert.equal(nextRunState(started, { type: "tiles", promptId: "p2", on: true }), started);
|
|
assert.equal(nextRunState(started, { type: "tiles", promptId: "p1", on: false }), started);
|
|
const done = nextRunState(started, { type: "end", promptId: "p1", kind: "success" });
|
|
assert.equal(nextRunState(done, { type: "tiles", promptId: "p1", on: true }), done);
|
|
// A new run forgets the last one's tiles.
|
|
const tiling = nextRunState(started, { type: "tiles", promptId: "p1", on: true });
|
|
assert.equal(nextRunState(tiling, { type: "start", promptId: "p3" }).tiling, false);
|
|
});
|