Training: steps instead of tabs, walked with Back and Next; no way on to training with an empty set; Results leads back to the set and its tags
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MpnF7GwNGMbngU11wjym7P
This commit is contained in:
@@ -9,9 +9,9 @@ import { makeSession, srcKey } from "./state.js";
|
||||
|
||||
const VIDEO = /\.(mp4|webm|mov|mkv)$/i;
|
||||
|
||||
const TABS = [{ value: "data", label: "1 Your media" }, { value: "tags", label: "2 Describe" }, { value: "train", label: "3 Train" }, { value: "results", label: "4 Results" }];
|
||||
const TABS = [{ value: "data" }, { value: "tags" }, { value: "train" }, { value: "results" }]; // the steps, in order: Back and Next walk them
|
||||
const STEPS = {
|
||||
data: { title: "Pick what to learn from", sub: "Drop pictures and clips, choose from your Bin, or take results you rated. This step is optional: a set you made earlier is ready as it is." },
|
||||
data: { title: "Pick what to learn from", sub: "Drop pictures and clips, choose from your Bin, or take results you rated. A set you filled earlier can go straight on; an empty one cannot be trained." },
|
||||
tags: { title: "Describe each one", sub: "The words under each picture are what the LoRA learns to connect with it. Fix them by hand, or let the tagger suggest." },
|
||||
train: { title: "Train the LoRA", sub: "Choose how thorough it should be and press Start. Progress and previews appear in Results." },
|
||||
results: { title: "Results", sub: "Watch the training, look at the previews and download a LoRA, the final one or any step." },
|
||||
@@ -74,19 +74,23 @@ export default {
|
||||
train: () => trainStep({ app, session, go, ui }),
|
||||
results: () => resultsStep({ app, ui }),
|
||||
};
|
||||
function go(tab) { ui.tab = tab; if (tab === "results") loadRuns(); draw(); }
|
||||
function go(tab) { if (tab !== "data" && tab !== "results" && !((session.doc || {}).items || []).length) tab = "data"; ui.tab = tab; if (tab === "results") loadRuns(); draw(); }
|
||||
function draw() {
|
||||
if (!win) return;
|
||||
const tabs = c.segmented.md({ label: "Steps", options: TABS, value: ui.tab, onChange: go });
|
||||
const note = session.error ? c.inlineError.default({ text: session.error }) : null;
|
||||
const info = STEPS[ui.tab];
|
||||
body.set([tabs, c.label.section({ text: info.title }), c.hint.default({ text: info.sub }), note, steps[ui.tab]()].filter(Boolean));
|
||||
const order = TABS.map((t) => t.value), at = order.indexOf(ui.tab);
|
||||
const back = at ? c.button.sm({ label: ui.tab === "results" ? "Back to training" : "Back", tone: "ghost", onClick: () => go(order[at - 1]) }) : null;
|
||||
const skip = ui.tab === "data" ? c.button.sm({ label: "Skip to training", tone: "ghost", onClick: () => go("train") }) : null;
|
||||
const next = ui.tab === "data" ? c.button.sm({ label: "Next: tags", tone: "primary", disabled: !session.name, onClick: () => go("tags") })
|
||||
: ui.tab === "tags" ? c.button.sm({ label: "Next: train", tone: "primary", onClick: () => go("train") }) : null;
|
||||
foot = () => win && win.setFooter({ actions: [back, skip, next, ...(ui.tab === "train" ? ui.controls || [] : [])].filter(Boolean), note: ui.tab === "train" ? footNote(ui) : "" });
|
||||
const info = STEPS[ui.tab], order = TABS.map((t) => t.value), at = order.indexOf(ui.tab);
|
||||
const ready = ((session.doc || {}).items || []).length > 0; // nothing to learn from: no way on to training
|
||||
body.set([c.label.section({ text: `${at + 1} / ${order.length} · ${info.title}` }), c.hint.default({ text: info.sub }), note, steps[ui.tab]()].filter(Boolean));
|
||||
const results = (ui.tab === "data" || ui.tab === "train") ? c.button.sm({ label: "See results", tone: "ghost", title: "Earlier runs, the live graph and downloads", onClick: () => go("results") }) : null;
|
||||
const back = ui.tab === "results" ? c.button.sm({ label: "Back to training", tone: "ghost", onClick: () => go("train") }) // a run carries on while another set is made or tags are fixed
|
||||
: at ? c.button.sm({ label: "Back", tone: "ghost", onClick: () => go(order[at - 1]) }) : null;
|
||||
const skip = ui.tab === "data" ? c.button.sm({ label: "Skip to training", tone: "ghost", disabled: !ready, title: ready ? "" : "Add pictures or clips to the set first", onClick: () => go("train") }) : null;
|
||||
const next = ui.tab === "data" ? c.button.sm({ label: "Next: tags", tone: "primary", disabled: !ready, title: ready ? "" : "Add pictures or clips to the set first", onClick: () => go("tags") })
|
||||
: ui.tab === "tags" ? c.button.sm({ label: "Next: train", tone: "primary", disabled: !ready, onClick: () => go("train") }) : null;
|
||||
const elsewhere = ui.tab === "results" ? [c.button.sm({ label: "Prepare a set", tone: "ghost", title: "Make or fill a set; a training going on carries on", onClick: () => go("data") }),
|
||||
c.button.sm({ label: "Fix tags", tone: "ghost", title: "Correct the captions; a training going on carries on", onClick: () => go("tags") })] : [];
|
||||
const actions = [results, ...elsewhere, back, skip, next];
|
||||
foot = () => win && win.setFooter({ actions: [...actions, ...(ui.tab === "train" ? ui.controls || [] : [])].filter(Boolean), note: ui.tab === "train" ? footNote(ui) : "" });
|
||||
foot();
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ export function notice(ui, go) {
|
||||
const where = c.button.sm({ label: "Open Results", tone: "ghost", onClick: () => go("results") });
|
||||
if (run.error) return c.region.stack({ gap: "md", children: [c.inlineError.default({ text: run.error }), where] });
|
||||
const text = { done: "Training finished.", stopped: "Training stopped.", paused: `Training paused at step ${run.step || 0}.`, previewing: "Making a preview." }[run.state] || "Training started.";
|
||||
return c.region.stack({ gap: "md", children: [c.banner.info({ text: `${aboutControls(run.message) ? `${run.message}. ` : ""}${text} Check graphs and previews in the Results tab.` }), where] });
|
||||
return c.region.stack({ gap: "md", children: [c.banner.info({ text: `${aboutControls(run.message) ? `${run.message}. ` : ""}${text} Graphs and previews are on the Results step.` }), where] });
|
||||
}
|
||||
|
||||
/** Progress, the message, and the loss graph. */
|
||||
|
||||
@@ -47,7 +47,7 @@ const open = async (f) => {
|
||||
return { host, button, off };
|
||||
};
|
||||
const text = () => document.body.textContent;
|
||||
const tab = (label) => [...document.querySelectorAll("button, [role=tab]")].find((b) => b.textContent.trim().endsWith(label));
|
||||
const tab = (label) => click(label === "Results" ? "See results" : label === "Train" ? "Back to training" : label);
|
||||
const click = (label, root = document) => [...root.querySelectorAll("button")].find((b) => b.textContent.trim() === label);
|
||||
|
||||
test("captions: tags are trimmed and unique, counts are per caption, bulk edits only the chosen", () => {
|
||||
@@ -202,7 +202,7 @@ test("a run in progress draws its loss, then reports the saved file; Stop asks t
|
||||
await wait(20);
|
||||
click("Start training").click();
|
||||
await wait(1200);
|
||||
assert.ok(text().includes("Check graphs and previews in the Results tab"), "Train only says where to look");
|
||||
assert.ok(text().includes("Graphs and previews are on the Results step"), "Train only says where to look");
|
||||
assert.ok(!document.querySelector(".cx-chart"), "the graph is not on the Train tab");
|
||||
tab("Results").click();
|
||||
await wait(20);
|
||||
@@ -525,3 +525,21 @@ test("Data step: results whose rating may be from another video are marked, not
|
||||
assert.ok(text().includes('1 marked "old rating?"'), "the count is shown, and nothing is hidden");
|
||||
off();
|
||||
});
|
||||
|
||||
test("Steps: no tabs; with no media the way on to training is closed, and Results leads back to the set and its tags", async () => {
|
||||
const f = fake();
|
||||
f.doc.items = [];
|
||||
const { button, off } = await open(f);
|
||||
button.click();
|
||||
await wait(60);
|
||||
assert.ok(!document.querySelector("[role=tab], [role=tablist]"), "no tab strip");
|
||||
assert.ok(text().includes("1 / 4"), "the step says where it is");
|
||||
assert.ok(click("Next: tags").disabled && click("Skip to training").disabled, "no media, no training");
|
||||
click("See results").click();
|
||||
await wait(40);
|
||||
assert.ok(click("Prepare a set") && click("Fix tags"), "back to the set or its tags while a run goes on");
|
||||
click("Fix tags").click();
|
||||
await wait(40);
|
||||
assert.ok(text().includes("1 / 4"), "an empty set sends you to add media first");
|
||||
off();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user