Skip to content

Feat/v2.7 editor sam audio memory - #174

Open
AEmotionStudio wants to merge 53 commits into
mainfrom
feat/v2.7-editor-sam-audio-memory
Open

Feat/v2.7 editor sam audio memory#174
AEmotionStudio wants to merge 53 commits into
mainfrom
feat/v2.7-editor-sam-audio-memory

Conversation

@AEmotionStudio

Copy link
Copy Markdown
Owner

Feat, bug, and regression checks needed.

AEmotionStudio and others added 30 commits March 12, 2026 21:47
…UI components, alongside various skill and node enhancements.
…rame picker node and UI, and expand shader effects.
… TTS, and Foundation-1 Music, along with new nodes, shaders, and various enhancements and fixes.
…extended editor parameters, and update segment parsing logic
Thread a `model` selector ("4b" / "9b" / "9b_fp8") through the FLUX Klein
edit/remove paths so callers can pick the model size. The auto_mask handler
reads `_flux_klein_model` from pipeline metadata and passes it into the
editor's remove/edit calls; composer, pipeline assembler, and batch processor
forward the variant through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vendored Sapiens2 (Meta, ICLR 2026) for human-centric tasks — pose (308
keypoints), segmentation, surface normals, pointmaps, matting, and pretrain
features — with detector, model definitions, FP8 conversion/runtime, weight
I/O, task registry, rendering, and an orchestrator. Exposed via the
`sapiens2_synthesizer` module and registered in the VRAM manager's synthesizer
list for cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add configurable square VAE tile size and overlap to the SeedVR upscaler's
model loader and generation phases (defaults 512 / 64), and update the memory
manager accordingly so tiling can be tuned to fit available VRAM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a `sam_version` selector ("sam3.1" default / "sam3") to the Effects
Builder and Load Video Path nodes, with per-version checkpoint discovery and
model caches in core/sam3_masker.py.

For SAM 3.1 video masking, run ComfyUI's native SAM3 model in-process instead
of the upstream `sam3` pip package in a subprocess. The model loads as a
standard ModelPatcher (VRAM-managed, no CUDA-context leak), so no child
process is needed. Both text- and point-prompted tracking are supported,
reusing ComfyUI's forward_video/forward_segment, _extract_text_prompts, and
unpack_masks. The existing mirror checkpoint loads natively as-is.

The native path is gated by FFMPEGA_SAM3_NATIVE (default on) and falls back
automatically to the legacy subprocess + upstream path on any error. The
Load Video Path node skips its aggressive unload_all_models() when native is
active, since ComfyUI offloads only what it needs.

Fix the SAM 3.1 video OOM/hang on 12 GB GPUs: running inference without
torch.inference_mode() let autograd retain every 1008x1008 activation
(~10-11.5 GB peak -> OOM). Wrapping the native inference in inference_mode
drops peak to ~3 GB. Public API of mask_video/mask_video_subprocess is
unchanged. Adds tests in test_auto_mask.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w UI

- Add core/image_resize.py and wire optional resize (enable_resize,
  resize_width/height, upscale_method) into Load Image Path.
- input_resolver: when no video/images tensor is connected, use a connected
  image_path as the primary source (so still images flow into ai_upscale /
  rembg / image-edit modes) instead of being treated only as references.
- Front-end/UI: Load Last Image node, image preview (TS + compiled JS),
  agent/load-image node UI, and ffmpega_ui refresh; rename the VideoEditor
  export alias to VideoEditorNode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-cutting wiring for the features added in this branch:

- model_manager: register sam3_1, flux_klein_9b / flux_klein_9b_fp8, and the
  sapiens2_* model entries (sizes, mirror repos, manual-download notes).
- install.py: SAM3 capability check that force-reinstalls sam3 from main when
  an existing install predates the SAM 3.1 multiplex builders; add Sapiens2
  dependencies.
- nollm_modes: new process_sapiens2_only handler; thread sam_version through
  sam3_premask and flux_klein_model through the FLUX Klein path; FlashVSR VAE
  tiling + still-image-source output handling in ai_upscale; fix the
  comparison-mode ffmpeg blend (time-animated reveal that evaluated crop width
  only at t=0).
- agent_node: expose the sapiens2 no-LLM mode, sapiens2_task, flux_klein_model,
  and upscale VAE-tiling widgets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The *_HANDOFF.md docs are working scratch notes (e.g. the SAM 3.1 video OOM
handoff is superseded by the native-path fix) — keep them local, not in the
repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Session transcripts, console captures, debug screenshots and the local
HuggingFace mirror cache are working scratch — they should not ship.
Follows the existing internal-handoff-notes pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Encoding wrote full-range (yuvj420p + color_range=pc) to avoid the
darkening seen on RGB -> YUV conversion. But yuvj420p is deprecated, and
players that normalise it to yuv420p re-expand the levels — crushing
blacks and blowing highlights.

Tag the input as full-range RGB so swscale does a correct full -> limited
conversion explicitly (scale=in_range=pc:out_range=tv), matching ComfyUI's
own SaveVideo. No darkening, no re-expansion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tracks a list of images against the native in-process SAM 3.1 model,
reusing the existing _load_native_sam31 loader. Consumed by the SCAIL-2
pipeline for reference-image and pose-video identity tracking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
register_blockswap() is not a custom offload engine — it leans on ComfyUI's
own lowvram partial loader. Inflating EXTRA_RESERVED_VRAM for the duration
of prepare_sampling shrinks the weight budget that loader computes, so it
leaves that many bytes on the offload device and casts them per-layer on
forward. LoRA still applies at cast time and GGUF weights keep working,
both of which a hand-rolled offload would break.

SVI: new blockswap_blocks (of 40 Wan 2.2 transformer blocks) and tiled_vae
options. The text encoder is now evicted once all prompts are encoded
up-front, so it stops competing with the UNets and VAE during sampling.

FlashVSR: blockswap_blocks drives DiffSynth's num_persistent_param_in_dit
budget (-1 auto-sizes from free VRAM), plus spatial decode tiling to bound
TCDecoder activation memory and whole/temporal/spatial processing modes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rebuilds the reference Wan21_SCAIL2 workflow inside a single synthesizer
that runs entirely in-process under ComfyUI's VRAM management — the same
approach that fixed the SAM 3.1 OOM, and no subprocess.

Loads the SCAIL-2 fp8 diffusion model via comfy.sd.load_diffusion_model
plus the Wan 2.1 VAE, UMT5 text encoder and CLIP-vision (ViT-H), reusing
the SVI/Wan-Animate discovery helpers. Applies model-only LoRAs and a
ModelSamplingSD3 shift, tracks the driving pose video and reference image
with native SAM 3.1, colorizes per-identity masks via SCAIL2ColoredMask,
builds conditioning with WanSCAILToVideo, and samples through
comfy.samplers. Emits both the animation and the colored pose mask.

Removes the vendored core/scail and core/scail_pose packages and
core/scail_synthesizer.py (~4,100 lines) that this supersedes.

model_manager also picks up small mirror/manual touch-ups for flux-klein,
seedvr2 and sapiens2 alongside the new scail2 entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
seedvr2_7b_fp8_mixed keeps the final transformer block (35) in FP16 while
the rest stays FP8. That removes the 7B-specific seam/grid artifacts of
pure FP8 at the same VRAM footprint.

Mirrored to AEmotionStudio/SeedVR2-models.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AEmotionStudio and others added 23 commits July 28, 2026 04:40
The sapiens2_size dropdown folds the fp8 model choice into the size value
via a ' (fp8)' suffix, so precision rides along with the size. Bare sizes
return no override and the caller falls back to 'auto'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FaceCam encodes camera pose as a drawn MediaPipe face mesh. The original
pipeline produced that mesh by rendering a 3D Gaussian proxy head along the
camera path and running a face detector over the renders — throwing away
the camera pose we had already computed analytically, then paying a
detector to guess it back. MediaPipe loses the mesh past roughly 40-50
degrees of yaw, which is exactly the pose a camera orbit exists to
produce, so conditioning went blank at the extremes.

Pose MediaPipe's own canonical face model with the same camera matrices
and project it instead. Coverage is total by construction, motion is
smooth because it is an analytic function of the camera path, and
orbit_left/orbit_right are exact mirrors rather than differing by however
well the detector coped with each direction.

The canonical model is not shipped in the mediapipe wheel, but the
face_landmarker task bundle already on disk is a zip containing the 468
canonical vertices — no vendored asset required.

Adds the mesh_source widget and wires blockswap_blocks through the shared
register_blockswap helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… Image

Both save nodes now take multiple sources through auto-growing images_a /
video_path_a / image_path_a inputs and combine them into a single
side-by-side or grid result.

Video panels are laid out with ffmpeg xstack, and shorter clips freeze on
their last frame until the longest ends. Images are composited tensor-only
in image_compare, so the image path needs no ffmpeg at all. Both share the
layout math in grid_layout so panels land identically either way.

New widgets: layout (auto/horizontal/vertical/grid), label_panels, labels
and panel_gap. Adds the Save Image frontend handler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…PhyFPS modes

Expands no_llm_mode to 31 entries, adding scail2, sapiens2, svi, sharp,
wan_animate, phyfps and video_matting.

Brings 25 scail2_* widgets (geometry, sampling, pose extension, object
selection/sorting, compositing, block swap, tiled VAE and SAM 3.1
detection controls), the sapiens2_size selector with its fp8 option, and
the FlashVSR processing/frame_window/color_fix/decode_tile controls.
upscale_model gains the flashvsr_* entries and seedvr2_7b_fp8_mixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…drop

enable_mask collapses mask_mode, mask_output_type, sam_version and
show_mask_preview behind one switch, so nodes that never mask show four
fewer rows. When off the backend forces mask_mode='none'; an upstream MASK
connection still passes through. onConfigure detects and repairs the
one-slot widgets_values shift this would otherwise cause in workflows
saved before the input existed — a string landing in the BOOLEAN is the
tell, and workflows predating mask_mode fall short of that index and
correctly skip the repair.

skip_first_frames and frame_load_cap now carry a dimmed 'N left' suffix
showing unused source frames. Written to widget.label, which ComfyUI
already draws in the theme's secondary colour and truncates on narrow
nodes — a widget-level draw override would have replaced the built-in
rendering wholesale instead of layering onto it.

Video files can now be dropped anywhere on the node. ComfyUI routes graph
drops through a document listener that dispatches to app.dragOverNode,
which is only ever set by a dragover listener on the canvas; the node's
DOM widgets sit above the canvas, so the whole preview area was dead and
crossing onto it actually cancelled an already-valid drag. New shared
attachFileDropZone helper handles the DOM side with an enter/leave depth
counter, and a dashed overlay covers the preview while dragging.

The upload button is pinned to a fixed row height — arrangeWidgets gives
computeSize widgets a fixed height and splits leftover node height among
the rest, so with no computeSize the button absorbed all of it. Uploads
now go through api.fetchApi rather than a bare fetch, so the route
resolves via api_base + /api and carries the Comfy-User and auth headers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Generated output for the preceding source commits. ffmpega_ui.js is a
single bundle spanning every frontend change, so it cannot be split along
source lines. Chunk hashes rotate with content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CHANGELOG [Unreleased] grows from 3 entries to 30 across Added/Changed/
Removed/Fixed. Several already-committed features had never been logged at
all — MatAnyone2, FlashVSR, SCAIL, Visual Chronometer, Wan-Animate,
Sapiens2, SVI and PhyFPS each had zero mentions in the file.

README gains an 'In Development (Unreleased)' section above the collapsed
v2.19.0 block, ten missing rows in the AI Models table (with their storage
paths, mirror repos, dependency warnings and the non-commercial licences
on MatAnyone2 and SHARP), and the full 31-entry no_llm_mode list, which
was still showing 24.

No version tag cut — everything stays under [Unreleased].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
core/video/encode_opts.py becomes the single source of truth for the colour
policy, the output format table, ffmpeg argument construction and the
tensor->bytes conversion; core/video/metadata.py handles embedding the
workflow into the container.

The metadata module follows ComfyUI's native convention (prompt/workflow
container tags written with -movflags use_metadata_tags) rather than
VideoHelperSuite's comment FFMETADATA pass, because the stock frontend
already reads those tags back -- dropping a saved mp4 onto the canvas
restores the workflow with no client-side code of our own. Matroska and WebM
uppercase their tag keys, so those containers additionally get a comment tag
for readers that only look there.

Both are imported lazily by callers rather than re-exported from
core/video/__init__.py: they pull in torch, which must not become an
import-time cost for core.video.analyzer users.

Nothing routes through them yet; the encoders follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each encoder had drifted to its own colour handling, and several disagreed
with the samples they actually wrote:

- media_converter tagged output yuvj420p full-range, which players normalise
  back to yuv420p and re-expand -- crushing blacks and blowing highlights.
- nollm_modes (_encode_frames_to_video, SHARP renderer) and svi_synthesizer
  passed -color_range pc while swscale wrote limited-range samples, so levels
  that were never compressed got expanded anyway.
- images_to_video (Video Editor, Shader Overlay, FacePoke, Load Last Video)
  wrote untagged BT.601, disagreeing with everything else in the pack.
- video_compare stacked panels with xstack, which does not reconcile inputs
  with different or missing colour tags, so a BT.601 clip beside a BT.709 one
  showed a visible seam. Panels are converted to BT.709 and the result tagged
  to match.

Two conversion bugs fixed with them: images_to_video cast frames with an
unclamped (x * 255).astype(uint8), so Wan's slightly out-of-range VAE output
wrapped instead of clipping and 1.004 came out black; and the cast truncated
rather than rounded, sending 0.999 to 254 and darkening every frame by 1/255.

export.py passes no colour arguments on purpose -- ffmpeg carries the
source's properties through the filter graph, so a tagged input stays tagged
across the multi-pass chain and an untagged one has nothing we could honestly
invent for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A node pair for extending a shot across queue runs. FFMPEGASaveLastFrame
writes the trailing frame(s) of a generation into a named slot under
output/ffmpega_last_frame/<slot>/; FFMPEGALoadLastFrame reads that slot back,
so a later run starts where the previous one ended. Built for i2v chaining
(Wan 2.2 and similar), where continuing a shot means handing the model the
frame you finished on and no authored end-frame exists.

When images is connected the frame comes straight from the IMAGE tensor and
is written to PNG with no h264 round-trip -- extracting from the encoded file
instead bakes in compression artifacts and yuv420p chroma subsampling, which
compound visibly across a chain of generations. video_path remains as an
ffmpeg fallback for any video on disk.

Named slots let several chains coexist without cross-talk and make the
Save/Load pairing an explicit contract rather than "newest file anywhere
wins"; names are sanitized to [A-Za-z0-9_-] before any path join. A slot
holds exactly frame_count files at fixed zero-padded names and stale files
from a longer previous run are pruned, so loading never returns a mix of two
runs -- keep_history switches to an accumulating archive instead.

on_missing (fallback / empty / error) plus a fallback_image input handle the
cold start, so the first run of a chain uses your original start image rather
than feeding a black frame into the sampler.

Both nodes show a thumbnail strip of whatever the slot currently holds, filled
from /ffmpega/last_frame/slot as soon as the node is placed -- without it you
would have to run the graph just to see which frame you are continuing from.
They are absent from NODE_COLORS on purpose: registerNodeStyling
short-circuits the handler chain, and these two need a real handler.

SaveLastFrame exposes a last_frame IMAGE passthrough -- the exact tensor
written -- which is the correct way to chain inside one workflow. Two
unconnected nodes have no guaranteed execution order and caching is decided
before execution begins, so a same-graph Load would read the previous run's
frame; a wildcard trigger input is provided as an explicit ordering hint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
frame_select_mode gains a `last` entry that selects only the final frame, for
the same scene-continuation use case the Last Frame nodes serve.

The back-off from the end is frame-relative (0.5 / fps) rather than the fixed
0.01s that first_last uses, which at 100fps is a full frame period and would
land on the second-to-last frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
attachPlayheadTracker reports playhead movement on a <video>: the position,
whether playback is running, and why it moved. requestVideoFrameCallback
fires once per rendered frame, so a readout built on it matches the picture
rather than trailing it; timeupdate (~4Hz) is the fallback. Seeks, pauses and
metadata loads are reported too, so a readout stays live while parked.

The cause is what makes it more than a convenience: a caller that snaps the
playhead around can act only on "frame" and leave seeks alone -- undoing a
seek fights whoever asked for it.

frameAtTime and rangeEndSeconds carry the maths that select_every_nth makes
subtle, where the count of selected frames and the span of source time they
cover come apart. Both are pure, and ui_helpers.test.ts covers them -- the
pack's first frontend unit test, on the vitest/happy-dom setup that was
already configured with no test files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eview

FFMPEGASaveVideo gains a full encoding surface, so webm/ProRes/lossless
output no longer means falling back to VideoHelperSuite. Every new widget is
optional, so workflows saved before them keep working unchanged.

All of it sits behind one show_advanced toggle, off by default, leaving the
node's resting size unchanged. Visibility is two-level -- the toggle reveals
the section, and within it any widget the chosen format cannot use collapses
on its own (VP9/AV1 take a deadline rather than an x264 preset, ProRes and
FFV1 have fixed quality, GIF/WebP carry no audio). Hiding is display-only:
values are kept and still honoured, so collapsing never silently changes an
encode.

The default stays a plain shutil.copy2 with no ffmpeg call at all. When only
the container differs -- or the source already holds the codec the chosen
format produces -- the file is stream-copied, which is lossless and near
instant; a full re-encode happens only when the codec has to change, and an
ffmpeg failure falls back to a plain copy rather than losing the render.
bit_depth=10 streams 16-bit rgb48le and encodes yuv420p10le, moving the 8-bit
quantisation into swscale and greatly reducing banding on gradients.

frame_output (preview (64) / all / none) makes the previously silent 64-frame
downsample of the images output explicit and skippable. A connected AUDIO
input is now muxed into an image encode, which used to drop it silently.

The preview's info bar gains the frame count: 1920x1080 | 120f @ 24fps |
5.0s | 12.3 MB, with a live playhead while playing. Both numbers are probed
from the file that was written -- reusing probe_video_stats() -- rather than
derived from the fps widget or images.shape[0], so pingpong, loop_count and
pass-through sources report the truth. A <video> exposes neither, so they
ride along in the UI payload next to file_size; where ffprobe is unavailable
the segment is omitted and the bar looks as it did before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…counter

Dragging the preview scrubber past the trim range's out-point jumped the
playhead to the in-point, paused or not. With frame_load_cap set (81 frames
of a 30s clip ends the range around 3s) nearly the whole scrubber was
unreachable, and the "Jump: Middle / End" menu items were undone the moment
they landed.

The range clamp lived in a timeupdate listener, which also fires while
seeking and never checked whether the video was playing. It now runs off
attachPlayheadTracker and loops back only when playback runs off the
out-point on its own: seeks are never reversed, and a previous-tick test
means playing on from a spot the user deliberately parked past the out-point
is left alone too.

Two more defects in the same code:

- The out-point ignored select_every_nth. It used availFrames / effFps, but
  60 frames taken every 2nd are spread across 120 source frames, so playback
  was cut off halfway. Now via rangeEndSeconds().
- Any trim-widget edit re-seeked to the in-point, and the widget-poll cache
  started empty so a spurious "everything changed" pass seeked to 0 about
  800ms after the node appeared. The seek now fires only when the in-point
  actually moves, and the cache is seeded from the widgets.

The live frame counter never appeared: it was gated on _srcMeta.fps > 0, set
only if the /ffmpega/video_info probe returned an fps, while the info bar
beside it fell back to _srcMeta.fps || 24. Whenever that probe failed or
lagged the bar rendered 24fps happily and the counter stayed hidden forever.
Both share the one fallback now.

The readout also gained its f unit and stopped duplicating the frame total
already in the bar: given a playhead that segment becomes the playhead and
moves to the front, where a number changing several times a second is easiest
to read and cannot be mistaken for the static total it replaced. It tracks
paused scrubbing too, since the frame you are parked on is the number
skip_first_frames needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the Save/Load Last Frame nodes, Load Last Video's last mode, Save
Video's advanced output options and frame count, the shared encoding module,
the colour-policy fixes across every encoder, and the Load Video Path
scrubber and frame-counter fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regenerated from src/ by vite. Picks up the Last Frame node UI, the Save
Video preview's frame count, the Load Video Path scrubber and frame-counter
fixes, and the shared playhead tracker they route through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds seedvr2_3b_int8 / seedvr2_7b_int8 upscale models. The loader detects
ComfyUI's int8_tensorwise + block-Hadamard layout from the per-layer
comfy_quant tags, swaps the matching nn.Linear layers for Int8ConvRotLinear
before the state dict is applied, and dispatches to comfy_kitchen.int8_linear
so the weight is never dequantized. Remaining FP16 tensors are cast to BF16,
and an architecture check refuses a checkpoint for the wrong variant instead
of loading noise under strict=False.

Also: models/diffusion_models/ is searched alongside models/SEEDVR2/, the
download gate now only fires for files that are actually missing, the
INT8 entries are added to every model list (node, skill, nollm) with a test
that keeps them in sync, and agent intermediates inherit the source fps
instead of a hardcoded 24.

_comfy_stubs.py gives both conftests a shared folder_paths model-folder stub
so the discovery code exercises its normal branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LoadVideoPathNode borrowed SaveVideoNode._extract_frames without a mode and
inherited its "preview (64)" default, so any clip over 64 frames was evenly
resampled and played back near double speed downstream. Decoding now goes
through core.media_converter.decode_video_frames() for both nodes; the
loader reports frame_count from the decoded tensor, and the save node's
_extract_frames requires an explicit mode. Save Video's images output
defaults to "all", and fps is a FLOAT so NTSC rates are expressible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Snaps a requested size and frame count to what each video model can
actually take (Wan /16 grid + 4n+1, H3 /32 + 17k+5, ...), registered as
FFMPEGAVideoResolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_panel_count defaulted to 2, so a run with no depth/normals pass was
SBS-unpacked to iw/2. It now defaults to 1 and unpack_sbs is a no-op for a
single panel.

Also fix the LoadLast decoder on ffmpeg 8+: -vsync was removed upstream, so
every dense decode failed and callers that swallow the error returned a
512x512 zero tensor. Use -fps_mode and retry with -vsync only when ffmpeg
reports it as unknown, matching core/last_frame.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the onNodeOutputsUpdated extension hook to rebuild the player
from app.nodeOutputs on tab switch / queue-history recovery, and persists a
descriptor in node properties so reloads and server restarts restore it
too. Adds a comfyui/* test alias with a minimal stub so the pure helpers
are unit-testable under vitest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ew persistence

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant