diff --git a/app/legacy/api.js b/app/legacy/api.js index ed3f1d21..a77a0ea5 100644 --- a/app/legacy/api.js +++ b/app/legacy/api.js @@ -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 .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;