Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
169 lines
12 KiB
JavaScript
169 lines
12 KiB
JavaScript
// Refinement & Taste: the refinement keys FunUI has learned from your ratings. Keys are files: carry one to another machine
|
|
// as a zip, or delete one to start that taste over.
|
|
import { composer as c } from "../../composer/composer.js";
|
|
|
|
/** Every clip of a project that carries a rating: the run ids its renders and earlier takes came from (what the learned rows
|
|
* are filed under) and how many scenes and takes hold a rating label. */
|
|
export function ratedIn(project) {
|
|
const ids = new Set();
|
|
let labelled = 0;
|
|
for (const r of Object.values((project && project.scene_renders) || {})) if (r && r.promptId) ids.add(r.promptId);
|
|
for (const takes of Object.values((project && project.scene_variants) || {})) for (const t of takes || []) {
|
|
if (t && t.promptId) ids.add(t.promptId);
|
|
if (t && t.rating) labelled++;
|
|
}
|
|
for (const s of (project && project.scenes) || []) if (s.rating) labelled++;
|
|
return { ids: [...ids], labelled };
|
|
}
|
|
|
|
export const taste = (app) => function mount() {
|
|
const page = c.region.stack({ gap: "md" });
|
|
const tell = (text) => c.toast.warn({ text });
|
|
let keys = null, error = "";
|
|
|
|
async function refresh() {
|
|
try { keys = (await app.api.tasteKeys()).keys || []; error = ""; } catch (err) { keys = []; error = err.message; }
|
|
draw();
|
|
}
|
|
const remove = async (name) => {
|
|
if (!(await c.modal.dialogue({ title: "Delete refinement key", message: `Delete “${name}” and everything it learned? This cannot be undone.`, tone: "danger", confirmLabel: "Delete" }).result)) return;
|
|
try { await app.api.deleteTasteKey(name); } catch (err) { tell(err.message); }
|
|
refresh();
|
|
};
|
|
const exportKey = async (name) => { // fetched first, so a refusal is said instead of navigating the page to its JSON
|
|
try {
|
|
const r = await fetch(app.api.tasteKeyUrl(name));
|
|
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).why || `The server refused (${r.status}).`);
|
|
app.dom.saveBlob(await r.blob(), `${name}.zip`);
|
|
} catch (err) { tell(err.message); }
|
|
};
|
|
async function bring([file]) {
|
|
const name = await c.modal.prompt({ title: "Import refinement key", label: "Name", value: file.name.replace(/\.zip$/i, ""), confirmLabel: "Import" }).result;
|
|
if (!name || !name.trim()) return;
|
|
try {
|
|
let r = await app.api.importTasteKey(name.trim(), file, false);
|
|
if (r.exists && await c.modal.dialogue({ title: "Replace key", message: `A key named “${name.trim()}” already exists. Replace it?`, tone: "danger", confirmLabel: "Replace" }).result) r = await app.api.importTasteKey(name.trim(), file, true);
|
|
if (!r.exists) c.toast.good({ text: `Imported “${name.trim()}”.` });
|
|
} catch (err) { tell(err.message); }
|
|
refresh();
|
|
}
|
|
async function clearRatings() {
|
|
const open = app.project.project, { ids, labelled } = ratedIn(open);
|
|
if (!(await c.modal.dialogue({ title: "Clear saved ratings", message: `Forget what ${open.name ? `“${open.name}”` : "this project"} taught (${ids.length} rendered ${ids.length === 1 ? "clip" : "clips"}) and remove its rating labels? Other projects and your keys' other lessons stay. This cannot be undone.`, tone: "danger", confirmLabel: "Clear" }).result)) return;
|
|
let failed = "";
|
|
for (const id of ids) { try { await app.api.rateTaste(id, null, null); } catch (err) { failed = err.message; } }
|
|
app.project.edit((pr) => {
|
|
for (const s of pr.scenes) { s.rating = ""; s.rated = {}; }
|
|
for (const takes of Object.values(pr.scene_variants || {})) for (const t of takes || []) { t.rating = ""; t.rated = {}; }
|
|
return pr;
|
|
});
|
|
if (failed) tell(`Some ratings could not be cleared: ${failed}`);
|
|
else c.toast.good({ text: "Ratings cleared from this project." });
|
|
draw();
|
|
}
|
|
function draw() {
|
|
const open = app.project && app.project.project, rated = ratedIn(open);
|
|
page.set([
|
|
error ? c.banner.warn({ text: `Could not read the keys: ${error}` }) : null,
|
|
keys && !keys.length && !error ? c.emptyState.default({ icon: "✦", title: "No keys yet", hint: "A key appears once you rate a render with learning on." }) : null,
|
|
...(keys || []).map((name) => c.settingsRow.default({ label: name, hint: "Taste key: what your ratings taught under this name", control: c.toolbar.default({ items: [
|
|
c.button.sm({ label: "⤓ Export", tone: "ghost", onClick: () => exportKey(name) }),
|
|
c.button.sm({ label: "Delete", tone: "danger", onClick: () => remove(name) })] }) })),
|
|
c.label.section({ text: "This project" }),
|
|
c.settingsRow.default({ label: "Saved ratings", hint: open ? "Clear this project's ratings: what its clips taught your keys, and the rating labels on its scenes." : "Open a project first.",
|
|
control: c.button.sm({ label: "Clear", tone: "danger", disabled: !open || !(rated.ids.length || rated.labelled), onClick: clearRatings }) }),
|
|
c.label.section({ text: "Bring a key here" }),
|
|
c.dropzone.default({ label: "Drop or choose an exported key (.zip)", hint: "from another machine", accept: ".zip,application/zip", multiple: false, onFiles: bring }),
|
|
].filter(Boolean));
|
|
}
|
|
const exportInfluence = async (key) => {
|
|
try {
|
|
const r = await fetch(app.api.blockInfluenceExportUrl(key));
|
|
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).why || `The server refused (${r.status}).`);
|
|
app.dom.saveBlob(await r.blob(), `${key}.block_influence.pt`);
|
|
} catch (err) { tell(err.message); }
|
|
};
|
|
const clearInfluence = async (key) => {
|
|
if (!(await c.modal.dialogue({ title: "Clear block influence", message: `Clear everything recorded for “${key}”? This cannot be undone.`, tone: "danger", confirmLabel: "Clear" }).result)) return;
|
|
try { await app.api.clearBlockInfluence(key); } catch (err) { tell(err.message); }
|
|
refreshResearch();
|
|
};
|
|
// Block influence: research recording, read here. Its numbers say whether the four groups point apart; no clip changes.
|
|
const research = c.region.stack({ gap: "sm" });
|
|
let influence = null;
|
|
const pct = (x) => (x >= 0 ? "+" : "") + x.toFixed(2);
|
|
async function refreshResearch() {
|
|
try {
|
|
// Separately: a failed report must not hide the switch, or the user cannot turn recording off.
|
|
const state = await app.api.blockInfluence("default").catch((err) => ({ error: err.message }));
|
|
const four = await app.api.blockInfluenceGroups("default").catch((err) => ({ error: err.message }));
|
|
influence = { state: state.error ? null : state, four: four.error ? null : four, error: [state.error, four.error].filter(Boolean).join("; ") };
|
|
} catch (err) { influence = { state: null, four: null, error: err.message }; }
|
|
drawResearch();
|
|
}
|
|
function drawResearch() {
|
|
const rows = [c.label.section({ text: "Block influence (research)" })];
|
|
if (influence && influence.error) rows.push(c.banner.warn({ text: `Could not read the recording: ${influence.error}` }));
|
|
// The switch is always here, even when the report could not be read: a failed read must not take away the way to turn recording off.
|
|
rows.push(c.toggle.default({ label: "Record block influence", hint: "On: each run on a Taste key is measured, and kept once you rate it. Off: nothing is recorded.",
|
|
checked: influence && influence.state ? !!influence.state.enabled : false,
|
|
onChange: async (on) => { try { await app.api.setBlockInfluence(on); } catch (err) { tell(err.message); } refreshResearch(); } }));
|
|
if (influence && influence.state) {
|
|
const { state, four } = influence;
|
|
if (state.problem) rows.push(c.banner.warn({ text: state.problem }));
|
|
rows.push(c.settingsRow.default({ label: "Rated runs kept", hint: `Runs on this key that have a rating: ${state.runs}, of which ${state.skipped} had no usable picture rows`,
|
|
control: c.hint.default({ text: `${state.used} kept` }) }));
|
|
if (state.used) rows.push(c.settingsRow.default({ label: "Flatness", hint: "How evenly the blocks move the picture. Near 0 means every block moves it equally, so there is nothing to aim at.",
|
|
control: c.hint.default({ text: state.flatness == null ? "n/a" : state.flatness.toFixed(3) }) }));
|
|
if (state.used && state.mean_novelty != null) rows.push(c.settingsRow.default({ label: "Novelty", hint: "Near 1: blocks amplify what came before. Near 0: they add something new.",
|
|
control: c.hint.default({ text: state.mean_novelty.toFixed(3) }) }));
|
|
rows.push(c.settingsRow.default({ label: "Recording data", hint: "Download the stored data for this key, or clear it.", control: c.toolbar.default({ items: [
|
|
c.button.sm({ label: "⤓ Download", tone: "ghost", onClick: () => exportInfluence(state.key) }),
|
|
c.button.sm({ label: "Clear", tone: "danger", onClick: () => clearInfluence(state.key) })] }) }));
|
|
if (four) rows.push(c.settingsRow.default({ label: "Clips rated", hint: `Key: ${four.key || state.key || "none yet"}`,
|
|
control: c.hint.default({ text: four.used ? Object.entries(four.counts || {}).map(([g, n]) => `${g} ${n}`).join(", ") : "none yet" }) }));
|
|
if (four && four.note) rows.push(c.hint.default({ text: four.note }));
|
|
if (four && four.used && Object.values(four.counts || {}).some((n) => n < 5))
|
|
rows.push(c.hint.default({ text: "Few clips in a group: a difference this size can be chance. Rate more before reading it." }));
|
|
for (const [pair, cos] of Object.entries((four && four.cos) || {})) {
|
|
const c0 = four.chance[pair];
|
|
rows.push(c.settingsRow.default({ label: pair, hint: `Chance from shuffled labels: mean ${pct(c0.mean)}, 95th ${pct(c0.p95)}`,
|
|
control: c.hint.default({ text: pct(cos) }) }));
|
|
}
|
|
}
|
|
research.set(rows);
|
|
}
|
|
// Block map: one profiling run per model. It nudges each group of blocks and measures how much the picture moves, then later
|
|
// runs spread the per-block features' strengths by it. The switch is read at the start of each run.
|
|
const profile = c.region.stack({ gap: "sm" });
|
|
let mapState = null;
|
|
async function refreshMap() {
|
|
mapState = await (async () => app.api.blockMap())().catch((err) => ({ error: err.message }));
|
|
drawMap();
|
|
}
|
|
async function clearMap() {
|
|
if (!(await c.modal.dialogue({ title: "Clear block maps", message: "Forget the saved block maps? Features then act the same in every block until a new profile is made.", tone: "danger", confirmLabel: "Clear" }).result)) return;
|
|
try { await app.api.clearBlockMap(); } catch (err) { tell(err.message); }
|
|
refreshMap();
|
|
}
|
|
function drawMap() {
|
|
const maps = (mapState && mapState.maps) || [];
|
|
profile.set([
|
|
c.label.section({ text: "Block map" }),
|
|
mapState && mapState.error ? c.banner.warn({ text: `Could not read the block map: ${mapState.error}` }) : null,
|
|
mapState && mapState.problem ? c.banner.warn({ text: mapState.problem }) : null,
|
|
c.toggle.default({ label: "Profile the blocks on the next run", hint: "Measures how much each group of blocks can take. The next generation takes extra model calls on three steps, then this turns itself off.",
|
|
checked: !!(mapState && mapState.enabled),
|
|
onChange: async (on) => { try { await app.api.setBlockMap(on); } catch (err) { tell(err.message); } refreshMap(); } }),
|
|
c.settingsRow.default({ label: "Saved maps", hint: "One per model size. Features follow a map when one exists for the loaded model.",
|
|
control: c.toolbar.default({ items: [c.hint.default({ text: maps.length ? maps.map((m) => m.replace("_blocks.json", " blocks")).join(", ") : "none yet" }),
|
|
c.button.sm({ label: "Clear", tone: "danger", disabled: !maps.length, onClick: clearMap })] }) }),
|
|
].filter(Boolean));
|
|
}
|
|
const hasMap = !app.hasModule || app.hasModule("block_map");
|
|
if (hasMap) refreshMap();
|
|
const hasResearch = !app.hasModule || app.hasModule("block_influence"); // its recording is another module's; without it the part is not shown
|
|
draw(); refresh(); if (hasResearch) refreshResearch();
|
|
return c.region.stack({ gap: "none", children: [page, hasResearch ? research : null, hasMap ? profile : null] });
|
|
};
|