feat: wire Generate/Cancel/reattach through v5's real check->queue->terminal flow
Adds generate_bridge.js, a small ES-module shim over the untouched app/shell/run.js + session.js + pipeline.js + client.js, and rewires store.js's generate()/generateMontage()/generateSelected()/interrupt()/ init() reattach to go through it instead of the now-permanently-stubbed API.generate/status/active. PipelineState (pipeline_state.js) stays the one client-side copy of the live pipeline; the bridge only adds the run/queue/socket layer on top of it. Single scene (or the whole active timeline, for a non-chain pipeline like H3) per real queue -- v5 has no chain-sampler/montage stage. REINS captures and the anchor-guide i2v bypass are said loudly (console.warn) rather than silently dropped, since neither has a v5 equivalent yet. Reference-image wiring is left as a placeholder: legacy's saveProject/getProject adapter doesn't round-trip source_image/references yet, so there is nothing to wire in from there today. Live-verified against the real dev server: a real Generate click built the scene's prompt, POSTed it through /funpack/api/pipeline's check, and surfaced the genuine refusal reason (an unset model loader) in the existing status UI, with the button correctly re-enabled after. 688/689 JS tests and 548/548 Python tests pass (the one JS failure is the pre-existing, unrelated projects.test.js import-race flake). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c6559765b7
commit
bb404d988d
@@ -0,0 +1,99 @@
|
||||
// Bridges v5's real check -> queue -> terminal generation flow into the classic-
|
||||
// script v4 world. app/shell/run.js, session.js, pipeline.js and client.js are
|
||||
// unmodified here -- they have no DOM/composer dependency (see the port plan's
|
||||
// "Preserved and rewired" list) -- this file is only the small ES-module shim
|
||||
// that hands their globals-free API to v4's global-script callers.
|
||||
//
|
||||
// Single scene at a time, on purpose: v5 has no chain-sampler/montage stage
|
||||
// (postponed, LTX-only -- see the port plan), so "Generate" here means what
|
||||
// app/boot.js's own generateCurrentScene/generateAll mean -- one real queue per
|
||||
// scene, run to a terminal state before the next one starts. store.js supplies
|
||||
// the scene loop; this only knows how to run ONE.
|
||||
import { createRun, viewUrl, DONE, FAILED, CANCELLED } from "../shell/run.js";
|
||||
import { clientId, connect, queuedFor, finishedFor } from "../shell/client.js";
|
||||
import { wire, waitForTerminal } from "../shell/session.js";
|
||||
import { check } from "../shell/pipeline.js";
|
||||
|
||||
const PS = window.PipelineState;
|
||||
const id = clientId();
|
||||
const run = createRun({ clientId: id, connect });
|
||||
|
||||
// The five hooks session.js's wire()/createGenerator call on `page.transport`.
|
||||
// Nothing here owns UI -- store.js already renders generation status through
|
||||
// its own `state.gen` -- so these just fan out to whoever subscribes via
|
||||
// GenerateBridge.on(...).
|
||||
const listeners = { say: [], warn: [], hold: [], release: [], disabled: [], adopt: [] };
|
||||
function fire(kind, arg) { (listeners[kind] || []).forEach((fn) => { try { fn(arg); } catch (_) {} }); }
|
||||
const transport = {
|
||||
generate: { setDisabled: (v) => fire("disabled", v) },
|
||||
say: (msg) => fire("say", msg),
|
||||
warn: (msg) => fire("warn", msg),
|
||||
draw: () => {}, // run.subscribe() below already covers this
|
||||
hold: (msg) => fire("hold", msg),
|
||||
release: (state) => fire("release", state),
|
||||
};
|
||||
|
||||
// Which scene/project a run belongs to. Set by generate() at click time; read
|
||||
// by run.start() at the moment IT actually queues (see session.js's own
|
||||
// comment on why this can't be read only after the fact).
|
||||
let ranFor = null;
|
||||
let ranForProject = null;
|
||||
// The inputs the NEXT queued run should send -- built by the caller (store.js
|
||||
// knows the scene text, anchor, postfix, variables; this file does not) and
|
||||
// handed to generate() right before it runs, not assembled here.
|
||||
let pendingInputs = null;
|
||||
|
||||
const session = wire({
|
||||
run, page: { transport }, check, id, queuedFor, finishedFor,
|
||||
// PipelineState (pipeline_state.js) is the one client-side copy of the live
|
||||
// pipeline, shared with Engine Settings and Models & Pipeline. A second copy
|
||||
// here would be the exact bug pipeline_state.js was extracted to fix.
|
||||
slots: () => PS.slots(),
|
||||
// Module settings are already IN `slots` by the time Generate runs -- every
|
||||
// Engine Settings edit is saved through PS.save(), which writes into the
|
||||
// sink node's input (see pipeline_state.js). `values` exists for the
|
||||
// composer's own role-mounted dials, which legacy does not use.
|
||||
values: () => ({}),
|
||||
inputs: async () => pendingInputs || {},
|
||||
extra: () => (ranForProject && ranFor
|
||||
? { funpack_scene_id: ranFor, funpack_project_id: ranForProject } : null),
|
||||
onAdopt: (sceneId, projectId) => {
|
||||
ranFor = sceneId; ranForProject = projectId;
|
||||
// A run this PAGE queued sets ranFor itself, at click time (see generate()
|
||||
// below). A run only found on reload -- nobody here called generate() for
|
||||
// it -- has no other way to tell store.js which scene it belongs to.
|
||||
fire("adopt", { sceneId, projectId });
|
||||
},
|
||||
});
|
||||
|
||||
/** The slot+role pair a pipeline role points at, or null if nothing offers it. */
|
||||
function slotForRole(at) {
|
||||
for (const slot of PS.slots() || []) {
|
||||
for (const role of slot.roles || []) {
|
||||
if (role.at === at) return { slot, role };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
window.GenerateBridge = {
|
||||
DONE, FAILED, CANCELLED,
|
||||
viewUrl: (image) => viewUrl(image),
|
||||
state: () => run.state,
|
||||
subscribe: (fn) => run.subscribe(fn),
|
||||
on: (kind, fn) => {
|
||||
(listeners[kind] || (listeners[kind] = [])).push(fn);
|
||||
return () => { listeners[kind] = (listeners[kind] || []).filter((f) => f !== fn); };
|
||||
},
|
||||
cancel: () => run.cancel(),
|
||||
waitForTerminal: () => waitForTerminal(run),
|
||||
ready: session.ready,
|
||||
slotForRole,
|
||||
/** Queue one scene's generation. `inputs` is {slotId: {inputName: value}}. */
|
||||
async generate({ sceneId, projectId, inputs } = {}) {
|
||||
ranFor = sceneId || null;
|
||||
ranForProject = projectId || null;
|
||||
pendingInputs = inputs || null;
|
||||
return session.generate();
|
||||
},
|
||||
};
|
||||
@@ -121,6 +121,10 @@
|
||||
<script src="api.js"></script>
|
||||
<script src="dom.js"></script>
|
||||
<script src="pipeline_state.js"></script>
|
||||
<!-- Module scripts always defer, so this runs after every classic script
|
||||
above regardless of position -- placed here only for readability next
|
||||
to the pipeline state it depends on (window.PipelineState). -->
|
||||
<script type="module" src="generate_bridge.js"></script>
|
||||
<script src="settings_window.js"></script>
|
||||
<script src="appearance_settings.js"></script>
|
||||
<script src="floating_window.js"></script>
|
||||
|
||||
+233
-220
@@ -3038,8 +3038,6 @@
|
||||
return raw;
|
||||
}
|
||||
|
||||
let pollTimer = null;
|
||||
let progressTimer = null;
|
||||
let pollStart = 0;
|
||||
// Wall clock for the WHOLE operation the user started, as distinct from pollStart, which
|
||||
// restarts on every run of a multi-run montage. The transport buttons count from the press
|
||||
@@ -3060,83 +3058,17 @@
|
||||
function _markGenInFlight(ids) { (ids || []).forEach((id) => _genInFlightIds.add(id)); }
|
||||
function _clearGenInFlight(ids) { (ids || []).forEach((id) => _genInFlightIds.delete(id)); }
|
||||
|
||||
function _clearGenTimers() { clearInterval(pollTimer); clearInterval(progressTimer); }
|
||||
|
||||
// Ask ComfyUI to stop the current generation. The running poll then resolves as
|
||||
// "Interrupted" and the run loop stops.
|
||||
// Ask ComfyUI to stop the current generation. GenerateBridge's own run.cancel()
|
||||
// (app/shell/run.js) knows the real state-agnostic /api/jobs/.../cancel route
|
||||
// and falls back to /interrupt on an older ComfyUI -- reused rather than
|
||||
// duplicated here (see that file's own comment on why plain /interrupt alone
|
||||
// silently does nothing for a job still waiting behind another one).
|
||||
async function interrupt() {
|
||||
_interrupted = true;
|
||||
updateGenProgress({ msg: "Interrupting…" });
|
||||
try { await API.interrupt(); } catch (_) {}
|
||||
}
|
||||
|
||||
// Re-attach to a generation that was already running when the UI (re)loaded. The job
|
||||
// lives in ComfyUI's still-running queue; bridge.active_generation hands back its
|
||||
// prompt id + the project/scenes it targets. We restore the "running" gen state so
|
||||
// Generate stays blocked and Interrupt is offered — exactly like a live run — and,
|
||||
// when the matching project is loaded, record the result onto its scenes on completion.
|
||||
async function resumeRunningGeneration(act) {
|
||||
if (!act || !act.running || !act.prompt_id) return false;
|
||||
_interrupted = false;
|
||||
// A reloaded montage can have further runs still queued behind the running one. Re-attach
|
||||
// to each in turn so Generate stays blocked (and outputs get recorded) until the queue
|
||||
// drains — not just for the one prompt that happened to be running at reload.
|
||||
let cur = act;
|
||||
let lastPromptId = null;
|
||||
// Re-attaching means we never saw the press, so the button's clock can only count from
|
||||
// here. Flagged approximate so it reads "1m 4s+" rather than claiming a total it can't know.
|
||||
_genClockStart("all", true);
|
||||
try {
|
||||
while (cur && cur.running && cur.prompt_id && cur.prompt_id !== lastPromptId && !_interrupted) {
|
||||
lastPromptId = cur.prompt_id;
|
||||
pollStart = Date.now();
|
||||
const sceneIds = (cur.scene_ids && cur.scene_ids.length)
|
||||
? cur.scene_ids
|
||||
: (cur.only_scene ? [cur.only_scene] : []);
|
||||
const more = cur.pending > 0 ? ` · ${cur.pending} run(s) queued` : "";
|
||||
set({ gen: { state: "running", promptId: cur.prompt_id, media: [], msg: `Generation running (reconnected after reload)…${more}` } });
|
||||
if (state.project && cur.pid && state.project.id === cur.pid && sceneIds.length) {
|
||||
// Full re-attach: poll history and place the output on the run's scenes when done.
|
||||
_markGenInFlight(sceneIds);
|
||||
try { await _pollPromise(cur.prompt_id, sceneIds, "Generation running (reconnected)"); }
|
||||
finally { _clearGenInFlight(sceneIds); }
|
||||
} else {
|
||||
// No matching project loaded (or single-scene run on another project): we can't map
|
||||
// the output, but still surface Interrupt and clear busy when it stops.
|
||||
await _monitorActive(cur.prompt_id);
|
||||
}
|
||||
if (_interrupted) break;
|
||||
// Another queued run may now be executing — re-attach to it.
|
||||
try { cur = await API.active(); } catch (_) { cur = null; }
|
||||
}
|
||||
} finally { _genClockStop(); }
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lightweight queue watcher used when we can't fully re-attach (see above). Polls
|
||||
// /active until the prompt leaves the queue or the user interrupts.
|
||||
function _monitorActive(promptId) {
|
||||
return new Promise((resolve) => {
|
||||
_clearGenTimers();
|
||||
pollTimer = setInterval(async () => {
|
||||
if (_interrupted) {
|
||||
_clearGenTimers();
|
||||
set({ gen: { state: "idle", promptId, media: [], msg: "Interrupted." } });
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const a = await API.active();
|
||||
if (!a || !a.running || a.prompt_id !== promptId) {
|
||||
_clearGenTimers();
|
||||
set({ gen: { state: "done", promptId, media: [], msg: "Generation finished. Open Settings ▸ Temp files to view or save the output, then regenerate to place it." } });
|
||||
resolve();
|
||||
} else {
|
||||
updateGenProgress({ msg: `Generation running (reconnected) ${_elapsed()}` });
|
||||
}
|
||||
} catch (_) { /* transient — keep watching */ }
|
||||
}, 2000);
|
||||
});
|
||||
const GB = window.GenerateBridge;
|
||||
if (GB) { try { await GB.cancel(); } catch (_) {} return; }
|
||||
try { await API.interrupt(); } catch (_) {} // bridge not loaded — last resort
|
||||
}
|
||||
|
||||
function _elapsed() {
|
||||
@@ -3449,83 +3381,160 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Poll a single queued prompt to completion. Resolves true on success, false on error.
|
||||
function _pollPromise(promptId, targetSceneIds, prefix) {
|
||||
prefix = prefix || "Generating…";
|
||||
return new Promise((resolve) => {
|
||||
_clearGenTimers();
|
||||
let pendingStreak = 0;
|
||||
let transientStreak = 0;
|
||||
// Faster step-progress poll (sampler current/total steps, plus what it says it's
|
||||
// doing — "scene 2/3 · pass 2 of 2"). The message is rebuilt from prefix + elapsed
|
||||
// the same way the slower poll below builds it, rather than regex-stripping the last
|
||||
// one off the end: a suffix the strip pattern doesn't know about (the phase label)
|
||||
// would otherwise be re-appended every tick and grow without limit.
|
||||
progressTimer = setInterval(async () => {
|
||||
if (_interrupted) return;
|
||||
try {
|
||||
const pr = await API.progress();
|
||||
if (pr && pr.max > 0) {
|
||||
const phase = pr.label ? ` · ${pr.label}` : "";
|
||||
updateGenProgress({
|
||||
step: pr.value,
|
||||
maxStep: pr.max,
|
||||
phase: pr.label || "",
|
||||
msg: `${prefix} ${_elapsed()} · sampling ${pr.value}/${pr.max}${phase}`,
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
}, 700);
|
||||
pollTimer = setInterval(async () => {
|
||||
if (_interrupted) {
|
||||
_clearGenTimers();
|
||||
set({ gen: { state: "idle", promptId, media: [], msg: "Interrupted." } });
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const s = await API.status(state.project.id, promptId);
|
||||
transientStreak = 0;
|
||||
if (s.state === "error") {
|
||||
_clearGenTimers();
|
||||
const msg = s.error ? `ComfyUI error: ${s.error}` : "Generation failed inside ComfyUI — check the ComfyUI terminal for details.";
|
||||
set({ gen: { state: "error", promptId, media: [], msg } });
|
||||
resolve(false);
|
||||
} else if (s.state === "completed") {
|
||||
_clearGenTimers();
|
||||
_recordSegment(s.media, targetSceneIds, { sceneLayout: s.scene_layout });
|
||||
set({ gen: { state: "done", promptId, media: s.media, msg: s.media.length ? "" : "Completed but no output media found — check ComfyUI terminal." } });
|
||||
resolve(true);
|
||||
} else {
|
||||
// "pending" after "running" means the job left the queue without a history
|
||||
// entry — it likely crashed or was interrupted by ComfyUI.
|
||||
if (s.state === "pending") pendingStreak++; else pendingStreak = 0;
|
||||
if (pendingStreak >= 3) {
|
||||
_clearGenTimers();
|
||||
set({ gen: { state: "error", promptId, media: [], msg: "Job disappeared from ComfyUI queue — it may have crashed or been interrupted. Check the ComfyUI terminal." } });
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const step = (state.gen.maxStep > 0) ? ` · sampling ${state.gen.step}/${state.gen.maxStep}` : "";
|
||||
const phase = state.gen.phase ? ` · ${state.gen.phase}` : "";
|
||||
updateGenProgress({ state: s.state, msg: `${prefix} ${_elapsed()}${step}${phase}` });
|
||||
}
|
||||
} catch (e) {
|
||||
if (_isTransientTunnelError(e)) {
|
||||
transientStreak++;
|
||||
if (transientStreak < 120) {
|
||||
updateGenProgress({
|
||||
msg: `${prefix} ${_elapsed()} · tunnel reconnect (${transientStreak})…`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
_clearGenTimers();
|
||||
set({ gen: { ...state.gen, state: "error", msg: _friendlyGenError(e.message) } });
|
||||
resolve(false);
|
||||
}
|
||||
}, 2000);
|
||||
// The extension is what actually tells kinds apart, not which ComfyUI output
|
||||
// key a file arrived under -- SaveVideo hands its file back through the same
|
||||
// generic "images" list the frontend's own gallery mechanism reuses for every
|
||||
// output kind (see app/shell/run.js's `handle()`), so the key alone can't
|
||||
// tell a produced video from a produced image.
|
||||
function _kindForFilename(name) {
|
||||
const ext = String(name || "").split(".").pop().toLowerCase();
|
||||
if (ext === "gif") return "gifs";
|
||||
if (["mp4", "webm", "mov", "mkv"].includes(ext)) return "videos";
|
||||
if (["mp3", "wav", "flac", "ogg", "m4a"].includes(ext)) return "audio";
|
||||
return "images";
|
||||
}
|
||||
|
||||
// Which scene(s) + status prefix the run GenerateBridge is currently driving
|
||||
// belongs to -- set by _generateRun right before it queues, or by the
|
||||
// "adopt" handler below when the run was found on reload instead of started
|
||||
// by this page.
|
||||
let _genRunPrefix = "Generating…";
|
||||
let _genRunSceneIds = [];
|
||||
let _genBridgeWired = false;
|
||||
|
||||
// One persistent subscription translating GenerateBridge's real run state
|
||||
// (app/shell/run.js -- ComfyUI's own /prompt + /ws, not a second queue of
|
||||
// FunPack's own) into the state.gen shape every existing status/progress/
|
||||
// clock UI already renders. Wired once; every _generateRun call after that
|
||||
// just sets what the NEXT status message should say and lets this paint it.
|
||||
function _wireGenerateBridge() {
|
||||
if (_genBridgeWired) return;
|
||||
const GB = window.GenerateBridge;
|
||||
if (!GB) return;
|
||||
_genBridgeWired = true;
|
||||
|
||||
GB.on("say", (msg) => {
|
||||
if (!msg) return;
|
||||
set({ gen: { state: "error", promptId: (state.gen && state.gen.promptId) || null, media: [], msg } });
|
||||
});
|
||||
GB.on("hold", (msg) => {
|
||||
set({ gen: { state: "queuing", promptId: null, media: [], msg: msg || "" } });
|
||||
});
|
||||
GB.on("release", (runState) => {
|
||||
// Only idle is worth clearing back to Ready here -- any other phase is
|
||||
// already being painted by the subscribe() below, and stomping it with
|
||||
// a stale "release" delivery would flash the status backwards.
|
||||
if (runState.phase === "idle") set({ gen: { state: "idle", promptId: null, media: [], msg: "" } });
|
||||
});
|
||||
// A run this page did not start itself -- found on reload via ComfyUI's
|
||||
// own /queue or /history, not anything remembered client-side (see
|
||||
// app/shell/client.js). There is no prefix for it, and no scene selected
|
||||
// it, so both are set from what the bridge found instead of a click.
|
||||
GB.on("adopt", ({ sceneId, projectId }) => {
|
||||
_genRunPrefix = "Generation";
|
||||
_genRunSceneIds = sceneId ? [sceneId] : [];
|
||||
if (projectId && (!state.project || state.project.id !== projectId)
|
||||
&& (state.projects || []).some((p) => p.id === projectId)) {
|
||||
loadProject(projectId).then(notify);
|
||||
}
|
||||
});
|
||||
|
||||
GB.subscribe((runState) => {
|
||||
const { phase, promptId, progress, images, audio, error } = runState;
|
||||
if (phase === "idle") return; // the resting state — nothing new to draw
|
||||
if (phase === "queued") {
|
||||
set({ gen: { state: "queuing", promptId, media: [], msg: `${_genRunPrefix}: queuing…` } });
|
||||
} else if (phase === "running") {
|
||||
const step = progress ? ` · sampling ${progress.value}/${progress.max}` : "";
|
||||
set({
|
||||
gen: {
|
||||
state: "running", promptId, media: [],
|
||||
step: progress ? progress.value : 0, maxStep: progress ? progress.max : 0,
|
||||
msg: `${_genRunPrefix}: generating… ${_elapsed()}${step}`,
|
||||
},
|
||||
});
|
||||
} else if (phase === "done") {
|
||||
const mediaList = [...(images || []), ...(audio || [])]
|
||||
.map((f) => ({ ...f, kind: _kindForFilename(f.filename) }));
|
||||
if (mediaList.length && _genRunSceneIds.length) _recordSegment(mediaList, _genRunSceneIds, {});
|
||||
set({
|
||||
gen: {
|
||||
state: "done", promptId, media: mediaList,
|
||||
msg: mediaList.length ? "" : "Completed but no output media found — check the ComfyUI terminal.",
|
||||
},
|
||||
});
|
||||
} else if (phase === "failed") {
|
||||
set({
|
||||
gen: {
|
||||
state: "error", promptId, media: [],
|
||||
msg: error ? `ComfyUI error: ${error.message}` : "Generation failed inside ComfyUI — check the ComfyUI terminal for details.",
|
||||
},
|
||||
});
|
||||
} else if (phase === "cancelled") {
|
||||
set({ gen: { state: "idle", promptId, media: [], msg: "Interrupted." } });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// What a run actually sends for the prompt: the scene's own text for a
|
||||
// single-scene run, or the full verbatim montage (anchor + every active
|
||||
// scene's text, transitions included) for a multi-scene one -- the same
|
||||
// split `generate()`/`generateMontage()` already make below. Plus
|
||||
// project.video's width/height, wherever the pipeline asks for them, by
|
||||
// role.input name -- same convention app/boot.js's syncVideo() uses.
|
||||
//
|
||||
// Reference-image wiring (assets.source_image/reference_N, app/boot.js's
|
||||
// wireReferences()) is NOT built here yet: v4's legacy project/scene shape
|
||||
// doesn't round-trip source_image/references through api.js's
|
||||
// getProject/saveProject adapter today, so there is nothing yet to wire —
|
||||
// a placeholder left for the next pass, not a silent gap.
|
||||
async function _buildQueueInputs(targetSceneIds) {
|
||||
const raw = {};
|
||||
const GB = window.GenerateBridge;
|
||||
if (!GB || !state.project) return raw;
|
||||
|
||||
for (const slot of (window.PipelineState.slots() || [])) {
|
||||
for (const role of (slot.roles || [])) {
|
||||
if (role.at !== "project.video") continue;
|
||||
const v = state.project[role.input];
|
||||
if (v === undefined) continue;
|
||||
raw[slot.id] = { ...(raw[slot.id] || {}), [role.input]: v };
|
||||
}
|
||||
}
|
||||
|
||||
const found = GB.slotForRole("generation.prompt");
|
||||
if (!found) return raw;
|
||||
const text = targetSceneIds.length > 1
|
||||
? buildGlobalPromptFromTimeline(state.project)
|
||||
: (() => {
|
||||
const sc = scene(targetSceneIds[0]);
|
||||
const anchor = (state.project.anchor || "").trim();
|
||||
const own = ((sc && sc.text) || "").trim();
|
||||
return [anchor, own].filter(Boolean).join(" ");
|
||||
})();
|
||||
if (text == null) return raw;
|
||||
try {
|
||||
const res = await fetch("/funpack/api/prompt/expand", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
text, anchor: state.project.anchor || "", postfix: state.project.postfix || "",
|
||||
postfix_enabled: !!state.project.postfix_enabled, variables: state.project.variables || [],
|
||||
// No per-run sampler seed is reachable from here (nothing threads a
|
||||
// real generation seed through this path yet) — a fresh one per
|
||||
// queue gives real variety across separate Generate clicks, same
|
||||
// as app/boot.js's own queueInputs().
|
||||
seed: Math.floor(Math.random() * 2 ** 31) || 1,
|
||||
}),
|
||||
});
|
||||
const expanded = res.ok ? (await res.json()).text : text;
|
||||
raw[found.slot.id] = { ...(raw[found.slot.id] || {}), [found.role.input]: expanded };
|
||||
} catch (_) {
|
||||
// The literal typed text still queues — the run just runs without
|
||||
// anchor/shortcuts/$variables applied, rather than being blocked by a
|
||||
// prompt-craft feature failing.
|
||||
raw[found.slot.id] = { ...(raw[found.slot.id] || {}), [found.role.input]: text };
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
async function _pollFfmpegJob(statusFn, jobId, clipCount, busyLabel) {
|
||||
@@ -3630,6 +3639,8 @@
|
||||
|
||||
// "Reference image 2" (input_sources "prevframe") auto-fills from whatever scene rendered
|
||||
// immediately before the one being generated — only meaningful for a single-scene run.
|
||||
// Currently unused: v5's reference-image wiring (assets.reference_N) isn't built for
|
||||
// legacy scenes yet (see _buildQueueInputs's own comment) — kept for when it is.
|
||||
function _prevSceneMedia(onlyScene) {
|
||||
if (!onlyScene) return null;
|
||||
const scenes = (state.project?.scenes || []).filter((s) => !s.excluded);
|
||||
@@ -3639,65 +3650,71 @@
|
||||
return media && media.filename ? { filename: media.filename, subfolder: media.subfolder || "" } : null;
|
||||
}
|
||||
|
||||
// Generate one run through v5's real check -> queue -> terminal flow
|
||||
// (GenerateBridge, wired in generate_bridge.js over app/shell/run.js and
|
||||
// session.js). `resetSession` (Studio-session reset) and `extraOverrides`
|
||||
// (REINS captures, the anchor-guide i2v bypass) are v4 Studio/Chain-Sampler
|
||||
// machinery with no v5 module providing them yet -- said loudly rather than
|
||||
// silently dropped, per the port plan's "leave placeholders for features".
|
||||
async function _generateRun(sceneIds, onlyScene, prefix, resetSession, extraOverrides) {
|
||||
_interrupted = false;
|
||||
_markGenInFlight(sceneIds);
|
||||
set({ gen: { state: "queuing", promptId: null, media: [], msg: `${prefix}: queuing…`, step: 0, maxStep: 0 } });
|
||||
try {
|
||||
const overrides = [...(_anchorGuideNodeOverrides(sceneIds) || []), ...(extraOverrides || [])];
|
||||
const r = await _retryOnTunnel(
|
||||
() => API.generate(state.project.id, onlyScene || null, onlyScene ? null : sceneIds, !!resetSession, overrides, _prevSceneMedia(onlyScene)),
|
||||
10,
|
||||
);
|
||||
if (!r.prompt_id) { set({ gen: { ...state.gen, state: "error", msg: "No prompt id returned." } }); return false; }
|
||||
_queueRenderPrompts(sceneIds);
|
||||
if (r.validation && state.project) {
|
||||
state.project.generation_meta = {
|
||||
...(state.project.generation_meta || {}),
|
||||
prompt_hash: r.validation.prompt_hash,
|
||||
run_hash: r.validation.run_hash,
|
||||
};
|
||||
}
|
||||
// The builder reports values the user SET that did not reach the graph — a linked
|
||||
// input pointing at a node that was replaced, a widget renamed out from under it. The
|
||||
// run is valid, it just isn't the one they configured, and nothing used to say so:
|
||||
// the whole report was being thrown away here.
|
||||
const ignored = (r.report && r.report.ignored) || [];
|
||||
// Positive confirmation, console-only: which linked inputs fired and with what value.
|
||||
// Checking that a project setting actually reached the graph should not require
|
||||
// reading the queued prompt, but it is not worth a UI element on every run either.
|
||||
const fired = ((r.report && r.report.wired) || []).filter((w) => w.startsWith("linked "));
|
||||
if (fired.length) console.info("[FunPack] linked inputs sent this run:\n " + fired.join("\n "));
|
||||
const notices = [];
|
||||
if (ignored.length) {
|
||||
notices.push(ignored.length === 1
|
||||
? ignored[0]
|
||||
: `${ignored.length} settings didn't reach the pipeline — ${ignored[0]}`);
|
||||
}
|
||||
// A predecessor render existed but its last frame/video couldn't be retrieved (temp
|
||||
// file gone, no ffmpeg) — must not look identical to "scene 1, nothing to chain from".
|
||||
// A scene can have both a prevframe AND a prevvideo socket, so both can fail at once —
|
||||
// collect every match, not just the first, or one silently drops.
|
||||
const prevChainMisses = ((r.report && r.report.unsatisfied) || [])
|
||||
.filter((u) => u.includes("previous scene's last frame") || u.includes("previous scene's video"));
|
||||
notices.push(...prevChainMisses);
|
||||
if (notices.length) state.notice = notices.join(" — ");
|
||||
pollStart = Date.now();
|
||||
let runMsg = `${prefix}: generating…`;
|
||||
if (r.prompt_repairs_cleared) {
|
||||
const anchorOnly = r.validation?.anchors_changed_since_last_queue && !r.validation?.text_changed_since_last_queue;
|
||||
runMsg += anchorOnly
|
||||
? " · guides refreshed, stale repairs cleared"
|
||||
: " · stale repairs cleared (training kept)";
|
||||
}
|
||||
else if (r.reset_session) runMsg += " · Studio session reset";
|
||||
set({ gen: { state: "running", promptId: r.prompt_id, media: [], msg: runMsg } });
|
||||
return await _pollPromise(r.prompt_id, sceneIds, prefix);
|
||||
} catch (e) {
|
||||
set({ gen: { state: "error", promptId: null, media: [], msg: _friendlyGenError(e.message) } });
|
||||
const GB = window.GenerateBridge;
|
||||
if (!GB) {
|
||||
set({ gen: { state: "error", promptId: null, media: [], msg: "Generation isn't wired up yet — reload the page." } });
|
||||
return false;
|
||||
}
|
||||
_wireGenerateBridge();
|
||||
if (resetSession) {
|
||||
console.warn("[FunPack] a Studio session reset was requested, but v5 has no Studio session to reset yet — ignored.");
|
||||
}
|
||||
const overrides = [...(_anchorGuideNodeOverrides(sceneIds) || []), ...(extraOverrides || [])];
|
||||
if (overrides.length) {
|
||||
console.warn("[FunPack] this run asked for per-node overrides (REINS captures / the anchor-guide "
|
||||
+ "i2v bypass) that v5's generate flow does not support yet -- ignored:", overrides);
|
||||
}
|
||||
const targetSceneIds = (sceneIds || []).filter(Boolean);
|
||||
if (!targetSceneIds.length) {
|
||||
set({ gen: { state: "error", promptId: null, media: [], msg: "No scenes to generate." } });
|
||||
return false;
|
||||
}
|
||||
_genRunPrefix = prefix || "Generating…";
|
||||
_genRunSceneIds = targetSceneIds;
|
||||
_markGenInFlight(targetSceneIds);
|
||||
set({ gen: { state: "queuing", promptId: null, media: [], msg: `${_genRunPrefix}: queuing…`, step: 0, maxStep: 0 } });
|
||||
try {
|
||||
let inputs;
|
||||
try {
|
||||
inputs = await _buildQueueInputs(targetSceneIds);
|
||||
} catch (e) {
|
||||
set({ gen: { state: "error", promptId: null, media: [], msg: `Could not build the prompt: ${e.message}` } });
|
||||
return false;
|
||||
}
|
||||
const waiting = GB.waitForTerminal();
|
||||
let queued;
|
||||
try {
|
||||
queued = await GB.generate({
|
||||
sceneId: onlyScene || targetSceneIds[0],
|
||||
projectId: state.project ? state.project.id : null,
|
||||
inputs,
|
||||
});
|
||||
} catch (e) {
|
||||
waiting.cancel();
|
||||
set({ gen: { state: "error", promptId: null, media: [], msg: _friendlyGenError(e.message) } });
|
||||
return false;
|
||||
}
|
||||
if (!queued) {
|
||||
// Refused before it ever reached run.start() (an incomplete pipeline,
|
||||
// or the queue itself saying no) -- the bridge's own "say" hook
|
||||
// already turned the reason into state.gen, and nothing will ever
|
||||
// transition the run this was waiting on.
|
||||
waiting.cancel();
|
||||
return false;
|
||||
}
|
||||
_queueRenderPrompts(targetSceneIds);
|
||||
pollStart = Date.now();
|
||||
return (await waiting) === GB.DONE;
|
||||
} finally {
|
||||
_clearGenInFlight(sceneIds);
|
||||
_clearGenInFlight(targetSceneIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4628,21 +4645,17 @@
|
||||
await loadModels();
|
||||
window.addEventListener("funpack-models-changed", loadModels);
|
||||
await refreshProjectList();
|
||||
// Re-attach to a generation already running in ComfyUI (UI was reloaded mid-run).
|
||||
// Load its project first so Generate is blocked, Interrupt is shown, and the result
|
||||
// lands on the right scenes — then skip the welcome page, there's work in flight.
|
||||
// Re-attach to a generation already running in ComfyUI (UI was reloaded mid-run) is
|
||||
// GenerateBridge's own job (generate_bridge.js, over app/shell/session.js's wire()) --
|
||||
// it asks ComfyUI's own /queue and /history directly, not anything remembered here,
|
||||
// and starts the moment its module loads. `ready` resolves once it knows whether it
|
||||
// found one; the "adopt" hook (see _wireGenerateBridge) loads the right project and
|
||||
// scene for it as soon as it does. Skip the welcome page in that case — there's work
|
||||
// in flight.
|
||||
let resumed = false;
|
||||
_wireGenerateBridge();
|
||||
try {
|
||||
const act = await API.active();
|
||||
if (act && act.running && act.prompt_id) {
|
||||
if (act.pid && (state.projects || []).some((p) => p.id === act.pid)
|
||||
&& (!state.project || state.project.id !== act.pid)) {
|
||||
await loadProject(act.pid);
|
||||
}
|
||||
notify();
|
||||
resumeRunningGeneration(act); // fire-and-forget poll loop
|
||||
resumed = true;
|
||||
}
|
||||
if (window.GenerateBridge) resumed = await window.GenerateBridge.ready;
|
||||
} catch (_) { /* queue unreachable — boot normally */ }
|
||||
notify();
|
||||
// One front door: the full-screen welcome, unless a running generation was resumed.
|
||||
|
||||
Reference in New Issue
Block a user