Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tw9fjW75aYB8UyVRZo6fCr
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: "On: the next generation takes extra model calls on three of its steps and measures how much each group of blocks can take. It turns itself off once the map is saved.",
|
|
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] });
|
|
};
|