Commit Graph
1248 Commits
Author SHA1 Message Date
digital-garbageandClaude Opus 5 eddeccaf68 fix: restore the GuideEntry dataclass, repairing the i2v guide stack
7106db0 added scene_anchor_media_refs by writing it over the "@dataclass /
class GuideEntry:" header lines. The class body survived as unreachable code
after that function's return, so GuideEntry was referenced in three places but
never defined.

Effect: every code path that switches the per-scene i2v guide stack on raised
NameError -- build_scene_guides_payload and continuity_media_refs both died as
soon as guide_settings.stack_enabled was true. It went unnoticed for seven
weeks because the stack defaults off and no test covered it.

Fix moves the stranded body back out of scene_anchor_media_refs and restores
the two lost header lines. The body is byte-identical to b9f6e74; only its
position and header changed. scene_anchor_media_refs is untouched.

Adds two regression tests, both verified to fail against the unfixed file.
Tests: 22 failed / 462 passed (baseline 22/460 -- +2 passing, 0 new failures).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:31:50 +03:00
digital-garbageandClaude Opus 5 8d4ed22853 maint: remove template helpers orphaned by the payload removal
json_safe and maybe_store_string were reachable only through
collect_template_payload (removed in 055b256). Caught by re-running the
orphan scan after that commit rather than assuming the first pass was final.

Checked: 1 occurrence each repo-wide (grep -a), 0 refs in ComfyUI core/FunUI.
Tests: 22 failed / 460 passed, identical to baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:17:36 +03:00
digital-garbageandClaude Opus 5 51093cde70 maint: remove unused leading-trigger regex cache
_leading_trigger_re plus its _TRIGGER_RE / _TRIGGER_RE_BUILT module globals.
The globals were touched only inside the function itself, so the whole
memoisation block was self-contained and unreachable.

Checked: 1 occurrence repo-wide (grep -a), not a route handler, not in
NODE_CLASS_MAPPINGS, not an INPUT_TYPES widget, 0 refs in ComfyUI core/FunUI.
Tests: 22 failed / 460 passed, identical to baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:17:02 +03:00
digital-garbageandClaude Opus 5 b68a83d6d9 maint: remove unreferenced movie_editor backend helpers
source_needs_anchor_media, is_chain_only_source_type (+ its now-orphaned
_CHAIN_ONLY_SOURCES constant), _job_snapshot, default_slot_extras,
_ui_output_links.

Kept deliberately: DEFAULT_WIRES_BY_ROLE and DEFAULT_INPUT_SOURCES_BY_ROLE
survive default_slot_extras — both are still exported via the slot-defaults
payload at pipeline_wiring.py:235-236.

Checked: 1 occurrence each repo-wide (grep -a), not route handlers, not in
NODE_CLASS_MAPPINGS, not INPUT_TYPES widgets, 0 refs in ComfyUI core/FunUI.
Tests: 22 failed / 460 passed, identical to baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:15:55 +03:00
digital-garbageandClaude Opus 5 7810ae7a5f maint: remove unused template payload/summary helpers
collect_template_payload and template_field_summary. Both from the original
template-manager commit; the surviving /funpack/templates routes build their
responses directly and never call either.

Checked: 1 occurrence each repo-wide (grep -a), not route handlers, not in
NODE_CLASS_MAPPINGS, not INPUT_TYPES widgets, 0 refs in ComfyUI core/FunUI.
Tests: 22 failed / 460 passed, identical to baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:15:03 +03:00
digital-garbageandClaude Opus 5 233333b97e maint: remove orphaned {a|b} wildcard engine
resolve_wildcards + its WILDCARD_RE pattern. Both arrived with the Creative
Template node (514e29d / 5f1c2ad); that node is no longer registered, and the
{a|b} syntax is handled nowhere else in Python or JS, so the resolver has had
no caller since the node was dropped.

User impact checked: 0 occurrences of {a|b} in the user's shortcuts file, so
no live prompt depended on this expanding.

Checked: 1 occurrence each repo-wide (grep -a), not a route handler, not in
NODE_CLASS_MAPPINGS, not an INPUT_TYPES widget, 0 refs in ComfyUI core/FunUI.
Tests: 22 failed / 460 passed, identical to baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:14:41 +03:00
digital-garbageandClaude Opus 5 2ace4dc4d7 maint: remove dead refiner-state template cluster
conditioning_from_refiner + its four exclusive helpers (load_refiner_state,
choose_prompt_history, best_history_conditioning, prompt_key_for_refiner).
Closed cluster: the only references to any of them were from each other.

Checked: 1 occurrence each repo-wide (grep -a, incl. NUL-byte frontend files),
not a route handler, not in NODE_CLASS_MAPPINGS, not an INPUT_TYPES widget,
no getattr/setattr or monkey-patch use, 0 refs in ComfyUI core and FunUI.
Tests: 22 failed / 460 passed, identical to baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:14:07 +03:00
digital-garbageandClaude Opus 5 44a9373524 Release 3.4.1: CHANGELOG, README, version bump
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:17:06 +03:00
digital-garbageandClaude Opus 5 2011b18b95 fix: don't reject projects saved with the old context_window_schedule names
Renaming the combo's choices to core's spellings broke queueing for anyone who
had one of the old ones saved: ComfyUI validates combo VALUES at queue time,
before the node ever runs, so the alias map added with the rename never got a
chance. The three legacy names are back in the list (they resolve through the
alias map at runtime), and the Editor now displays a stored legacy value as the
name it maps to and writes that back, so the dropdown can't show one setting
while another runs.

And that rejection was unreadable. ComfyUI's top-level `error` for a validation
failure is a fixed blob ("Prompt outputs failed validation", details ""); the
part naming the node, the widget and the bad value is in `node_errors`, which
`error or node_errors` never reached because the blob is always truthy. Every
rejected prompt therefore reported the same nothing. Both are formatted now:

  Prompt outputs failed validation; FunPackLTXAVSceneChainSampler: Value not in
  list — context_window_schedule: 'static_standard' not in [...]

Also: the progress poll no longer imports anything. current_progress() reads
run_phase's label straight off `sys`. It fires every 700ms on ComfyUI's event
loop WHILE the worker thread samples, and that is not a path to do import work
on — the direct read is both cheaper and the correct one, since run_phase keeps
its state there anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:10:10 +03:00
digital-garbageandClaude Opus 5 6457512838 second_pass_op: show the upsampler it depends on, and say why it was skipped
Answering "how does sharpen operate — I never saw the upsampler download?":
it uses the same file segmented detailing does, resolved the same way (any
model already in models/latent_upscale_models wins; the ~1 GB official file is
downloaded only when that folder is empty). So no download is expected on a
machine that already has one — but nothing in the Editor said so.

The upsampler picker rendered only under Experimental, and only while segmented
detailing was on, so choosing 'sharpen' alone gave no model control and no hint
that a 1 GB file was involved. It now also renders in the Second pass group
whenever an operation is selected, and the knob's hint states the dependency
and that the pass still runs (operation skipped, reason in the scene report)
when the file can't be obtained.

That reason is also worth something now: a failure said only "no latent
upsampler could be loaded", which is the same message for a missing
huggingface_hub, a failed download and an unreadable file. The real cause is
carried into the scene report and printed to the console once — previously only
the successful download printed, so a silent skip read as the op having run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:00:12 +03:00
digital-garbageandClaude Opus 5 7681c599a0 progress: say which scene and which pass is running; fix context_window_schedule
Three things, all about a run telling you what it is doing.

1. The second pass is now visible. ComfyUI's progress channel carries only
   (value, max), so a chain sampling some scenes twice was one anonymous bar.
   The sampler publishes a live label ("scene 2/3 · pass 2 of 2") through the
   new run_phase module; /api/progress hands it to the editor, which appends it
   to the readout, and pass 2 also announces itself on the console the moment it
   starts rather than only in the after-the-fact run report.

   run_phase keeps its state on `sys`, not in module globals: the writer reaches
   it package-relative and the reader imports FunPack modules top-level by name,
   so they can hold different module objects and the label would never arrive.

2. The progress bar total now counts both passes. It only ever counted the main
   schedule, so with a second pass on it overflowed during scene 1 and jumped
   backwards at scene 2, whose offset is a multiple of the per-scene stride.

3. context_windows was unusable. The knob offered "uniform_standard" etc.;
   core's names are the other way round ("standard_uniform"), so every schedule
   except "batched" raised ValueError out of the sampler and FAILED THE RENDER.
   Core's names are the choices now, the old spellings are aliased so saved
   projects keep working, and an unknown name is refused with a reason naming
   what core accepts instead of crashing.

   Same call had a second, quieter break: a keyword core's handler doesn't take
   (latent_retain_index_list is absent on some builds, including this Mac's)
   raised TypeError, which was reported as "ComfyUI core too old" — silently
   switching off a feature core fully supports. Extras are dropped now, with a
   note naming the one knob that stops working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:57:21 +03:00
digital-garbageandClaude Opus 5 39533dfde4 anchor_shift: remove the shift, keep only the head cut
The two-pass shift regrew the frames it dropped, and the invented ending
consistently came out with worse or missing movement — not worth paying
steps for. What is left is the part that works: generate the scene exactly
as normal, anchor pinned at full strength the whole way, then cut the
opening off the finished clip. Nothing is regrown, so the scene comes out
that much shorter and the audio is cropped to match.

Six knobs collapse into one INT, cut_opening_frames (0 = off), in the
Editor under Chain Sampler ▸ Timing & Seed rather than Experimental.
Gone with the shift: the mid-schedule split, the rewind re-noise
(_renoise_to_sigma), the tail refill modes, the fresh-audio reset, and the
schedule_context/continue_from_state plumbing that existed only so a split
scene would not re-time the sampler inside each half.

second_pass is untouched — it never used any of that machinery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:36:13 +03:00
digital-garbageandClaude Opus 5 1dd27101aa second_pass: say plainly that the sampler does the re-entry, and what the first sigma costs
User asked why there is an "img2img re-noise" at all — "you just pass the original latent
from 1st pass as latent_image into 2nd pass and that's it, sampler does everything itself."
That is exactly what the code does. Pass 1's latent goes in as latent_image with
continue_from_state off, so _sample_chunk calls prepare_noise and comfy's own noise_scaling
handles the entry; _renoise_to_sigma is never called on this path (it is anchor_shift's
only, for its genuinely mid-trajectory case). Between the passes there are just two things,
one of them usually nothing: the optional op (skipped at "none") and re-attaching the i2v
noise_mask so pass 2 does not re-denoise the anchor.

So no code change — but the docs said "re-noising the finished clip", which named comfy's
internal step as if it were machinery I had added, and that is what prompted the question.
Reworded to say the sampler does it.

The same wording hid the answer to their actual symptom (softness). CONST scaling is
x = s*noise + (1-s)*picture, so the schedule's first sigma is a literal strength: at 0.812
pass 2 starts from 81% fresh noise over 19% of the pass-1 picture with 4 steps to resolve
it, which is a very strong img2img and is expected to read soft. Both tooltips now give the
0.8 / 0.4 / 0.2 scale so the dial is legible instead of implicit.

Suites at baseline: 22 pre-existing failures, 467 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 14:05:49 +03:00
digital-garbageandClaude Opus 5 63f225e231 second_pass: both schedules run in FULL — remove the cut I invented
User, after a live run: "First pass runs its own FULL schedule. Then 2nd pass begins.
If I want to stop 1st pass earlier - I remove sigmas from schedule... 'Continue from'
is ONLY relevant in our shifting technique which is a separate entity."

Correct, and the cut was mine, not theirs. I carried anchor_shift's cut/restart
semantics into a feature that has no use for them. Consequences they hit on the rental:

- Steps. Pass 1 was cut at the second schedule's first sigma, so a 9-step main plus a
  4-step second pass ran 3+4=7, not 13. They noticed the missing steps.
- Distortion. With the cut, the between-pass operation ran on a MID-TRAJECTORY latent,
  so "sharpen" was sharpening the noise still in it. It only makes sense on a finished
  clip.

Now: pass 1 runs the main schedule in full, exactly as written. The operation runs on
that finished clip. Pass 2 runs second_pass_sigmas in full, exactly as written, entering
the way any img2img refine does — comfy's ordinary CONST scaling on a clean latent, so
continue_from_state stays off. Total steps are the two schedules added up. To make pass 1
shorter, shorten the main schedule.

second_pass_sigma and second_pass_restart_sigma are deleted outright: with both schedules
run as written there is nothing to cut and nothing to continue from. The second schedule's
first sigma is now simply the strength dial, the way it is in every img2img refine.
_second_pass_plan is replaced by _second_pass_schedule, which only validates.

Also reported: "I have only sharpening enabled but log tells me upsampling is enabled as
well." The load line printed "[FunPackSceneChain] segmented detailing upsampler: ..."
regardless of which feature pulled it in, and sharpen legitimately loads that model —
so a sharpen-only run announced a feature that was off. Now "latent upsampler loaded".

Kept: the malformed-schedule refusals (ascending, or not ending at 0) — both are silent
in the output rather than loud at runtime, which is how the distorted run read.

tests/test_second_pass.py rewritten around the corrected shape, including that a 9-step
main plus a 4-step second pass is 13. Suites at baseline: 22 pre-existing failures, 467
passing. Verified live that typing a schedule sets second_pass with no cut knob leaking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:55:48 +03:00
digital-garbageandClaude Opus 5 1c6afe2e4d second_pass: in the Editor it is just the schedule — enable and cut derived from it
User: "'2nd pass starts at' and '2nd pass re-enters at' are confusing - just give me
the schedule to operate and that's it." They are right, and it was two controls saying
the same thing in different words: with an explicit pass-2 schedule, where pass 1 stops
and where pass 2 re-enters are both simply that schedule's first sigma.

The Editor now has ONE control for this: the Second pass schedule field. Typing a
schedule turns the second pass on and sets the cut to its first sigma; clearing it turns
the pass off (both sampler inputs are unset, not left behind). Cut == entry, so the
hand-over is exact and nothing is re-noised — the case that needed the re-entry sigma
does not arise. Only the optional between-pass operation is left beside it.

Frontend-only derivation (syncSecondPassFromSchedule), so the node keeps its full
range: second_pass / second_pass_sigma / second_pass_restart_sigma are still there for
graph users with no schedule wired, where they are the only way to express a rewind.
A single number is not treated as a schedule.

Also cleaned up after my own UI verification: earlier passes had left anchor_shift=true
in the README demo project's sampler_inputs. Unset and saved — that was my residue, not
a setting the user picked.

Verified live: no "Enable second pass" / "starts at" / "re-enters at" controls remain in
the Editor; typing 0.812, 0.6, 0.35, 0.15, 0.0 yields second_pass=true and
second_pass_sigma=0.812, and clearing the field removes both. Suites at baseline: 22
pre-existing failures, 469 passing across both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:45:13 +03:00
digital-garbageandClaude Opus 5 8d0e7ffaa3 second_pass: expose the pass-2 schedule in the Editor, move it out of Experimental
Three corrections from the user, all right.

1. The pass-2 schedule needed no graph-topology change at all — I was wrong to leave
   it out. Studio has ALWAYS emitted a second schedule: _build_samplers builds both
   samplers_cfg["high"] and ["low"], and low_pass_sigmas is output 7. The Editor simply
   never wired it (sampler_panel.js even says so: "outputs 6+7 are not connected").
   So this is one CORE_LINKS entry, "second_pass_sigmas": ("studio", 7), plus surfacing
   the low pass's existing `sigmas` text field. No new node, no new output.

   Only the SIGMAS is taken, not the low-pass SAMPLER: pass 2 reuses the sampler
   configured above it. So the panel deliberately shows just that one field rather than
   the whole low-pass sampler section — rendering its algorithm settings would offer
   knobs that quietly do nothing, which is the thing the editor rules forbid.

2. No re-entry sigma in the Editor. With an explicit pass-2 schedule the first sigma of
   that schedule IS where pass 2 re-enters, so second_pass_restart_sigma was a redundant
   second way to say the same thing. Dropped from the Editor; still on the node for
   graph users with no second schedule wired, where it is the only way to say it.

3. Not experimental. At its defaults the split is behaviour-neutral by construction —
   pass 2 resumes from exactly the state pass 1 handed over, same step count, same
   output — so calling it experimental overstated it. Moved from Chain Sampler >
   Experimental to FunPack Studio > Sampler algorithm, next to the schedule field it
   depends on, and the EXPERIMENTAL / UNVALIDATED LIVE framing is gone from both
   tooltips.

Verified live in the running app: the Sampler algorithm pane shows the Second pass
group and the schedule field, the re-entry knob is gone, and typing a schedule persists
to studio_settings.samplers.low.sigmas (confirmed the high pass is untouched). Suites at
baseline: 22 pre-existing failures / 306 passing, movie_editor 163 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:42:37 +03:00
digital-garbageandClaude Opus 5 16045e086c second_pass: optional latent operations between the passes (none by default)
Per the user's reminder that these are OPTIONS, not defaults: second_pass_op is a
combo defaulting to "none", and nothing runs unless it is picked. The expensive one
is therefore not a tax on anything — it costs what it costs only when chosen.

- "sharpen": one forward of Lightricks' trained 2x LatentUpsampler, resampled straight
  back to the original size. No video-model calls, so a fraction of a step; pass 2 then
  re-denoises the sharpened latent, which is what makes it stick. Adds detail consistent
  with what is there and CANNOT fix wrong structure — the same limit segmented
  detailing's sharpen mode documents.
- "upscale_2x": same upsampler, result KEPT at 2x, so pass 2 runs at 4x the pixels and
  the scene decodes at double resolution. 3-5x the second half's cost.

Reuses detailing.py's _run_upsampler / _downscale_to and its lazily-loaded upsampler
(same model, same detail_upsampler knob, "auto" downloads the official file) rather
than adding a second copy of any of it.

Ordering that matters: the op runs BEFORE the rewind re-noise, because sharpening a
latent that has just had noise added back sharpens the noise.

Two failure modes handled rather than left implicit:
- upscale_2x changes the latent resolution, so the chunk's i2v anchor and mask are the
  wrong size. Rescaling them would be inventing an anchor, so the pin is dropped and
  the scene report SAYS the pass runs unpinned and may drift from the reference image.
- A missing or throwing upsampler degrades to "second pass, no operation" with the
  reason, instead of killing a whole montage over an optional sharpen.

Found while writing the tests: an unrecognised op name was validated only AFTER the
upsampler forward had already run, so a typo paid full price and then reported the
wrong reason. Name is checked first now.

Editor + Easy Gen get the combo under Engine > Experimental; verified live that it
renders with none/sharpen/upscale_2x and defaults to none. Suite at baseline: 22
pre-existing failures, 306 passing.

Not built: downscale-then-rescale (DeepShrink-shaped). It is not a between-passes op —
pass 1 itself has to run at reduced resolution, which means downscaling the chunk, its
mask and the anchor before pass 1, so it needs its own insertion point rather than
this hook.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:35:29 +03:00
digital-garbageandClaude Opus 5 b7fa3756f1 Chain Sampler: general "Enable second pass", with an optional separate schedule
anchor_shift built the two-pass machinery — stop the schedule, hand the state over,
re-enter exactly (_sample_chunk continue_from_state, _renoise_to_sigma, _schedule_view).
The user's point: that is generally useful, not anchor-specific. This exposes it on its
own, and keeps the two features independent as they asked: reaching the second_pass
branch means anchor_shift did not act on this scene, so they can never nest.

  second_pass                 stop partway and finish in a second pass
  second_pass_sigma           where pass 1 stops (walked against the real schedule)
  second_pass_restart_sigma   where pass 2 re-enters; 0 = carry straight on
  second_pass_sigmas          OPTIONAL SIGMAS socket: pass 2 runs THAT schedule instead

At restart 0 with no second schedule the split is behaviour-neutral by construction —
pass 2 resumes from the exact handed-over state, nothing added, and 5+4 steps on a
9-step schedule is still 9. It earns its keep when the second half is made different:
re-entering higher (the model reworks what it committed to) or finishing on a different
schedule entirely, so the tail can have its own step count, spacing or shift.

Two things a split gets silently wrong if you let it, both handled:
- The i2v anchor. _sample_chunk drops noise_mask from what it returns, so pass 2 would
  run UNPINNED and re-denoise the reference frame. anchor_shift wants exactly that;
  a plain second pass must not. _restore_pinned_prefix puts the mask back AND restores
  the pinned frames from the original chunk, since a rewind re-noise adds noise
  everywhere including frames that are meant to be frozen.
- The sampler's own phases. Pass 2's schedule_context is the alternate schedule at
  offset 0 when one is connected, or the main schedule at the slice offset when not —
  so quality steps / final-correction window / AB2 ramp are measured against whichever
  schedule pass 2 is actually running.

Every refusal names its fix (cut outside the schedule, entry below the cut, a
single-sigma SIGMAS input that is not a schedule) and lands in the scene report as
second_pass(SKIPPED: ...) — never a silent no-op.

Editor + Easy Gen get the three widgets under Engine > Experimental. The SIGMAS socket
is node-graph only for now: the Editor's fixed graph takes sigmas from ("studio", 5) and
has no second scheduler node, so exposing it there is a graph-topology change I have not
made unilaterally.

Latent/pixel-space pass-2 operations (sharpen, downscale->rescale, upscale) are next,
as opt-in options with nothing on by default.

New tests/test_second_pass.py: 10 tests over the split, the separate schedule, the
refusals and the pin. Suite at baseline: 22 pre-existing failures, 299 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:31:07 +03:00
digital-garbageandClaude Opus 5 71e001afff anchor_shift: sigma 0 = no second pass, just cut the head off the finished clip
Reported by the user: setting the shift sigma to 1 or 0 with restart 0 got them
nowhere, and what they wanted was "generate me a full video and don't make the 2nd
pass, just cut off the beginning".

That was a gap, not a crash. Both extremes fell through to the splitter, which
correctly refused (a cut at the first sigma leaves pass 1 no step; a cut at the last
leaves pass 2 none) and reported a skip — so anchor_shift silently did nothing and
the skip reason did not name the thing they were actually after.

anchor_shift_sigma=0 now means it: a cut at sigma 0 IS the end of the schedule, so
pass 1 is the entire run and there is nothing left to denoise. The scene samples
exactly as if anchor_shift were off — anchor pinned at full strength the whole way,
which is the point — and the slide is applied to the FINISHED latent. Zero extra
sampling: the cheapest form of this feature by a wide margin, and with tail=crop it
is plainly "generate the video, then delete the opening". Widget min lowered 0.05 -> 0.

Two things are refused rather than half-worked, each naming its fix:
- tail=extend_last / empty, because with no second pass a refilled tail is never
  denoised — extend_last would freeze the last frame for anchor_shift_frames and
  empty would leave an empty latent on screen. crop and wrap are pure rearrangements
  of finished content, so both are allowed.
- a non-zero restart sigma, which has nothing to restart.
And anchor_shift_fresh_audio is forced off in this mode (reported, not silent):
resetting the audio with nothing left to regenerate it would just produce a silent
clip.

The old "schedule never reaches ..." skip reason now points at sigma=0 as the way to
run the whole schedule and only cut the head, so the next person hitting the extreme
is told what to set.

Suite at baseline: 22 pre-existing failures, 289 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:25:56 +03:00
digital-garbageandClaude Opus 5 9f7e8bb3c6 anchor_shift: add tail=crop — no regrown tail, the clip just ends earlier
User's idea, as an OPTION (default stays extend_last). The three existing tail modes
all refill the freed end so the clip keeps its length, which means pass 2 has to invent
an ending and blend a joint — the source of the "thin ending" and of most of the reason
to reach for a rewind restart. 'crop' regrows nothing: the scene ends anchor_shift_frames
earlier and every surviving frame was generated as part of one continuous shot.

Checked the model side before assuming this was free, since a bad latent size would
mismatch rather than degrade:
- LTXAV's video patchifier is SymmetricPatchifier(1) -> temporal patch size 1, so there
  is no divisibility rule on video latent frames.
- The streams reach the transformer as independent tensors (separate_audio_and_video_
  latents just unbinds the list); no video:audio ratio is enforced there.
- comfy re-packs per sample_custom call and re-derives latent_shapes, and the denoise
  masks off them (comfy/samplers.py outer_sample), so nothing is baked in from
  scene-build time and a shorter latent needs no conditioning rebuild.
- VAE decode is (f-1)*time_scale+1 for any f >= 1.
So no size is rejected. What IS real is proportion: audio timings come from the audio
stream's own index and video's from its own RoPE, so cropping the two by different
amounts of TIME desyncs the clip silently — no error, just drifting sound. Each extra
stream's KEPT length is therefore derived from the new video length directly rather than
by subtracting a separately-rounded drop, so rounding cannot leave them describing clips
of different durations. fresh_audio zeroes after the crop, at the new length.

Downstream needed nothing: _prepend_soft_continuation already clamps to the previous
scene's real length and _derived_overlap clamps to the stream, so a shorter scene flows
through the chain as-is.

The scene report now states the cropped length in latent frames and in real frames,
because a scene coming out shorter than the length that was set must never be something
the user has to infer from the output.

Tests: crop shortens instead of refilling and the surviving frames are the untouched
originals; audio/video stay in proportion (including four rates that do not divide
evenly); fresh_audio resets at the cropped length; the other three modes still keep the
clip length. Suite at baseline: 22 pre-existing failures, 288 passing (+5).

Verified live in the running app: the Engine ▸ Experimental dropdown offers
extend_last/wrap/empty/crop, default unchanged, hint present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:15:01 +03:00
digital-garbageandClaude Opus 5 4c2a6b6b54 anchor_shift: the sampler must see the real schedule, not its own half of it
Raised by the user: "it should read the actual sampler schedule." They were right,
and this is a bigger deal than anchor_shift.

Both FunPack samplers derive their phase boundaries from len(sigmas) — the tensor
they are handed. anchor_shift runs a scene as two sample_custom calls over slices of
one schedule, so each half re-derived every phase INSIDE the slice:

  _sample_const_rf_full (Hybrid Euler 2S, and the LTXAV path of it)
    - quality phase: late_start = pct of the SLICE. On the user's 9-step schedule at
      high_quality_pct 0.35 the quality phase starts at sigma 0.603; split at 0.482 it
      starts at 0.812 in pass 1 and 0.482 in pass 2.
    - motion-pulse anchors keyed by step index within the slice
    - eta decay + velocity-bias ratio measured against the slice's first sigma
    - the euler->Heun correction ramp restarted per call
  sample_funpack_distilled_flow
    - final-correction window: the last N steps OF THE SLICE, so pass 1 ran a "final"
      Heun correction in the middle of the trajectory
    - AB2 ramp w = i/(total-1) restarted from 0 in pass 2
    - velocity-bias sigma ratio against the slice's first sigma

So turning anchor_shift on silently re-timed the sampler's own machinery in both
halves. Present since 015f274 and a far better explanation for a visible joint or a
thin ending than anything in the shift itself.

Fix: _schedule_view(sigmas, schedule_context) -> (real schedule, step offset). The
chain sampler passes {"sigmas": <the scene's schedule>, "offset": <slice start>} via
extra_options (the same chunk-scoped mechanism the alg_guide_* options already use,
always written even as None so it cannot leak into a later chunk). Both samplers take
the kwarg; every phase-relative quantity now reads the schedule and every step index
is step_abs = i + offset. No context = a normal single call = sigmas IS the schedule,
offset 0, byte-identical behaviour.

_anchor_shift_split_sigmas also returns pass 2's index into the schedule, and its
docstring now states the boundary rule the way the user framed it: walk the schedule,
the last pre-shift step is the one whose NEXT sigma has crossed the threshold. On
1.000 0.955 0.893 0.812 0.715 0.603 0.482 0.241 0.121 0.0 with shift 0.482 and restart
0.812 that gives pass 1 = 1.000..0.482 and pass 2 = 0.812..0.0, which is now asserted
directly as a test on that schedule.

Not changed, deliberately: sample_funpack_hybrid_euler_2s's own non-CONST loop (LTXAV
always takes the RF port, so no anchor_shift run reaches it) and segmented detailing's
stage-2 tail (a separate refine pass over a finished latent with its own schedule by
design, not a slice of one).

Suite at baseline: 22 pre-existing failures, 283 passing (+4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:59:39 +03:00
digital-garbageandClaude Opus 5 5aac3cc5c1 Easy Gen: re-attach to a running generation after a page reload
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>
2026-07-31 12:37:18 +03:00
digital-garbageandClaude Opus 5 2b26c89ebb anchor_shift: restore the design — pass 1 always stops at the shift sigma
Reported by the user from a scene report: with a restart sigma set, the log said
anchor_shift_sigma was ignored. It was. That is my doing, and it should never have
shipped that way.

The original implementation did what was designed: cut pass 1 at anchor_shift_sigma,
shift, restart pass 2 at anchor_shift_restart_sigma, the two independent. It produced
noise artifacts because pass 2 re-entered through comfy's CONST scaling, which assumes
a CLEAN latent. There were two ways to fix that — write a re-noise that is valid on a
half-denoised latent, or make pass 1 always run to completion so the input is clean.
327f544 took the second. That silently reduced a two-knob design to one knob per mode,
was described in the commit as the modes being "genuinely different" rather than as a
parameter being removed, and left the user sweeping a value that could not do anything.

This does the work that should have been done then.

_anchor_shift_renoise: given x_a mid-trajectory and a target b >= a,

    x_b = alpha*x_a + beta*n,  alpha = (1-b)/(1-a),  beta = sqrt(b^2 - (a*alpha)^2)

alpha reproduces x0's coefficient exactly, beta tops the noise term back up to b, so
the result lands on the manifold the model expects at b. Both old modes are special
cases of the one formula: b == a gives alpha=1, beta=0 (an exact resume) and a == 0
gives comfy's own fresh-noise scaling. beta has a real root only when b >= a, which is
why a restart BELOW the cut is now refused with a message saying why — noise can be
added back, never removed.

Streams wiped by anchor_shift_fresh_audio are not mid-trajectory, so they re-enter at
a=0 and get the full b*n a from-scratch run starts from, not a partial top-up of a
latent that no longer exists.

Continue mode (restart 0, the validated config) is unchanged and still bit-identical:
no re-noise, cost one full schedule. Rewind now costs the cut plus the restart tail
rather than a whole schedule plus the tail.

The scene-report NOTE about an ignored parameter is gone, because nothing is ignored.
Tooltips in samplers.py and engine_settings.js (Editor + Easy Gen share it) rewritten.

Tests: the old test asserting pass 1 runs to completion is replaced by one asserting it
stops at the cut; plus the re-noise identity at b==a, equivalence with comfy's scaling
at a==0, the coefficient/variance invariants on a synthetic latent with known x0 and
eps, reset-stream handling, non-mutation, and the below-the-cut refusal. Suite at
baseline: 22 pre-existing failures, 279 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:31:35 +03:00
digital-garbageandClaude Opus 5 a106d3ae2d anchor_shift: record the frames-to-drop finding in both tooltips
Live result: 48 frames (6 latent) was needed for a quick-cut prompt to open
strictly on the requested action - the default 8 removes only the anchor and
leaves the settling-in stretch where the shot is still departing the reference
still and nothing happens yet.

Cited in the tooltips as a starting point FOR THIS PIPELINE (768x768x305@30 i2v
chain, quick-cut prompt), not a universal default, and the node default stays
8 - one workflow's tuned value is not evidence for another's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 22:42:36 +03:00
digital-garbageandClaude Opus 5 f607425ea9 anchor_shift: say when rewind mode ignores the shift sigma
Rewind mode runs pass 1 over the whole schedule (so the re-noise gets a clean
latent), which means anchor_shift_sigma has no effect there at all. A knob that
silently does nothing reads as broken - the scene report now names it and says
to set restart sigma to 0 if you want the cut point to matter. Noted in both
tooltips too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 22:35:59 +03:00
digital-garbageandClaude Opus 5 9ed53c8346 Fix anchor_shift under-denoising: pass 2 was re-noising a mid-trajectory latent
Reported from the first live run: the idea works (opening reads as t2v,
character details preserved) but the output is full of noise artifacts, as if
under-denoised, with the last steps seemingly skipped. That was a real bug, not
the concept.

comfy's CONST scaling is x = sigma*noise + (1-sigma)*latent_image, and it
treats latent_image as a CLEAN image. Pass 2 was handed the PARTIALLY DENOISED
latent as latent_image, so at a 0.725 cut it entered at
0.725*fresh_noise + 0.275*picture — pass 1's content attenuated to a quarter
and buried under a full dose of new noise, with only the tail steps left to
recover. Exactly the reported symptom.

I cited segmented detailing's stage-2 tail as the precedent for this re-entry,
but that recipe feeds a FINISHED latent, where re-noising is correct. The
precedent does not transfer mid-trajectory. My error.

Fix — the two modes are now actually different things:
- CONTINUE (restart sigma 0, the default): pass 2 resumes from exactly the
  state pass 1 handed over. Passing that state as BOTH noise and latent_image
  collapses the CONST expression to an identity (s*x + (1-s)*x == x), so no
  noise is added at all. Cost still one full schedule.
- REWIND (restart sigma > 0): pass 1 now runs the WHOLE schedule to a finished
  clip first, the shift is applied to that, and pass 2 re-noises it upward and
  denoises down. That is the img2img recipe, and it is valid precisely because
  the input is finished. Costs a full schedule plus the tail.

Also fixed pass 2's progress offset, which restarted at pass 1's, so the bar
appeared to stall short of the end.

Two new tests reproduce the CONST arithmetic directly and assert the identity
holds in continue mode and does not in rewind mode. Their comfy stubs are now
snapshot/restored — the first version leaked into other modules and shifted the
suite off baseline (3 unrelated tests changed result). Back to baseline: 22
pre-existing failures, 272 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 22:32:33 +03:00
digital-garbageandClaude Opus 5 83124a37ab anchor_shift: refuse on t2v scenes, and warn when the anchor survives the slide
Found while checking whether this works in Easy Gen. It does - the knobs render
and persist there through the same shared engine_settings.js - but Easy Gen's
default scene has no anchor image, which exposed a hole: nothing checked that
the scene actually HAS a pinned i2v anchor. On a genuine t2v scene the slide
would have dropped real generated frames and regrown them at the end, for no
benefit at all. This is the "fake t2v" trick; it needs a real anchor to fake
it with. Now refused with that reason in the scene report.

Second guard: if anchor_shift_frames drops FEWER latent frames than the scene
actually pins, part of the anchor survives and stays visible - the exact
outcome the feature exists to prevent. The report now names the pinned count
and the value to raise the knob to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 22:23:24 +03:00
digital-garbageandClaude Opus 5 e158d02e46 Chain Sampler: anchor_shift - keep the real i2v anchor, then delete it
User's idea, and the framing is the point: this is "faking t2v" when it is
really i2v. Every existing way of stopping the anchor from dominating the
opening WEAKENS it first - ALG blurs it and loses character detail, and if you
under-blur you get both failure modes at once; Best-FaceID overlap tokens
approximate it and lose some detail too. This weakens nothing. The anchor is
pinned at full strength, untouched, for as long as it takes to transfer
identity/style/composition - and is then removed from the clip entirely.

Two passes:
  pass 1  sigma[0] .. anchor_shift_sigma, anchor pinned as usual
  shift   drop the first N latent frames, slide everything left, refill the
          freed tail, drop the pin (clip LENGTH is unchanged - nothing extra
          is ever generated; the dropped head is regrown at the end)
  pass 2  restart sigma .. 0, unpinned

Two passes rather than a roll between steps for a specific reason: pass 2
re-enters through comfy's CONST noise scaling (x = s*noise + (1-s)*latent),
which puts the shifted latent back ON the rectified-flow manifold. A raw
in-flight roll skips that, and project_sigmas records that this model cannot
recover from off-manifold latent states - that is what killed churn. Same
on-manifold re-entry that segmented detailing's stage-2 tail already uses.

Every choice is a knob, because WHERE to cut is the entire experiment and I
have no measurement for it: shift sigma (default 0.909, the first step where
structure actually forms - above it nothing has formed and the anchor would be
discarded before doing any work), frames dropped, pass-2 restart sigma
(0 = continue, higher = rewind for more freedom at the joint), what the
regrown tail starts from, and whether audio is rewritten in pass 2 (on by
default: pass 1's audio was scored against frames that have since moved).

Refused, loudly and with the reason in the scene report, on continuation
scenes (the head is carried frames, not an anchor), on scenes carrying guide
frames (the slide would overwrite them) and on scenes with JoyAI audio memory
(resetting audio would destroy the injected tail).

One real bug caught by the new tests while writing them: SIGMAS are float32,
so a threshold typed as 0.725 is a float64 sitting just below the schedule's
float32 0.725, and a bare `v <= s` cut one step LATER than the number entered.
Now matched against core's own isclose tolerance.

11 new tests; suite at baseline (22 pre-existing failures, no new). Editor
checked live: knobs render, gate on the toggle, persist to sampler_inputs.
UNVALIDATED LIVE - the shift sigma is yours to find.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 22:19:09 +03:00
digital-garbageandClaude Opus 5 2ea42448fd Context windows: correct the cost figure in both tooltips
overlap/length (~28%) is the per-window redundancy, not the whole-clip cost.
The real figure is stride-based: at 145/40 each window advances 105 new frames,
so a windowed scene does ~1.45x the per-frame work regardless of length. That
is offset by attention, which is quadratic in one pass and near-flat when
windowed: ~1.05x at 200 frames, 0.68x at 305, 0.42x at 500. Net: roughly
break-even around 200 frames, a real win past ~300.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:54:00 +03:00
digital-garbageandClaude Opus 5 e490530d16 Chain Sampler: plateau_cache and context_windows are mutually exclusive
The plateau step-cache is keyed by (input.shape, cond_or_uncond), and every
context window inside one step calls the model with the SAME shape. With both
toggles on, windows 2..N would have been handed window 1's cached prediction
on every plateau step - one window's content bleeding across the whole clip,
silently, on a combination that is two ticked checkboxes away.

Context windows now wins and the cache is skipped, with the reason in the
scene report and in both tooltips, rather than quietly honouring one of two
requested settings. Caught by reasoning through the interaction, not by a
live run - neither feature has been validated on a rental yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:53:07 +03:00
digital-garbageandClaude Opus 5 e01af72445 Context windows on the Chain Sampler + fix upsampler load on ComfyUI v0.29.0
Two things, both driven by upstream ComfyUI moving 7 weeks ahead of the
local checkout (2cdaaf4a 2026-06-06 -> e651b7be 2026-07-28, 263 commits).

1. Segmented detailing crashed on ComfyUI v0.29.0. Upstream f8a3fd9d moved
   the LTX latent upsampler onto DynamicVram and made `operations` a required
   argument of LatentUpsampler.from_config; detailing.py still called it with
   one arg, so the pending live probe would have died before detecting
   anything. Now passes comfy.ops.disable_weight_init (what core's own loader
   does) with a TypeError fallback, since rentals run whatever they pulled.

2. New Chain Sampler option: context_windows. Upstream cd77c551 (CORE-3)
   added context-window sampling with real LTX2/LTXAV support - it unpacks
   the packed AV latent, maps each video window onto its proportional audio
   window, and re-slices denoise masks, keyframe_idxs and
   guide_attention_entries per window, so anchors, mid-scene guides and JoyAI
   memory survive inside a window. Nothing is ported: we build core's handler
   and hang it on the scene's model_options exactly as the stock
   LTXVContextWindows node does. Core picks it up in
   comfy.samplers.calc_cond_batch, which every FunPack sampler already routes
   through, so no sampler-loop change was needed and no graph change either -
   it rides the sampler_inputs bridge into both Editor and Easy Gen.

   Refuses (loudly, once per run, with a scene-report note) on cores without
   BaseModel.map_context_window_to_modalities: the handler class predates
   LTXAV support and would window the packed tensor blindly, which is the
   same packed-vs-unpacked trap that killed DynamicConditioning. Install and
   removal are per-scene under the existing wrapper-hygiene finally, so an
   interrupt cannot leak the handler or its two patcher wrappers into later
   runs.

   Knobs (all exposed, none hardcoded): length, overlap, schedule, blend,
   FreeNoise, and pin-anchor-in-every-window. Off by default. Engages only
   on scenes longer than the window, and says so in the scene report when it
   does not, so a scene that sampled whole never reads as a broken feature.

Verified: 8 new unit tests for the install/remove/capability-gate contract;
full suite at baseline (22 pre-existing cross-module stub failures, no new).
Editor checked live - knobs render in Engine > Chain Sampler > Experimental,
dependents gate on the toggle, and values persist into sampler_inputs.

UNVALIDATED LIVE on a rental.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:49:31 +03:00
digital-garbageandClaude Opus 5 3948f64033 Identity transfer: support ComfyUI's new RoPE matrix layout
ComfyUI 7c59a07 ("Use comfy kitchen rope functions in ltx models", 2026-07-24)
changed the LTX positional-embedding payload from (cos, sin, split_mode) to
(rotation_matrix, split_mode), where the matrix is (B, T, heads, D, 2, 2)
holding [[cos, -sin], [sin, cos]].

rotate_overlap_freqs read pe[1] as the sin tensor — now a bool — so it raised,
and prepare_pe's bare `except: return pe` swallowed it. The Best-FaceID
reference tokens were still appended on the target's frame-0 coordinate grid
but WITHOUT the source-phase tag that marks them as a reference, so the model
read them as literal frame 0 and opened the clip on the reference image.
Anyone on ComfyUI >= 2026-07-24 saw i2v-like behaviour they couldn't bypass.

rotate_overlap_freqs now detects the layout at runtime (pe[1] is a tensor =>
legacy cos/sin; otherwise the matrix form, tokens on axis 1, frequencies on
-3) and composes the same R(angle + phase) either way. An unrecognised payload
raises instead of silently skipping the tag.

The four LTXBaseModel wrappers no longer fail silently: each reports once per
install that identity conditioning is not being applied and that a ComfyUI
update is the likely cause. A logged warning would have made this obvious.

Tests: both layouts rotate only the ref-token tail, produce identical angles,
reject unknown payloads, and the matrix form survives end-to-end through
_install_identity_overlap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 17:28:35 +03:00
digital-garbageandClaude Opus 5 5af0a1b5d3 Models: treat V3 MultiType widgets as widgets, not required sockets
LTXVEmptyLatentAudio.frame_rate arrives from object_info as
("FLOAT,INT", {"widgetType": "INT", ...}) — a V3 MultiType wrapped around an
int widget. The editor split types on exact string match, so the union looked
like an unknown connection type: the field vanished from the node's widgets,
turned into a required input source no node could satisfy, and blocked
generation with 'has no source'.

Classification now goes through nodes.widget_type_of(): an input whose members
are all widget types (or that carries a widgetType hint) is a widget, rendered
and defaulted as its widget member. Genuine unions ("IMAGE,MASK") stay sockets
and match a producer of any member via type_accepts(), used by auto-wire,
bypass passthrough, the source/destination pickers and the image-target list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:25:12 +03:00
digital-garbageandClaude 3222fbcb31 Easy Gen: Interrupt button on the progress panel
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>
2026-07-26 20:22:48 +03:00
digital-garbageandClaude 8e9c0479a8 Variables: own window in Easy Gen, auto-growing value fields in both UIs
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>
2026-07-26 20:18:15 +03:00
digital-garbageandClaude 70cd015219 Easy Gen: prompt $name variables panel
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>
2026-07-26 20:11:38 +03:00
digital-garbage e29f7fea3b Refresh Hub screenshot with a clean git status 2026-07-24 12:27:00 +03:00
digital-garbage 62606d694e README: document the Hub and the two UI variants, with screenshots
Adds a Hub screenshot and a Cutting Room / Easy Gen side-by-side comparing
what each is for, plus a short 3.4.0 callout in the version history section.
2026-07-24 12:25:35 +03:00
digital-garbage f1a376e286 Remove dead "Seed" field from Easy Gen settings; bump to 3.4.0
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.
2026-07-24 12:19:49 +03:00
digital-garbage fadfc9626d Add continuity pin button to Easy Gen's Gallery
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.
2026-07-24 11:47:38 +03:00
digital-garbage 99c7dd5ba6 Add project Export button to Easy Gen
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.
2026-07-24 11:29:58 +03:00
digital-garbage 6a58b35f6f Fix Easy Gen silently reverting Models & Pipeline edits; two smaller fixes
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.
2026-07-24 11:23:23 +03:00
digital-garbage 96ba54f17d Fix bypass toggle silently doing nothing when the mapping is ambiguous
_apply_bypass() only skips a bypassed node's effect when it can find exactly
one connection-input matching each of its output types (and that input is
actually wired to something). When either condition failed — e.g. an i2v
preprocessing node with more than one output type, or a MASK/IMAGE pair with
no unambiguous 1:1 match — it silently left the node fully active and only
recorded a reason in report["unsatisfied"], which no frontend code ever
reads or displays. A user toggling bypass got no error and no bypass: the
node just kept running as if untouched.

Both failure paths now also add to report["blocking"], which server.py's
generate route already rejects with a clear "Generation blocked — ..."
error shown to the user. A bypass a user explicitly asked for now either
happens or the run stops and says why — never a silent no-op.

Updated the test that asserted the old (buggy) "leaves it active" behavior
to assert the new blocking behavior instead.
2026-07-24 11:14:51 +03:00
digital-garbage d21026b2a6 Fix shortcut autocomplete menu opening off-screen below the field
position() always placed the suggestion menu below the textarea. Easy Gen's
prompt box lives in a footer bar at the bottom of the viewport, so the menu
opened mostly or entirely off-screen with no way to see or click a
suggestion. Now flips above the field when there isn't room below, and
clamps max-height to whatever space is actually available either way.
2026-07-24 11:04:56 +03:00
digital-garbage c2d1257a8e Easy Gen: add Log button, stop preview auto-resuming, hide inert eye buttons
- 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.
2026-07-24 11:02:04 +03:00
digital-garbage cfe606fb95 Force Easy Gen into a rating-free Studio Prompt-only pipeline
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.
2026-07-24 10:50:36 +03:00
digital-garbage d4236be5fb Easy Gen: shortcut/transition library import, Engine settings, Save, Gallery
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.
2026-07-24 10:37:40 +03:00
digital-garbage 0191f71606 Add prompt autocomplete, splits, anchor, and shortcut revolver to Easy Gen
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).
2026-07-23 17:27:03 +03:00
digital-garbage fe45c422ce Show uploaded media in Easy Gen preview, with a close/remove button
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.
2026-07-23 16:58:28 +03:00
digital-garbage 9791cb8ccd Add FunPack Easy Gen (simplified single-scene UI) and a /funpack Hub
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.
2026-07-23 16:37:26 +03:00