Three things that were each locked to one implementation detail:
- steps + schedule were a KSampler-branch feature, though every sampler here
walks a sigma list handed in from outside. They are now pass-level, so
Distilled Flow and Hybrid Euler 2S can run a named schedule (and the second
pass can be computed rather than typed) in both frontends and on the node.
- the project's Frames field snapped to the model's grid on every family. On
H3 that is required, but its arrows moved by 1 and the snap put the value
straight back; on LTX the builder rounds up by itself, so the snap only
fought the user. H3 now steps a whole 17 frames per arrow, LTX keeps what
you type and walks 8k+1 with its arrows.
- ALG lived inside the Distilled Flow step loop, so it was unreachable with a
stock KSampler. Its blurred/sharp swap is decided by the step's sigma, and
sigma is an argument of every model call, so a denoiser proxy drives it from
outside any sampler. New alg_anchor toggle on the Chain Sampler drives the
in-loop version too, so there is one control wherever you are.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the Engine Settings 'Reference media' tab. Any image / video / audio in
the Media Bin (Editor) or Gallery (Easy Gen) takes an 'R' mark beside the identity
pin; any number can carry one, and mark ORDER is the numbering the badge shows.
Each marked item then appears as an input source in Models & Pipeline — 'R1 ·
face.png (image)' — for every socket its kind can fill, so references reach a node
the same way the timeline image does.
At generation the marked files are copied into ComfyUI's input folder and the
builder injects one loader per reference, chosen by what the DESTINATION socket
asks for (IMAGE / AUDIO / VIDEO), shared when two sockets take the same one. A
reference whose file is gone, or one no installed node can load into that socket,
is reported against the socket that wanted it instead of silently unwired.
The editor no longer feeds Studio's built-in ref2va list: reference media is
wired now, not configured. Project.h3_references stays readable so old project
files still load, but nothing writes or sends it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
H3 support was written against ComfyUI's PR; it has since merged (57500fc5, shipped in
v0.30.0) and the weights are out. Everything the adapter assumed still holds — audio time
axis, 17k+5 grid, batch-1 forward, the imported optimized_attention symbol, every payload
key — so this is what the release itself changed, not a correction of the reading. LTX-2 /
LTXAV is untouched throughout: a project picks a family and both are first-class.
* i2v anchors now work on H3. LTX writes the image into the starting latent
(LTXVImgToVideoInplace); H3 has no such path — its anchor is a condition row packed
beside the text — so a scene's anchor image becomes a frame-0 keyframe pin instead, and
the LTX node is no longer run against an H3 latent. Studio hands the source image's
pixels to the sampler (it owns the CLIP, the sampler owns the VAE). Pins now MERGE, so
a first-frame anchor and a last-frame guide coexist: H3's own first-and-last mode.
* Frame geometry follows the model: frames per scene snap to the family's grid (LTX 8k+1,
H3 17k+5) in both frontends, in the graph builder, and in the sampler, and H3 renders at
its fixed 24 fps. Picking a family moves the project onto its geometry and says so.
* The two checkpoints are named. fl2va (anchors) and ref2va (references) load identically
and never reject each other's conditioning, so the run reports which mode it is in and
the setup panel spells out the split.
* Pipeline setup is no longer a placeholder: released, with the published filenames.
* Reference images can be encoded at the reference pipeline's 2048px short edge.
Fixes found on the way:
* A video reference lost its soundtrack between Studio and the sampler — Studio had
already announced it to the text encoder as <Audio 1>, so every later audio ordinal in
the prompt pointed one reference earlier than written.
* Easy Gen never knew it was on H3: its model config lives on the project, which the
shared capability check did not read, so every H3 warning silently reported LTX.
* An off-grid scene length failed with arithmetic instead of an explanation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The graph builder is now parameterized by model family instead of assuming LTXAV. H3
keeps the core's shape but three of its nodes do not apply: LTXVConditioning (stamps LTX's
frame_rate onto conditioning, which H3 has no field for), LTXVConcatAVLatent (H3's own
EmptyMiniMaxH3LatentAV emits both streams, so the latent feeds the sampler directly) and
LTXVAudioVAEDecode (reads an output_sample_rate H3's audio VAE does not define — it would
raise). Requirements, wirable ports, role targets, guided-wiring hints and the port picker
all follow the family, so a loader can no longer be wired into a node the graph never emits.
The family is a Models setting, asked and never guessed from a checkpoint filename: a wrong
guess fails deep inside ComfyUI at generate time instead of in the panel that can fix it.
Video references now work, rather than being refused. FunPack owns the pipeline and already
requires ffmpeg for preview/export, so minimax_h3 decodes reference clips itself — once, at
24 fps — and derives both views from that single decode: the 2 fps timestamped frames Qwen
sees, and the full-rate latent block the DiT packs. A clip's own soundtrack rides with it,
presented as its <Audio j> label immediately before its <Video k>, which is what ties the
two together. Capped at 15s because reference tokens ride every sampling step.
Editor UI: a Reference media view (H3 only — there is no LTX equivalent to grey out) with
per-type 1-based tags shown exactly as the prompt must spell them, reorder controls that
renumber both sides at once, and a soundtrack picker on video entries. Warning chips beside
Generate for the LTX-only sampler settings H3 switches off, so that is known before a
generation is spent rather than after; Easy Gen gets the same warnings on its strip and the
same family selector (it already loads models.js and pipeline_caps.js).
Verified live in the Cutting Room: the family toggle flips requirements, ports and hints
both ways and persists per project; tags renumber on reorder; the H3 chips replace the
Best-FaceID chip on H3 and hand back to it on LTXAV.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Generating (44s)", counted from the press until the finished video comes
back. Both frontends.
Editor: the clock spans the WHOLE operation, not one run. pollStart already
existed but restarts on every run of a multi-run montage, so a press that
kicks off three runs would have shown the third run's age. genWallStart is
wrapped around the public entry points (generate / generateMontage /
generateSelected) in a finally, which no early-return error path can miss.
The tick writes the button's textContent directly and never re-renders the
zone: rebuilding every button once a second would throw away focus and hover
for a number one label owns. For the same reason the elapsed value is NOT in
store state and NOT in fpActionbar — the zone still repaints on gen.state,
which is what starts and stops the ticker.
A run started from Selected counts on that button; a montage or a single
scene fired from the inspector counts on the main one. Re-attaching after a
reload reads "1m 04s+" with a tooltip saying it counts from the reconnect,
since the press happened before the page load and the real total is unknown.
Seconds are zero-padded so the label's width changes once a minute rather
than once a second; tabular figures keep it from twitching in between.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Best-FaceID reads identity from an identity_pin-tagged guide, and only the AUTO
continuity builder ever applies that tag (timeline._identity_pin_guide). Every
way of losing it is silent: the run succeeds and simply carries no identity.
Three such states, all verified against _run_sampler_inputs (tagged guides in
the emitted funpack_scene_guides payload: 2 when healthy, 0 in each bad case):
- no identity pin set at all
- custom guide stack on -- manual guides REPLACE auto guides (`manual or auto`),
and manual GuideEntry has no identity_pin field, so the pin is dropped
- auto continuity off -- same outcome by the other route
Shown on the main window of both frontends, not inside the settings pane that
turns it on: you would only see it there if you already went looking.
- shared predicate: PipelineCaps.identityTransferIssue -> {short, detail}
- Editor: amber chip beside Generate in the timeline header, click opens
Engine settings; full text in the tooltip
- Easy Gen: full-width amber strip between the preview and the Generate row
fpActionbar had to grow the identity fields -- it hashed only pid/sel/gen, so
setting the pin would have left a stale warning sitting next to Generate.
Verified live in both UIs across all five transitions (no pin -> pin -> stack on
-> stack off -> auto off -> feature off); appears and clears correctly in each.
Silent when the Chain Sampler is not in the pipeline, where the knob is inert.
Tests: 22 failed / 465 passed, unchanged from baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Easy Gen had no equivalent of the Cutting Room's reconnect-after-reload: reloading
mid-run left Generate re-enabled with no Interrupt and no progress, while the run
carried on server-side.
Nothing was missing on the backend. Easy Gen queues through the Editor's own
/generate route, so its prompts already carry the editor client_id + funpack run
identity, already come back from GET /api/active, and api.js already exposed
active(). Easy Gen simply never asked.
app.js gains reattachRunning(), run after the project is restored on boot:
- Run targets the loaded project -> full re-attach through the existing
preview.watch(), so the result still lands in the preview when it finishes.
- Run targets something else (typically a montage queued from the Cutting Room)
-> preview.monitor(), a new queue watcher over /api/active that shows progress
and Interrupt without claiming the output. It deliberately does NOT switch
projects out from under the user to take ownership of a foreign run.
- Loops while further runs are queued behind the running one, so Generate stays
blocked until the queue drains.
Also fixes a live-run bug found on the way: render() ran on every store notify
(health poll every 15s, every prompt keystroke) and unconditionally set
generateBtn.disabled = !project — re-enabling Generate underneath a generation
that was still going. Both paths now go through one setGenerating() flag, which
render() respects, and a project switch mid-run no longer tears down the poll.
preview.js: watch() takes an optional `note` for the initial label so a re-attach
says "Reconnected after reload" instead of "Starting…"; the Interrupt wiring is
shared between watch() and monitor() as _armStopButton().
Verified in the local ComfyUI: page boots clean (only ComfyUI's own pre-existing
console errors), /api/active is polled on boot, monitor() stays pending with an
armed Interrupt while a job reports running and resolves + hides when it leaves
the queue, and Generate survives a store notify. The full-reattach branch reuses
the already-validated preview.watch() path but could not be exercised end to end
locally, since a real run needs the rental GPU.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Easy Gen had no way to stop a run — the only exit was the ComfyUI queue. Adds
the same "■ Interrupt" control the Cutting Room's player readout uses (shared
.gen-interrupt styling), living inside the progress panel so it exists only
while there is something to stop.
ComfyUI records an interrupted prompt in history as status_str "error" with no
media, which the status endpoint cannot tell apart from a real crash — so watch()
remembers that the stop was asked for and reports "Generation stopped." instead
of "Generation failed.", while still showing any partial media the run wrote.
A failed interrupt call re-enables the button and says so rather than leaving a
dead control over a run that is still going.
Co-Authored-By: Claude <noreply@anthropic.com>
Follow-up to the previous commit, per review:
- the panel is no longer docked into Easy Gen's main screen — the "$ Variables"
button now sits in the prompt bar (next to Upload/Gallery, not pinned to the
header) and opens a modal window, same shell as the Gallery picker
- value fields are auto-growing textareas instead of one-line inputs, in the
Cutting Room's Composer as well: a long phrase was impossible to edit in a
single-line field. Height is measured from scrollHeight with the border-box
borders added back, re-fit once after layout (scrollHeight reads 0 before it)
- fully-empty rows are dropped on persist, so an accidental "Add variable" click
never sticks to the project (the row stays on screen until it's filled in —
the open window keeps its own working copy for exactly this reason)
Co-Authored-By: Claude <noreply@anthropic.com>
Ports the Editor Composer's $variables to Easy Gen: a "$ Variables" toggle in
the header opens a collapsible name/value editor docked above the prompt bar.
Same project.variables field the Editor writes, so nothing backend-side is new —
movie_editor/server.py already forwards it on every generate and Studio resolves
it dead-last inside refine_v2 (after shortcut expansion AND after the scene
split), which is what makes a $var work inside a shortcut's replacement while a
value containing a comma or trigger word can never move a scene cut.
- easy_store: projectVariables/setProjectVariables (leading $ stripped on store)
- easy_variables.js: rows + Add/Remove, count badge, live hint for undeclared
names and self-referencing loops (cycle detection mirrors resolve_variables)
- rows rebuild only on add/remove/project-change, so typing never loses focus
Co-Authored-By: Claude <noreply@anthropic.com>
The Generation panel's Seed field was bound to Project.seed, a legacy field
(movie_editor/backend/timeline.py:562) that nothing in generation reads —
the real seed resolution (server.py's _resolve_run_seed) only ever reads
sampler_inputs.seed, which already defaults to random when unset. The field
always showed "1" and editing it had zero effect, reading as "seed is
stuck, not randomized" when generation was in fact already randomizing
correctly via the real (and already-present) Seed control under Engine ▸
Timing & Seed. Removed the dead field; the settings hint now points there.
Also bumps pyproject.toml to 3.4.0 and adds the CHANGELOG entry for this
session's Easy Gen work (shortcut/transition import, Engine settings, Save/
Gallery/Export buttons, continuity pin, and the bypass/state-sync/
autoplay/filename fixes), ahead of tagging the release on main.
Mirrors mediabrowser.js's card pin button in the Editor: a 📌 button on
each image thumbnail (chain-sampler pipelines only) toggles
continuity_settings.identity_pin_ref, re-enabling auto_enabled if it was
off (a pin does nothing with auto continuity disabled). Same setting as
Engine settings > Continuity > Identity pin, just reachable without
leaving the Gallery. Verified both surfaces read/write the same
continuity_settings and stay in sync.
Easy Gen's "Save" persists into FunPack's own project store on that
machine, same as the Editor's autosave — it was never a way to move a
project between machines. The backend route for that (download the actual
.funpack_project.json, re-importable via "Load Project File...") already
existed and easy_gen/frontend/api.js already had the URL helper, but no
button anywhere called it, unlike the Editor's File > "Save Project File...".
Added an "Export..." button next to Save that flushes a save first, then
downloads the project file.
Root cause of "bypass I just disabled is still bypassed after reopening
settings": models.js saves bypass/expose/widget edits straight to the
server via its own PUT /projects/{id}/models, entirely independent of Easy
Gen's state.project. Any later save() (prompt debounce, the Save button, an
upload) sent the STALE in-memory project.models back to the server and
silently clobbered whatever was just changed there — same class of bug the
Editor's store.js already guards against by syncing state.models into
project.models before every autosave commit, which Easy Gen never got.
Fixed by refetching the authoritative models config at the top of every
save() call. Verified live: toggling bypass via the Models API then calling
Store.save() no longer reverts it.
Also:
- preview.js: video elements used the `autoplay` attribute, which makes the
browser call .play() internally with no promise we can catch. Replacing
the element before that settles (e.g. picking a new Gallery item) surfaced
as an unhandled "fetching process...aborted" rejection banner. Now calls
.play() ourselves so we own (and can catch) the promise.
- server.py: the /result route had no Content-Disposition, so "Save Video
As" fell back to a generic name derived from the URL path ("result.mp4")
every time. The underlying render file is already uniquely named per run
and already lives in ComfyUI's temp dir (cleared on restart) — just
wasn't surfaced to downloads. Now sets Content-Disposition with the real
per-run filename on all three /result response paths.
- Add the Cutting Room's "Log" button/panel to Easy Gen (log.js is already
fully self-contained and hits the same shared /api/log route Easy Gen's
api.js already calls) — just needed the script tag and a .menubar-right
wrapper for it to mount into.
- Fix: any Store notify (health poll every 15s, prompt debounce) rebuilt the
preview's <video>/<img> from scratch, reapplying autoplay/loop and silently
resuming anything the user had paused. draw() now skips the rebuild when
the actual content being shown hasn't changed.
- Hide the "expose to editor" eye buttons (both plain widgets and the bypass
toggle) in Models & Pipeline when running under Easy Gen — there's no main
editor / per-scene inspector for an exposed control to ever appear in, so
they were inert clutter that could look actionable and silently do
nothing. Gated on window.FunPackAppName, same discriminator engine_settings.js
and sampler_panel.js already use; Movie Editor is unaffected.
Easy Gen has no rating UI and won't get one — so every Engine setting that
is a no-op without a trained refinement key/rated history is now hidden
there (gated on window.FunPackAppName, the existing Easy-Gen discriminator),
not just harder to find: refinement key, value guidance, steer mode,
reference injection, embed/score/output guidance, taste retrieval,
DynaShift, and sampler-panel velocity bias/rescue. A note points users to
the Cutting Room or the ComfyUI graph for the full learned refiner.
Also enforced at the pipeline level, not just the UI: every Easy Gen
generate call now forces the Studio node into "Prompt only" mode and the
rating-gated Chain Sampler knobs off via node_overrides, so a project
edited in the Cutting Room with these enabled still generates plainly when
run from Easy Gen. Movie Editor is untouched — FunPackAppName is unset
there, so every added gate evaluates false.
Easy Gen shipped shortcuts/transitions and multi-scene splitting with no way
to load a shortcut/transition JSON library, no path to Engine Settings
(blocking Generate on stale continuity media refs, no Best-FaceID toggle),
no explicit Save, and no way to reuse an already-uploaded file as an anchor.
- Shortcut/transition JSON import/export in the Shortcuts settings section.
- Full Engine Settings (Studio/Chain Sampler/Continuity/Best-FaceID) now
loads via the same frontend-fallback as autocomplete.js, backed by new
patchProject/setSamplerInput*/setStudioInput*/mediaBin support in
easy_store.js. Removed the now-duplicate Refinement key field from the
slim Generation panel.
- New Gallery picker (easy_gallery.js) to apply prior uploads as the anchor.
- Explicit Save button with Saving…/Saved feedback.
The one prompt textarea is now the verbatim "global prompt" and silently
splits into N scenes via the same backend parser the Editor's Composer
uses (POST /projects/{id}/parse) — no per-scene UI is ever shown, and
Generate already runs the whole project as one chain request (no
scene_ids filter), so N scenes still come back as one stitched video.
- easy_store.js: applyGlobalPrompt()/scheduleGlobalPromptApply() port
the Editor's split+anchor-fold logic (_normalizeGlobalPromptParse,
_inferSplitMarkers), sequence-guarded against a stale parse response
racing a newer keystroke. Scene 0 keeps its uploaded source across
re-splits; scenes 1..N default to "carry" (continue the chain).
Also added getEditorSetting/setEditorSetting (same localStorage key
as the Editor) and loadLibraries() (shortcuts + transitions).
- easy_shortcuts_settings.js (new): "Shortcuts" settings section —
autocomplete + anchor toggles, plus the shortcut revolver (server-side,
shared with the Editor and with generation regardless of which UI
queued the run). Skips the Editor's "ideas" bulb and its custom-node
anchor-guide bypass section, neither relevant here.
- app.js: prompt box now drives scheduleGlobalPromptApply() instead of
editing scene 0 directly, displays project.global_prompt, and attaches
ShortcutAutocomplete.
Verified live: multi-scene split + anchor fold match real project data,
upload survives a re-split, autocomplete fires against the real shortcut
library, revolver toggle round-trips, and Generate submits the whole
multi-scene project correctly (fails at the same "missing pipeline
nodes" point as before on this bare dev install — no regression).
Preview now shows the uploaded image/video (badged "Uploaded source")
when there's no generated result yet, or the generated result itself
with a corner ✕ to dismiss it and reveal the upload underneath.
Fixed two real bugs found while testing this:
- Upload never actually attached to the scene: POST /media returns
{media: [...]} (a list, for multi-file support), but app.js read the
response as if it were the media object itself.
- A save-ordering race could silently lose an edit: two overlapping
save() calls could land on the server out of order, letting an
older save clobber a newer one. save() now serializes through one
queue in easy_store.js, always reading live project state.
- CSS: .easy-preview-close's unconditional display:flex was beating
the hidden attribute (equal specificity); added an explicit
[hidden] override.
Easy Gen is a new Fooocus-style frontend at /funpack/easy — big preview,
one prompt box, Upload, Generate, and an Advanced settings panel — built
entirely on the Movie Editor's existing project format and API (no new
backend endpoints), so projects stay interchangeable between the two UIs.
The Hub at /funpack links to both UIs and surfaces version/branch/commit
plus an Update/Restart action reusing the Editor's git_update.py flow.
Startup now prints full host:port URLs for all three (Hub, Editor, Easy
Gen) via a new config.comfy_display_url() helper.
Also: settings_window.js's About section now labels itself via
window.FunPackAppName instead of hardcoding "Cutting Room", and Easy
Gen's boot-error banner accumulates distinct errors instead of
overwriting, so a cascading failure (e.g. dom.js not loading) surfaces
its root cause instead of just the last script in load order.