feat: rewire api.js against v5's real routes, stub the rest safely
Three buckets per the port plan: (a) same-shape routes just renamed
(projects, media, shortcuts, packs, git/system, temp, log), (b) a small
adapter where v5's shape differs (parsePrompt -> /api/prompt/expand,
resultUrl -> ComfyUI's own /view), (c) no v5 equivalent yet -- refinement
keys, REINS, probes, render/stitch, Models/pipeline-roles, Generate's
actual queue flow.
Every stub resolves to {unsupported, reason} instead of throwing, so a
module nobody has built yet can't take a caller down with it -- the
"feature-independent" backend the port asked for. Live-verified: wizard
through project creation into the real editor, zero console errors, every
real route round-tripping 200 OK.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6303f1312c
commit
b7acc2e91a
+183
-335
@@ -1,25 +1,18 @@
|
||||
// Thin backend client. Served by ComfyUI, so all URLs are same-origin (relative):
|
||||
// the browser automatically uses whatever host:port ComfyUI runs on.
|
||||
// Thin backend client, rewired against v5's real routes (core/routes.py).
|
||||
//
|
||||
// Three kinds of function live here, per the port plan:
|
||||
// (a) compatible, same shape -- just a different URL.
|
||||
// (b) compatible route, different shape -- a small adapter in the body.
|
||||
// (c) no v5 equivalent yet -- a stub. Calling one NEVER throws or
|
||||
// rejects: it resolves to `{ unsupported: true, reason }`. This is the
|
||||
// "feature-independent" contract the port asked for -- one module that
|
||||
// hasn't been built yet must not be able to crash a caller that forgot
|
||||
// to check for it. Real UI wiring for "show this control disabled" is
|
||||
// a separate, later pass; this just makes the absence survivable.
|
||||
(function () {
|
||||
// Base = the directory this app is served from, i.e. /funpack
|
||||
const BASE = window.location.pathname.replace(/\/+$/, "").replace(/\/index\.html$/, "");
|
||||
const API = (p) => `${BASE}/api${p}`;
|
||||
const FUNPACK = "/funpack";
|
||||
|
||||
async function funpackFetch(method, path, body) {
|
||||
const opts = { method, headers: {}, cache: "no-store" };
|
||||
if (body !== undefined) {
|
||||
opts.headers["Content-Type"] = "application/json";
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(path, opts);
|
||||
if (!res.ok) {
|
||||
let payload = null;
|
||||
try { payload = await res.json(); } catch (_) {}
|
||||
throw new Error(readApiError(res, payload));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const API = (p) => `${FUNPACK}${p}`; // v5 mounts everything under core/config.py's UI_PREFIX ("/funpack")
|
||||
|
||||
function readApiError(res, payload) {
|
||||
if (payload && typeof payload === "object") {
|
||||
@@ -33,9 +26,7 @@
|
||||
if (parts.length) return parts.join("; ");
|
||||
}
|
||||
if (payload.error) return String(payload.error);
|
||||
if (payload.parse_errors && typeof payload.parse_errors === "object") {
|
||||
return Object.entries(payload.parse_errors).map(([k, v]) => `${k}: ${v}`).join("; ");
|
||||
}
|
||||
if (Array.isArray(payload.problems) && payload.problems.length) return payload.problems.join("; ");
|
||||
}
|
||||
const status = res && res.status ? `HTTP ${res.status}` : "";
|
||||
const statusText = (res && res.statusText ? res.statusText : "").trim();
|
||||
@@ -57,193 +48,174 @@
|
||||
return res.status === 204 ? null : res.json();
|
||||
}
|
||||
|
||||
// A module v5 has not built yet. Resolves rather than rejects, so a caller
|
||||
// that does `const x = await API.thing()` without a try/catch gets a value
|
||||
// it can branch on instead of an uncaught rejection taking down whatever
|
||||
// called it.
|
||||
const unsupported = (reason) => Promise.resolve({ unsupported: true, reason });
|
||||
// Same contract for a plain (non-async) URL getter: a placeholder anchor
|
||||
// rather than a broken href built from `undefined`.
|
||||
const unsupportedUrl = () => "#";
|
||||
|
||||
const ClientAPI = {
|
||||
health: () => j("GET", API("/health")),
|
||||
health: () => j("GET", API("/api/health")),
|
||||
|
||||
// projects
|
||||
listProjects: () => j("GET", API("/projects")),
|
||||
createProject: (name) => j("POST", API("/projects"), { name }),
|
||||
getProject: (id) => j("GET", API(`/projects/${id}`)),
|
||||
saveProject: (id, data) => j("PUT", API(`/projects/${id}`), data),
|
||||
deleteProject: (id) => j("DELETE", API(`/projects/${id}`)),
|
||||
downloadProjectUrl: (id) => API(`/projects/${id}/download`),
|
||||
importProject: (data) => j("POST", API("/projects/import"), data),
|
||||
// --- projects (a) ------------------------------------------------------
|
||||
listProjects: async () => (await j("GET", API("/api/projects"))).projects,
|
||||
createProject: (name) => j("POST", API("/api/projects"), { name }),
|
||||
getProject: (id) => j("GET", API(`/api/projects/${id}`)),
|
||||
saveProject: (id, data) => j("PUT", API(`/api/projects/${id}`), data),
|
||||
deleteProject: (id) => j("DELETE", API(`/api/projects/${id}`)),
|
||||
downloadProjectUrl: (id) => API(`/api/projects/${id}/download`),
|
||||
importProject: (data) => j("POST", API("/api/projects/import"), data),
|
||||
|
||||
// timeline preview
|
||||
preview: (id, includeExcluded, forGeneration = true) =>
|
||||
j("GET", API(`/projects/${id}/preview?include_excluded=${includeExcluded ? "true" : "false"}&for_generation=${forGeneration ? "true" : "false"}`)),
|
||||
parsePrompt: (id, prompt) => j("POST", API(`/projects/${id}/parse`), { prompt }),
|
||||
// --- prompt preview (b) -------------------------------------------------
|
||||
// v4's `preview()` rendered a whole scene-by-scene montage plan server
|
||||
// side; v5 has no render/stitch stage yet (HIGH #2, not built). Stub.
|
||||
preview: () => unsupported("no render/stitch stage yet"),
|
||||
// `parsePrompt` becomes an adapter onto /api/prompt/expand: fetch the
|
||||
// project for its anchor/postfix/variables, then expand THIS text the
|
||||
// same way generation would.
|
||||
async parsePrompt(id, prompt) {
|
||||
const project = await ClientAPI.getProject(id).catch(() => null);
|
||||
return j("POST", API("/api/prompt/expand"), {
|
||||
text: prompt,
|
||||
anchor: project?.anchor || "",
|
||||
postfix: project?.postfix || "",
|
||||
postfix_enabled: project?.postfix_enabled !== false,
|
||||
variables: project?.variables || [],
|
||||
});
|
||||
},
|
||||
|
||||
// libraries
|
||||
transitions: () => j("GET", API("/library/transitions")),
|
||||
saveTransition: (item) => j("POST", API("/library/transitions"), item),
|
||||
deleteTransition: (name) => j("DELETE", API(`/library/transitions/${encodeURIComponent(name)}`)),
|
||||
exportTransitionsUrl: () => API("/library/transitions/export"),
|
||||
importTransitions: (data, mode) => j("POST", API(`/library/transitions/import?mode=${mode || "merge"}`), data),
|
||||
clearTransitions: () => j("POST", API("/library/transitions/clear"), {}),
|
||||
shortcuts: () => j("GET", API("/library/shortcuts")),
|
||||
suggestionStats: () => j("GET", API("/library/suggestion_stats")),
|
||||
saveCategory: (payload) => j("POST", API("/library/categories"), payload),
|
||||
saveShortcut: (item) => j("POST", API("/library/shortcuts"), item),
|
||||
deleteShortcut: (name) => j("DELETE", API(`/library/shortcuts/${encodeURIComponent(name)}`)),
|
||||
exportShortcutsUrl: () => API("/library/shortcuts/export"),
|
||||
importShortcuts: (data, mode) => j("POST", API(`/library/shortcuts/import?mode=${mode || "merge"}`), data),
|
||||
clearShortcuts: () => j("POST", API("/library/shortcuts/clear"), {}),
|
||||
revolverSettings: () => j("GET", API("/library/revolver")),
|
||||
setRevolverSettings: (payload) => j("POST", API("/library/revolver"), payload),
|
||||
// --- transitions library (c) -- no equivalent in v5 --------------------
|
||||
transitions: () => unsupported("no transitions library in v5 yet"),
|
||||
saveTransition: () => unsupported("no transitions library in v5 yet"),
|
||||
deleteTransition: () => unsupported("no transitions library in v5 yet"),
|
||||
exportTransitionsUrl: unsupportedUrl,
|
||||
importTransitions: () => unsupported("no transitions library in v5 yet"),
|
||||
clearTransitions: () => unsupported("no transitions library in v5 yet"),
|
||||
|
||||
// FunPack file manager (Composer ▸ Files)
|
||||
listFiles: () => j("GET", API("/files")),
|
||||
deleteFile: (group, name) => j("DELETE", API(`/files/${encodeURIComponent(group)}/${encodeURIComponent(name)}`)),
|
||||
clearFiles: (group) => j("POST", API(`/files/${encodeURIComponent(group)}/clear`), {}),
|
||||
nleLibrary: () => j("GET", API("/library/nle")),
|
||||
customNodes: () => j("GET", API("/custom-nodes")),
|
||||
customNodesCheck: () => j("POST", API("/custom-nodes/check"), {}),
|
||||
customNodeInstall: (url) => j("POST", API("/custom-nodes/install"), { url }),
|
||||
customNodeUpdate: (name) => j("POST", API("/custom-nodes/update"), { name }),
|
||||
customNodeRemove: (name) => j("POST", API("/custom-nodes/remove"), { name }),
|
||||
// --- prompt shortcuts (a/b) ---------------------------------------------
|
||||
shortcuts: async () => (await j("GET", API("/api/shortcuts"))).shortcuts,
|
||||
suggestionStats: () => unsupported("no revolver in v5 yet"),
|
||||
saveCategory: () => unsupported("shortcut categories are per-shortcut fields in v5, not a separate CRUD"),
|
||||
saveShortcut: (item) => j("POST", API("/api/shortcuts"), item),
|
||||
deleteShortcut: (name) => j("DELETE", API(`/api/shortcuts/${encodeURIComponent(name)}`)),
|
||||
exportShortcutsUrl: unsupportedUrl,
|
||||
importShortcuts: () => unsupported("no import route in v5 yet"),
|
||||
clearShortcuts: () => j("POST", API("/api/shortcuts/clear"), {}),
|
||||
revolverSettings: () => unsupported("no revolver in v5 yet"),
|
||||
setRevolverSettings: () => unsupported("no revolver in v5 yet"),
|
||||
|
||||
// media bin
|
||||
listMedia: () => j("GET", API("/media")),
|
||||
mediaUrl: (id) => API(`/media/${encodeURIComponent(id)}`),
|
||||
deleteMedia: (id) => j("DELETE", API(`/media/${encodeURIComponent(id)}`)),
|
||||
renameMedia: (id, name) => j("PATCH", API(`/media/${encodeURIComponent(id)}`), { name }),
|
||||
importClipToMediaBin: (clip, name) => j("POST", API("/media/import-clip"), { clip, name: name || null }),
|
||||
// --- Composer file manager / NLE library (c) ----------------------------
|
||||
listFiles: () => unsupported("the Composer file manager was retired with the composer shell"),
|
||||
deleteFile: () => unsupported("the Composer file manager was retired with the composer shell"),
|
||||
clearFiles: () => unsupported("the Composer file manager was retired with the composer shell"),
|
||||
nleLibrary: () => unsupported("no render/stitch stage yet"),
|
||||
|
||||
// --- node packs (a) ------------------------------------------------------
|
||||
customNodes: async () => (await j("GET", API("/api/packs"))),
|
||||
customNodesCheck: () => j("POST", API("/api/packs/check"), {}),
|
||||
customNodeInstall: (url) => j("POST", API("/api/packs/install"), { url }),
|
||||
customNodeUpdate: (name) => j("POST", API("/api/packs/update"), { name }),
|
||||
customNodeRemove: (name) => j("POST", API("/api/packs/remove"), { name }),
|
||||
|
||||
// --- media bin (a) ---------------------------------------------------------
|
||||
listMedia: async () => (await j("GET", API("/api/media"))).media,
|
||||
mediaUrl: (id) => API(`/api/media/${encodeURIComponent(id)}/file`),
|
||||
deleteMedia: (id) => j("DELETE", API(`/api/media/${encodeURIComponent(id)}`)),
|
||||
renameMedia: () => unsupported("media has no name field in v5 -- it is addressed by id"),
|
||||
importClipToMediaBin: () => unsupported("no render/stitch stage yet, so there is no clip to import"),
|
||||
async uploadMedia(file) {
|
||||
const fd = new FormData(); fd.append("file", file, file.name);
|
||||
const res = await fetch(API("/media"), { method: "POST", body: fd });
|
||||
const res = await fetch(API("/api/media"), { method: "POST", body: fd });
|
||||
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).detail || res.statusText);
|
||||
return res.json();
|
||||
const body = await res.json();
|
||||
return body.media && body.media[0] ? body.media[0] : body;
|
||||
},
|
||||
|
||||
// models / node slots
|
||||
nodeRoles: () => j("GET", API("/node-roles")),
|
||||
nodeCandidates: (role, refresh) => j("GET", API(`/node-candidates/${role}${refresh ? "?refresh=true" : ""}`)),
|
||||
allNodes: () => j("GET", API("/all-nodes")),
|
||||
nodeSpec: (cls) => j("GET", API(`/node/${encodeURIComponent(cls)}`)),
|
||||
// pid is optional but should be passed whenever a project is open: without it the answer
|
||||
// describes the GLOBAL default, which is not necessarily this project's family.
|
||||
pipelinePorts: (pid) => j("GET", API("/pipeline-ports" + (pid ? "?pid=" + encodeURIComponent(pid) : ""))),
|
||||
pipelineDeps: (pid) => j("GET", API("/pipeline-deps" + (pid ? "?pid=" + encodeURIComponent(pid) : ""))),
|
||||
pipelineDepsInstall: (packIds) => j("POST", API("/pipeline-deps/install"), { pack_ids: packIds }),
|
||||
pipelineDepsInstallManager: () => j("POST", API("/pipeline-deps/install"), { install_manager: true }),
|
||||
pipelineDepsInstallStatus: (jobId) => j("GET", API(`/pipeline-deps/install/${encodeURIComponent(jobId)}`)),
|
||||
pipelineDepsInstallCancel: (jobId) => j("POST", API(`/pipeline-deps/install/${encodeURIComponent(jobId)}/cancel`), {}),
|
||||
imageTargets: (pid) => j("GET", API("/image-targets" + (pid ? `?pid=${encodeURIComponent(pid)}` : ""))),
|
||||
// Models config is per-project; pass the project id. Falls back to the global
|
||||
// default route (used as the seed/template) when no project is given.
|
||||
coreGraph: (pid) => j("GET", API("/core-graph" + (pid ? `?pid=${encodeURIComponent(pid)}` : ""))),
|
||||
getModels: (pid) => j("GET", API(pid ? `/projects/${pid}/models` : "/models")),
|
||||
// The settings card is a PNG, not JSON — fetched as a blob so the modal can show it,
|
||||
// download it and put it on the clipboard from the one response.
|
||||
settingsCard: async (pid, theme) => {
|
||||
const q = new URLSearchParams();
|
||||
if (pid) q.set("pid", pid);
|
||||
if (theme) q.set("theme", theme);
|
||||
const r = await fetch(API("/settings-card") + (q.toString() ? `?${q}` : ""));
|
||||
if (!r.ok) throw new Error(await r.text().catch(() => r.statusText));
|
||||
return r.blob();
|
||||
// --- models / node slots (b/c) -------------------------------------------
|
||||
// v4's Studio-shaped "roles" and "candidates per role" have no v5
|
||||
// equivalent -- v5's pipeline is a flat list of swappable slots, not a
|
||||
// fixed set of named roles. Real replacements below; the rest stub until
|
||||
// the Models & Pipeline panel itself is rewritten against this shape
|
||||
// (tracked separately -- passing mismatched data through here would make
|
||||
// that panel fail in a way that looks like a bug in the panel, not an
|
||||
// honest "not built yet").
|
||||
nodeRoles: () => unsupported("v5 has slots, not fixed roles -- see pipelinePorts()"),
|
||||
nodeCandidates: () => unsupported("v5 has slots, not fixed roles -- see nodeSpec()/pipelinePorts()"),
|
||||
allNodes: () => unsupported("no whole-catalogue route in v5 -- nothing scans every installed node at once"),
|
||||
nodeSpec: async (cls) => {
|
||||
const body = await j("GET", API(`/api/nodes?classes=${encodeURIComponent(cls)}`));
|
||||
return body.nodes ? body.nodes[cls] : null;
|
||||
},
|
||||
saveModels: (pid, data) => j("PUT", API(pid ? `/projects/${pid}/models` : "/models"), data),
|
||||
refreshModels: () => j("POST", API("/models/refresh")),
|
||||
parseWorkflow: (workflow) => j("POST", API("/workflow/parse"), { workflow }),
|
||||
applyWorkflow: (pid, workflow, bindings) =>
|
||||
j("POST", API(`/projects/${pid}/workflow/apply`), { workflow, bindings }),
|
||||
restart: () => j("POST", API("/restart")),
|
||||
systemInfo: () => j("GET", API("/system/info")),
|
||||
gitStatus: () => j("GET", API("/git/status")),
|
||||
gitUpdate: (branch) => j("POST", API("/git/update"), branch ? { branch } : {}),
|
||||
gitCheckout: (branch) => j("POST", API("/git/checkout"), { branch }),
|
||||
gitRollback: () => j("POST", API("/git/rollback"), {}),
|
||||
pipelinePorts: () => j("GET", API("/api/pipeline")), // pid ignored: v5's pipeline draft is global, not per-project
|
||||
pipelineDeps: () => unsupported("v5 offers manual pack install only -- no auto-detect for the loaded pipeline"),
|
||||
pipelineDepsInstall: () => unsupported("v5 offers manual pack install only -- no auto-detect for the loaded pipeline"),
|
||||
pipelineDepsInstallManager: () => unsupported("v5 offers manual pack install only -- no auto-detect for the loaded pipeline"),
|
||||
pipelineDepsInstallStatus: () => unsupported("v5 offers manual pack install only -- no auto-detect for the loaded pipeline"),
|
||||
pipelineDepsInstallCancel: () => unsupported("v5 offers manual pack install only -- no auto-detect for the loaded pipeline"),
|
||||
imageTargets: () => unsupported("not built in v5 yet"),
|
||||
coreGraph: () => j("GET", API("/api/pipeline")),
|
||||
getModels: () => unsupported("the Models panel needs its own rewrite against v5's pipeline-slot shape"),
|
||||
saveModels: () => unsupported("the Models panel needs its own rewrite against v5's pipeline-slot shape"),
|
||||
settingsCard: () => { throw new Error("settings card export is not built in v5 yet"); },
|
||||
refreshModels: () => unsupported("v5 has no model-file cache to refresh -- it reads the folders live"),
|
||||
parseWorkflow: () => unsupported("workflow import is LOW priority, not built yet"),
|
||||
applyWorkflow: () => unsupported("workflow import is LOW priority, not built yet"),
|
||||
|
||||
// generate (a single scene, or an explicit run of scene ids = one chain request)
|
||||
// `simple` strips the enhancements for THIS run only (see pipeline_caps.apply_simple_mode).
|
||||
generate: (id, onlyScene, sceneIds, resetSession, nodeOverrides, prevSceneMedia) =>
|
||||
j("POST", API(`/projects/${id}/generate`), { only_scene: onlyScene || null, scene_ids: sceneIds || null, reset_session: !!resetSession, node_overrides: nodeOverrides || null, simple: !!window.FunPackMode?.isSimple(), prev_scene_media: prevSceneMedia || null }),
|
||||
status: (id, promptId) => j("GET", API(`/projects/${id}/status/${promptId}`)),
|
||||
progress: () => j("GET", API("/progress")),
|
||||
// The editor's own in-flight generation, recovered from ComfyUI's queue (survives a UI reload).
|
||||
active: () => j("GET", API("/active")),
|
||||
ratingLabels: () => j("GET", API("/rating-labels")),
|
||||
log: (limit) => j("GET", API("/log" + (limit ? `?limit=${limit}` : ""))),
|
||||
interrupt: () => j("POST", API("/interrupt")),
|
||||
// --- updates / system (a) ------------------------------------------------
|
||||
restart: () => j("POST", API("/api/git/restart"), {}),
|
||||
systemInfo: () => j("GET", API("/api/system")),
|
||||
gitStatus: () => j("GET", API("/api/git/status")),
|
||||
gitUpdate: (branch) => j("POST", API("/api/git/update"), branch ? { branch } : {}),
|
||||
gitCheckout: (branch) => j("POST", API("/api/git/checkout"), { branch }),
|
||||
gitRollback: () => j("POST", API("/api/git/rollback"), {}),
|
||||
|
||||
// ComfyUI temp folder browser (scene previews & other transient outputs, wiped on restart).
|
||||
listTemp: () => j("GET", API("/temp")),
|
||||
// --- model-family detection (a) -- new in v5, no v4 equivalent ---------
|
||||
probeFamily: (file) => j("GET", API(`/api/probe?file=${encodeURIComponent(file)}`)),
|
||||
|
||||
// trajectory probe (Settings ▸ Learning)
|
||||
probeStatus: () => j("GET", API("/trajectory_probe")),
|
||||
probeSetEnabled: (enabled) => j("POST", API("/trajectory_probe"), { enabled: !!enabled }),
|
||||
probeClear: () => j("POST", API("/trajectory_probe/clear"), {}),
|
||||
probeAnalyse: (trials) => j("POST", API("/trajectory_probe/analyse"), { trials: trials || 2000 }),
|
||||
// --- generate / run (c) --------------------------------------------------
|
||||
// v5 deliberately has no server-side queue/status route (run.js's own
|
||||
// header comment: "two things tracking what is running is two things
|
||||
// that can disagree"). Real generation goes through app/shell/session.js
|
||||
// + run.js talking to ComfyUI's own /prompt and websocket directly --
|
||||
// that is a UI-flow rewrite (the Generate button's click handler), not
|
||||
// an api.js body swap, and is tracked as its own step in the port plan.
|
||||
generate: () => unsupported("Generate is wired directly through session.js/run.js, not this seam -- pending the button rewrite"),
|
||||
status: () => unsupported("no server-side run status in v5 -- see run.js"),
|
||||
progress: () => unsupported("no server-side run status in v5 -- see run.js"),
|
||||
active: () => unsupported("no server-side run status in v5 -- see run.js"),
|
||||
ratingLabels: () => unsupported("v5's ratings are the fixed pair liked/disliked -- nothing to label"),
|
||||
log: (limit) => j("GET", API("/api/log" + (limit ? `?limit=${limit}` : ""))),
|
||||
interrupt: () => j("POST", "/interrupt"), // ComfyUI's own native route, not FunPack's
|
||||
|
||||
// The measurement has to outlive the box it was taken on: a rental gets
|
||||
// replaced, refinements/ is not in git, and the reading needs ~16 rated runs.
|
||||
async probeExport() {
|
||||
const res = await fetch(API("/trajectory_probe/export") + `?t=${Date.now()}`, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(readApiError(res, null));
|
||||
const url = URL.createObjectURL(await res.blob());
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "trajectory_probe.pt";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
async probeImport(file) {
|
||||
const res = await fetch(API("/trajectory_probe/import"), { method: "POST", body: file });
|
||||
let payload = null;
|
||||
try { payload = await res.json(); } catch (_) {}
|
||||
if (!res.ok) throw new Error(readApiError(res, payload));
|
||||
return payload;
|
||||
},
|
||||
// --- temp files (a) -------------------------------------------------------
|
||||
listTemp: () => j("GET", API("/api/temp")),
|
||||
|
||||
// H3 representation steering (Settings ▸ Learning) — per-KEY, unlike the probe above.
|
||||
reinsStatus: (key) => j("GET", API("/h3_repr_steering") + `?key=${encodeURIComponent(key || "default")}`),
|
||||
reinsSweep: (key, trials) => j("POST", API("/h3_repr_steering/sweep"), { key: key || "default", trials: trials || 2000 }),
|
||||
reinsClear: (key) => j("POST", API("/h3_repr_steering/clear"), { key: key || "default" }),
|
||||
// Batch/sweep results save under their OWN slot (h3_repr_capture_slot), not the single
|
||||
// overwritable "pending" — so rating one is a direct commit, no extra generation needed.
|
||||
reinsRateSlot: (key, slot, rating) => j("POST", API("/h3_repr_steering/rate-slot"), { key: key || "default", slot, rating }),
|
||||
reinsDiscardSlot: (key, slot) => j("POST", API("/h3_repr_steering/discard-slot"), { key: key || "default", slot }),
|
||||
async reinsExport(key) {
|
||||
const k = key || "default";
|
||||
const res = await fetch(API("/h3_repr_steering/export") + `?key=${encodeURIComponent(k)}&t=${Date.now()}`, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(readApiError(res, null));
|
||||
const url = URL.createObjectURL(await res.blob());
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${k}.repr_steer.pt`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
// --- trajectory probe / REINS / block-influence / detail probe (c) ------
|
||||
// All LOW priority research tooling per the roadmap; none built in v5.
|
||||
probeStatus: () => unsupported("trajectory probe is LOW priority, not built yet"),
|
||||
probeSetEnabled: () => unsupported("trajectory probe is LOW priority, not built yet"),
|
||||
probeClear: () => unsupported("trajectory probe is LOW priority, not built yet"),
|
||||
probeAnalyse: () => unsupported("trajectory probe is LOW priority, not built yet"),
|
||||
probeExport: () => { throw new Error("trajectory probe is LOW priority, not built yet"); },
|
||||
probeImport: () => unsupported("trajectory probe is LOW priority, not built yet"),
|
||||
reinsStatus: () => unsupported("REINS is LOW priority, not built yet"),
|
||||
reinsSweep: () => unsupported("REINS is LOW priority, not built yet"),
|
||||
reinsClear: () => unsupported("REINS is LOW priority, not built yet"),
|
||||
reinsRateSlot: () => unsupported("REINS is LOW priority, not built yet"),
|
||||
reinsDiscardSlot: () => unsupported("REINS is LOW priority, not built yet"),
|
||||
reinsExport: () => { throw new Error("REINS is LOW priority, not built yet"); },
|
||||
blockInfluenceStatus: () => unsupported("block influence probe is LOW priority, not built yet"),
|
||||
blockInfluenceSetEnabled: () => unsupported("block influence probe is LOW priority, not built yet"),
|
||||
blockInfluenceClear: () => unsupported("block influence probe is LOW priority, not built yet"),
|
||||
blockInfluenceExport: () => { throw new Error("block influence probe is LOW priority, not built yet"); },
|
||||
detailProbeStatus: () => unsupported("detail probe is LOW priority, not built yet"),
|
||||
detailProbeSetEnabled: () => unsupported("detail probe is LOW priority, not built yet"),
|
||||
detailProbeClear: () => unsupported("detail probe is LOW priority, not built yet"),
|
||||
|
||||
// Block-influence probe (Settings ▸ Learning) — per-KEY data, but the collection
|
||||
// switch itself is global, like the trajectory probe's.
|
||||
blockInfluenceStatus: (key) => j("GET", API("/block_influence") + `?key=${encodeURIComponent(key || "default")}`),
|
||||
blockInfluenceSetEnabled: (key, enabled) => j("POST", API("/block_influence"), { key: key || "default", enabled: !!enabled }),
|
||||
blockInfluenceClear: (key) => j("POST", API("/block_influence/clear"), { key: key || "default" }),
|
||||
async blockInfluenceExport(key) {
|
||||
const k = key || "default";
|
||||
const res = await fetch(API("/block_influence/export") + `?key=${encodeURIComponent(k)}&t=${Date.now()}`, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(readApiError(res, null));
|
||||
const url = URL.createObjectURL(await res.blob());
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${k}.block_influence.pt`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
detailProbeStatus: (key) => j("GET", API("/detail_probe") + `?key=${encodeURIComponent(key || "default")}`),
|
||||
detailProbeSetEnabled: (key, enabled) => j("POST", API("/detail_probe"), { key: key || "default", enabled: !!enabled }),
|
||||
detailProbeClear: (key) => j("POST", API("/detail_probe/clear"), { key: key || "default" }),
|
||||
|
||||
// Served by ComfyUI's own same-origin /view endpoint — no editor route needed.
|
||||
// --- ComfyUI's own temp-output viewer (a) -- unrelated to FunPack's prefix
|
||||
tempFileUrl: (f) => "/view?" + new URLSearchParams({
|
||||
filename: f.filename, subfolder: f.subfolder || "", type: "temp", t: String(f.mtime || Date.now()),
|
||||
}).toString(),
|
||||
@@ -260,152 +232,28 @@
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
renderFinal: (id, clips) => j("POST", API(`/projects/${id}/render`), { clips }),
|
||||
renderFinalStatus: (id, jobId) => j("GET", API(`/projects/${id}/render/${encodeURIComponent(jobId)}`)),
|
||||
exportClip: (id, clip) => j("POST", API(`/projects/${id}/export-clip`), { clip }),
|
||||
exportClipsCombined: (id, clips) => j("POST", API(`/projects/${id}/export-clips`), { clips }),
|
||||
exportClipsStatus: (id, jobId) => j("GET", API(`/projects/${id}/export-clips/${encodeURIComponent(jobId)}`)),
|
||||
resultUrl: (id, m) =>
|
||||
API(`/projects/${id}/result?filename=${encodeURIComponent(m.filename)}`) +
|
||||
`&subfolder=${encodeURIComponent(m.subfolder || "")}&type=${encodeURIComponent(m.type || "output")}`,
|
||||
previewSegmentUrl: (id, sceneId, spec) => {
|
||||
let u = API(`/projects/${id}/preview-segment/${encodeURIComponent(sceneId)}`);
|
||||
const m = spec?.media;
|
||||
if (m?.filename) {
|
||||
const q = new URLSearchParams({
|
||||
filename: m.filename,
|
||||
subfolder: m.subfolder || "",
|
||||
type: m.type || "output",
|
||||
render_in: String(spec.renderIn != null ? spec.renderIn : 0),
|
||||
});
|
||||
// dur is in the URL for two reasons: ghosts have no scene server-side (the trim
|
||||
// window must travel in the query), and it versions the browser cache — segments
|
||||
// are cached for an hour, so a timeline trim must produce a different URL.
|
||||
if (spec.dur != null) q.set("dur", String(spec.dur));
|
||||
// Reverse changes the bytes, so it must change the URL — segments are cached for
|
||||
// an hour and would otherwise replay the forward encode.
|
||||
if (spec.reverse) q.set("rev", "1");
|
||||
u += "?" + q.toString();
|
||||
}
|
||||
return u;
|
||||
},
|
||||
|
||||
// FunPack refinement keys (ComfyUI root routes, same as Studio / Refinement Key Loader)
|
||||
refinementKeys: async () => {
|
||||
const res = await funpackFetch("GET", `${FUNPACK}/refinement_keys?cache_bust=${Date.now()}`);
|
||||
return res.json();
|
||||
},
|
||||
importRefinementKey: async (data, opts = {}) => {
|
||||
const ow = opts.overwrite ? "&overwrite=true" : "";
|
||||
const json = JSON.stringify(data);
|
||||
const bytes = new TextEncoder().encode(json);
|
||||
// A 409 means a key of that name already exists — surface it as a tagged
|
||||
// error so the caller can ask the user to overwrite (then retry).
|
||||
const finish = async (res) => {
|
||||
if (res.status === 409) {
|
||||
let p = null; try { p = await res.json(); } catch (_) {}
|
||||
const err = new Error((p && p.error) || "Refinement key already exists.");
|
||||
err.exists = true; err.key = (p && p.key) || "";
|
||||
throw err;
|
||||
}
|
||||
if (!res.ok) {
|
||||
let p = null; try { p = await res.json(); } catch (_) {}
|
||||
throw new Error(readApiError(res, p));
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
// Reverse proxies on Vast.ai / Runpod cap the request body well below
|
||||
// ComfyUI's 100 MB, so a one-shot POST of a large key gets HTTP 413 at the
|
||||
// proxy. Keep a fast single POST for small keys; stream big ones in chunks
|
||||
// (each far under any proxy limit) so they can never be rejected.
|
||||
const CHUNK = 256 * 1024;
|
||||
if (bytes.length <= CHUNK) {
|
||||
const res = await fetch(`${FUNPACK}/refinement_keys/import?_=${Date.now()}${ow}`, {
|
||||
method: "POST", cache: "no-store",
|
||||
headers: { "Content-Type": "application/json" }, body: json,
|
||||
});
|
||||
if (res.status !== 413) return finish(res); // 413 → fall through to chunked
|
||||
}
|
||||
const uploadId = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
const postChunk = async (path, body) => {
|
||||
const res = await fetch(path, {
|
||||
method: "POST", cache: "no-store",
|
||||
headers: { "Content-Type": "application/octet-stream" }, body,
|
||||
});
|
||||
if (!res.ok) {
|
||||
let payload = null;
|
||||
try { payload = await res.json(); } catch (_) {}
|
||||
throw new Error(readApiError(res, payload));
|
||||
}
|
||||
return res;
|
||||
};
|
||||
let index = 0;
|
||||
for (let off = 0; off < bytes.length; off += CHUNK, index++) {
|
||||
// Slice by bytes (not string chars) so a multi-byte char is never split
|
||||
// across the seam where the two halves get re-encoded independently.
|
||||
await postChunk(
|
||||
`${FUNPACK}/refinement_keys/import_chunk?upload_id=${encodeURIComponent(uploadId)}&index=${index}`,
|
||||
bytes.slice(off, off + CHUNK),
|
||||
);
|
||||
}
|
||||
return finish(await fetch(
|
||||
`${FUNPACK}/refinement_keys/import_finalize?upload_id=${encodeURIComponent(uploadId)}${ow}`,
|
||||
{ method: "POST", cache: "no-store" },
|
||||
));
|
||||
},
|
||||
async exportRefinementKeyFile(key) {
|
||||
const res = await fetch(
|
||||
`${FUNPACK}/refinement_keys/export?key=${encodeURIComponent(key)}&cache_bust=${Date.now()}`,
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
if (!res.ok) {
|
||||
let payload = null;
|
||||
try { payload = await res.json(); } catch (_) {}
|
||||
throw new Error(readApiError(res, payload));
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${key}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
// --- render / stitch / export (c) -- HIGH #2, not built ------------------
|
||||
renderFinal: () => unsupported("no render/stitch stage yet"),
|
||||
renderFinalStatus: () => unsupported("no render/stitch stage yet"),
|
||||
exportClip: () => unsupported("no render/stitch stage yet"),
|
||||
exportClipsCombined: () => unsupported("no render/stitch stage yet"),
|
||||
exportClipsStatus: () => unsupported("no render/stitch stage yet"),
|
||||
// (b): v5 has no per-project /result route, but a finished output is just
|
||||
// a file ComfyUI itself saved -- its own /view endpoint serves it exactly
|
||||
// the way it serves a temp preview.
|
||||
resultUrl: (_id, m) => "/view?" + new URLSearchParams({
|
||||
filename: m.filename, subfolder: m.subfolder || "", type: m.type || "output",
|
||||
}).toString(),
|
||||
previewSegmentUrl: unsupportedUrl,
|
||||
|
||||
// Atomically delete a refinement key and ALL its sidecars (value function,
|
||||
// blessed attention / K-V banks, creativity latent, velocity store). Deleting
|
||||
// only the visible <key>.json orphans those, and they keep steering future runs
|
||||
// across restarts — the backend sweep is what makes "key deleted" mean it.
|
||||
async deleteRefinementKey(key) {
|
||||
const res = await funpackFetch(
|
||||
"POST", `${FUNPACK}/refinement_keys/delete?key=${encodeURIComponent(key)}&_=${Date.now()}`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
let payload = null;
|
||||
try { payload = await res.json(); } catch (_) {}
|
||||
throw new Error(readApiError(res, payload));
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// The keyless Absolute "global taste" store — learns from every rated generation
|
||||
// across all prompts, invisible in the key list, applied only in absolute/both
|
||||
// steer mode, and otherwise only wiped by Session Reset.
|
||||
async absoluteStoreInfo() {
|
||||
const res = await funpackFetch("GET", `${FUNPACK}/refinement_keys/absolute?_=${Date.now()}`);
|
||||
return res.json();
|
||||
},
|
||||
async clearAbsoluteStore() {
|
||||
const res = await funpackFetch("POST", `${FUNPACK}/refinement_keys/clear_absolute?_=${Date.now()}`);
|
||||
if (!res.ok) {
|
||||
let payload = null;
|
||||
try { payload = await res.json(); } catch (_) {}
|
||||
throw new Error(readApiError(res, payload));
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
// --- refinement keys / absolute taste store (c) -- not built in v5 -------
|
||||
refinementKeys: () => unsupported("refinement keys are not built in v5 yet"),
|
||||
importRefinementKey: () => unsupported("refinement keys are not built in v5 yet"),
|
||||
exportRefinementKeyFile: () => { throw new Error("refinement keys are not built in v5 yet"); },
|
||||
deleteRefinementKey: () => unsupported("refinement keys are not built in v5 yet"),
|
||||
absoluteStoreInfo: () => unsupported("refinement keys are not built in v5 yet"),
|
||||
clearAbsoluteStore: () => unsupported("refinement keys are not built in v5 yet"),
|
||||
};
|
||||
|
||||
window.MovieEditorAPI = ClientAPI;
|
||||
|
||||
Reference in New Issue
Block a user