| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix(images): make single-image and intermediate deletion transactional (#9361) * fix(images): make single-image and intermediate deletion transactional Addresses two review findings from JPPhoto: 1. Single-image deletion was nontransactional and reported failure as success. ImageService.delete() now stages the image and thumbnail via stage_delete(), deletes the database record, then commits the stage and fires on-deleted callbacks. A database failure rolls the staged files back to their original paths and re-raises; a failed rollback is logged without masking the database error; a failed final purge is logged but does not fail the deletion (startup recovery cleans the staging directory). The delete_image route no longer swallows exceptions into an empty 200 payload: a missing image returns 404 and a service failure returns 500, mirroring the reviewed video route. 2. Intermediate cleanup deleted records before files, so a filesystem failure orphaned files and aborted cleanup. delete_intermediates() is now all-or-nothing: every intermediate file is staged first (any staging failure rolls back all prior stages and aborts before any record is touched), records are then deleted in a single delete_many call, and stages are committed afterwards with per-item error isolation. Callbacks fire only for committed deletions and no .delete_* staging directories remain after success. The destructive ImageRecordStorage.delete_intermediates() DB method is replaced by a read-only get_intermediates() so listing and record deletion are separate steps. Test coverage: - Service: positive single-delete (files, thumbnail, record, callback exactly once, no staging dirs); staging failure; database failure with on-disk restore of image and thumbnail; rollback failure preserving the database error; purge failure logged without failing. - Service: positive multi-intermediate cleanup; first and later staging failures (mock orchestration plus on-disk restore proof); database failure restoring all staged files; one rollback failure not abandoning remaining rollbacks; commit failure logged with remaining commits attempted and callbacks fired for committed deletions. - Route: successful delete through a real ImageService with real disk storage and SQLite records; missing image returns 404; database failure returns 500 with image and thumbnail restored and the record intact. - DB: get_intermediates() returns pairs without deleting; deletion via delete_many() verified separately. The public-board delete authorization test now wires urls/image_files services and asserts the deleted payload, since the route no longer masks service failures behind an empty success response. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(images): address review of intermediate cleanup and delete route JPPhoto's review raised two merge blockers. Intermediate cleanup snapshotted the intermediates, then deleted those names unconditionally after the database window. An image promoted out of intermediate status in between lost both its record and its staged files. Deletion now runs through `delete_intermediates_by_names()`, which carries the `is_intermediate` predicate on the DELETE itself rather than on a preceding SELECT — Python's legacy sqlite3 transaction control opens a transaction only before a write, so a SELECT there holds no read lock to rely on. The method reports `(deleted, retained)` so the service can tell a promoted record from one that is simply gone: only a record still present earns a file restore. Restoring files for a record deleted elsewhere would strand them with no row and no staging dir for startup recovery, so the rollback path re-checks existence and errs towards keeping the files when the database can't answer. The name lists are chunked to stay under SQLITE_MAX_VARIABLE_NUMBER, which the previous `delete_many(all_intermediates)` call could exceed on a large library. The delete route turned every `get_dto()` failure into a 404, so a database fault on a live image told the frontend to drop it. It now returns 404 only for `ImageRecordNotFoundException` and 500 otherwise. That split could not work on its own: the record store converted every `sqlite3.Error` from `get()` and `get_metadata()` into `ImageRecordNotFoundException`, so a fault on the primary lookup still read as "missing". Those two methods now raise not-found only when the row is genuinely absent. This also stops `__recover_staged_deletes` from purging a live image's staged files on a transient database fault. Tests cover the promotion race at both the store and the service level (including a promotion interleaved inside the call, and a record deleted between the database window and the rollback), chunk boundaries, and that a database fault reaches the route as 500 rather than 404. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(images): delete intermediate records before files to close the promoted-image orphan race Addresses JPPhoto's round-2 merge blocker on PR #9361. The prior revision staged every intermediate file, conditionally deleted the records, then restored the files of any image promoted out of intermediate status mid-operation. That restore is unfixably racy: while a promoted image's files sit in our staging directory, a concurrent single-image or board delete can stage-empty (find no files to move) and then remove the record; our restore then puts the files back with no record referencing them and no staging dir for startup recovery — a permanent orphan. Holding the record-store write transaction across the restore (the suggested fix) narrows but does not close the window, because the competing delete's file-staging happens under no lock and can precede the restore. delete_intermediates() now deletes records first and files second. The conditional DELETE is atomic and returns exactly the names it removed; we then purge only those files, best-effort (a filesystem failure orphans one file but never aborts the remaining purges or undoes the committed deletions). A promoted image is never deleted and its files are never staged, so there is no restore step for a concurrent delete to race, and a concurrent delete of that image operates on real files in the output folder and stays consistent. delete_intermediates_by_names() now returns just the deleted names instead of (deleted, retained); the retained set is no longer needed. Tests rewritten to the records-first contract, including a regression test that concurrently deletes a promoted image right after the conditional DELETE keeps it and asserts its files are not resurrected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(images): journal deletions and never restore files whose record is gone Round-3 review (JPPhoto) found two ways the delete paths could still strand files on disk with nothing referencing them. B1 — delete_intermediates() commits the record deletions before purging the files, so a crash or a filesystem failure in that window left orphans with no trace, and the call still reported success. Deletion now writes a durable journal first: DiskImageFileStorage.begin_delete() fsyncs a manifest naming every image about to be purged, commit_delete() purges and drops it, and abandon_delete() discards it when the record deletion failed. Startup recovery reconciles any journal that outlives its operation by asking the record store: an image whose record survives keeps its files, an image whose record is gone has its files purged. A purge that fails keeps its journal and is retried at the next startup instead of being logged and forgotten. B2 — single-image delete staged the files before deleting the record, so two concurrent deletes of the same image could interleave such that the one that failed restored files the other had already unreferenced. delete() is now records-first over the same journal and moves nothing, so there is no restore to race. The one remaining staging user (delete_images_on_board, which keeps its documented per-item failure contract) is covered by rollback_delete(): it re-checks the record after restoring and purges instead of orphaning. That check is race-free because every deleter purges an image's files strictly after its record is committed as gone. Recovery now probes with image_records.exists() rather than a deserializing get(), and the manifest carries a list so one journal covers a whole intermediates sweep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XwVyhSN5AUBXGACM8yaLKD * fix(images): close the orphan paths an adversarial review found in the journal Three of them break the invariant the rollback re-check rests on — that every deleter purges an image's files strictly after its record is committed as gone. 1. commit_delete() on a staged token only removed the staging directory. stage_delete() captures whatever is on disk at that instant, so a second board delete racing the first gets an EMPTY token; if that second one is the one whose delete_many() succeeds, it removes the record and purges nothing, while the first one's rollback — re-checking a record that is still present at that moment — puts the files back. Permanent orphan, no journal. commit_delete() now purges the live paths too: committing means no file for that image survives, whichever request moved them. 2. create()'s cleanup after a failed save purged the files before deleting the record, so a board delete rolling back in that window was told to restore an image that was about to lose its record. It is now records-first over a journal like every other path. 3. begin_delete() fsynced the manifest and the journal directory but not the journal directory's own entry in the output folder, while SQLite does fsync the record deletion — so a power loss could drop the journal and keep the deletion. Both directories are now fsynced, and stage_delete() does the same before it moves any file (previously it fsynced neither, so a lost manifest stranded staged files in a directory naming nothing). Recovery also no longer aborts startup on a stray .delete_* entry that is not a directory, and says so when a journal has no manifest instead of silently walking past files it cannot attribute. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XwVyhSN5AUBXGACM8yaLKD * fix(images): re-check the record after a recovery restore; 404 a delete that lost the race Startup recovery restored a staged image's files after a single `exists()` check and then dropped the journal. Another Invoke sharing the output folder could delete the record while the files sat staged — its purge finds nothing — and the restore then stranded the files with no record and no journal to find them by. Recovery now re-checks the record after a restore, exactly as rollback_delete() already does, and purges when it is gone. A record-store fault on the re-check keeps the journal, like every other lookup in the recovery loop. The delete route answered 500 when another request deleted the image between its DTO lookup and the service call. The image is gone, which is what the client asked for; it now answers 404 the way the lookup would have. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfBJJqxqmM3b6fKE1FiYGt * fix(images): serialize image mutations against subfolder moves A delete unit reads an image's subfolder, deletes its record, then purges its files at that subfolder, and a move unit does the opposite: relocates files and repoints the record. Interleaved, the delete purges the path its snapshot named while the files sit at the new one — permanent orphans, unrecoverable because the record is gone and a clean purge drops the journal (JPPhoto, PR #9361). ImageMoveService gains a shared image-mutation lock, held across each full plan-relocate-repoint batch cycle in move_all_images() and each per-job unit in startup_recovery(). ImageService holds the same lock across the bodies of delete(), delete_images_on_board(), delete_intermediates(), __clean_up_failed_save() and create()'s record-save-through-file-save span. create() needs it too: the record is visible to the move planner the moment it commits, and under the date strategy its subfolder can disagree with the move target by a day (local clock vs UTC created_at), so a relocation landing mid-save would name a subfolder the files were never written to. The failed-save cleanup additionally journals both the subfolder the save captured and the one the record names now, since partial files can be left at either. The request-entry maintenance guard stays: the lock is what makes the guard's check-then-act safe once a request has passed it. Tests drive both real services against one db and disk store from two threads; each fails against the unfixed source. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(images): propagate directory fsync failures and sync purges before dropping the journal Two gaps in the delete journal's crash-consistency story: - `__fsync_directory` swallowed every fsync error, so a journal whose durability was unknown still licensed the record deletion. It now raises on real errors (EIO, ENOSPC, ...) and tolerates only the errnos that say the filesystem cannot sync a directory at all (EINVAL/ENOTSUP/ENOSYS/EBADF, the PostgreSQL rule). begin_delete()/stage_delete() fail closed: no journal, no files moved, image intact. - Every commit path unlinked the files and dropped the journal without making the unlinks durable. A filesystem that persists the journal's removal ahead of unlinks in other directories could bring the files back after a power loss with no journal left to find them. Commit (both token shapes), rollback and startup recovery now fsync the parent directories of every touched file before removing the journal; a failed sync keeps the journal for the next startup. Tests inject directory-fsync failures (EIO vs tolerated errnos), assert the sync-before-rmtree ordering on each path, and drive the failed-commit and failed-recovery journals through a restart. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PS1h4vJZLhfxMfPHNQRJTa * fix(images): keep the record when a failed save's cleanup cannot be journaled __clean_up_failed_save() deleted the record even when begin_delete() had raised, then fell back to a journal-less stage+commit that fails at the same directory-fsync step. Any file that survived the save became an orphan with no journal to find it. Like every other delete path, the cleanup now removes the record only once a durable journal exists. When journaling fails it logs and leaves the half-created image whole: the record is what lets a later, ordinary delete find and clear it. The dead journal-less fallback is gone with it. Tests: a mock-level test that the record is never deleted after a failed begin_delete(), and a real-storage test that leaves a file behind, makes directory fsync raise EIO during the save's cleanup, checks record + file survive with no journal, and then clears the image with a normal delete once the disk is healthy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PS1h4vJZLhfxMfPHNQRJTa --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com> | 1 天前 | |
fix(model cache): evict records at shutdown() instead of only releasing shared weights (#9494) * fix(model cache): release shared weights when a cache goes away Nothing released a cache's SharedCpuWeightsStore references except _delete_cache_entry(): shutdown() left every resident record's refcount held, and a cache dropped without shutdown() (test teardown; any future wiring that rebuilds caches at runtime) stranded the canonical tensors and their accounting forever. Today's production wiring tears the store down together with its caches, so the live exposure is cross-test pollution of the process-global store and RAM pinned past ModelManagerService.stop() — but the refcount invariant ('every acquire is paired with exactly one release') was simply not upheld, and this makes it self-healing before any wiring change turns it into a real peer-accounting bug. Two mechanisms, for the two ways a cache goes away: - shutdown() now releases its resident records' shared references synchronously — it runs in a normal thread context, so the direct (locking) release is safe there, and teardown does not depend on a later store operation happening. - Each wrapper registers a weakref.finalize fallback for the dropped-without-shutdown case. The finalizer runs in GC context, where taking the store's non-reentrant lock could self-deadlock (a collection can fire inside acquire()'s critical section on the same thread — the rule ModelCache.release_first_use_grace documents), so it only ENQUEUES into a SimpleQueue; every public store method drains the queue under the lock. The finalizer is registered inside the acquire's try (a registration failure must release too), its args carry the key and canonical dict rather than the wrapper (finalize holds args strongly — referencing self would make the wrapper immortal), and release_shared_weights() detaches it before releasing synchronously so eviction-then-collection releases exactly once. The state-dict identity keeps releases correct across invalidate()'s retired entries. RamBudget.total_in_use() now documents why its store read must stay outside the budget lock: the drain allocates under the store lock, so GC can run _on_cache_collected (store→budget) there, and a budget→store order anywhere would complete the deadlock cycle. Six regression tests, verified to fail before the fix, covering: shutdown releases synchronously with an empty queue; collection returns refcount/bytes/budget to zero; the collection-time release is enqueue-only (never applied inline by GC); eviction + collection release exactly once across two caches; a retired (invalidated) entry is freed by a collected holder; and the partial-load wrapper behaves like the full-load one. One existing test relied on an abandoned wrapper leaking its reference and now binds it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(model cache): evict records at shutdown() instead of only releasing shared weights shutdown() released the resident records' shared-store references while retaining the records themselves, so the accounting stopped describing reality: - The store (and RamBudget) reported zero for bytes whose tensors the retained wrappers still held. - A post-shutdown load of the same key on a peer cache registered a duplicate canonical alongside the still-resident released copy. - A post-shutdown eviction of a released record (put() after shutdown() is reachable: Invoker.stop() stops the model manager before the session processor) read uses_shared_weights as already-False and debited the non-shared budget for bytes that were admitted as shared. shutdown() now routes idle records through _delete_cache_entry(), which releases shared ownership and budget accounting together, exactly once. Records still in use — locked by an in-flight generation or inside the put()->lock() admission window — keep their references and are marked stale; unlock() evicts them through the existing stale path when the generation lets go, so the accounting stays truthful at every point. All five regression tests verified to fail against the previous shutdown() behavior. Follow-on to #9403, addressing JPPhoto's review comment there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(model cache): match records by identity in stale eviction and _delete_cache_entry Surfaced by adversarial review of the shutdown() change: a stale-marked record can be detached while still locked (the VRAM-move error paths call _delete_cache_entry on a locked record) and its key re-admitted before the record's last unlock(). The stale-eviction path matched by key only, so it popped the NEW record — detaching it from the cache and all accounting — and, the old record's shared release having already happened, read uses_shared_weights as False and debited the non-shared budget for bytes that were admitted as shared. The hazard predates the shutdown() change (drop_model() sets the same flag), but shutdown() now arms stale marks at every server stop that overlaps in-flight work, so close it here: _delete_cache_entry() and unlock()'s stale eviction act only when the record passed in IS the record currently held under its key; a delete of a detached record is a full no-op. Regression test verified to fail against the key-only matching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(model cache): track get()->lock() holders through shutdown and abandonment Two defects found in review of the shutdown eviction change (JPPhoto, 2026-08-13): 1. shutdown() racing the gap between get() and the LoadedModel's first lock evicted the warm record out from under its holder: the holder locked a detached record whose shared-store ownership had just been released, so a peer's reload of the same key minted a duplicate canonical copy while the budget counted one. 2. A record retained by the shutdown sweep for a never-locked holder could never be evicted if that holder was simply dropped: the abandonment finalizer's deferred work was discarded post-shutdown (and the worker was stopped), pinning the record, its shared-store refcount and its budget bytes for the life of the process. The fix tracks every wrapper's get()->lock() window with a per-record hold count (CacheRecord.first_use_holds), armed in LoadedModelWithoutConfig's constructor and released exactly once per wrapper — on its first lock, or by its weakref finalizer if it is dropped un-entered. Held records are treated like locked ones by every eviction path (shutdown, budget reconcile, peer-requested eviction, make_room, drop_model, unlock's stale eviction); stale-marked records whose last holder is abandoned are evicted by the deferred worker, which now outlives shutdown() for exactly that purpose (it already exits via the cache-collection finalizer). Holds are only granted while a worker is alive to carry the finalizer's release, and a worker death zeroes surviving holds at the next start so no record can stay shielded with nothing left to unshield it. Admissions landing after shutdown() are marked stale at birth so their final release evicts them too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(model cache): epoch-guard hold releases and recover stranded holds at shutdown Hardening from adversarial review of the first-use-hold mechanism: - Hold releases (the wrapper's first-lock release and the abandonment finalizer's deferred release) now quote the epoch the hold was armed under, and dead-worker recovery bumps the record's epoch when it zeroes stranded holds. Without this, a surviving wrapper's late release — or a release enqueued before the worker died and drained after the restart — would decrement a fresh hold armed by a different wrapper under the healthy replacement worker, silently unshielding that wrapper's window. - shutdown() now runs the dead-worker hold recovery itself (and clears the put()-grace flags in the same situation): a hold whose abandonment release was dropped by the dead-thread dispatch check has no other releaser, and after shutdown no put() is guaranteed to run the usual next-start recovery — the sweep would stale-retain the record, its shared-store refcount and its budget bytes for the life of the process. - register_first_use_hold() declines to arm on a record that is no longer the occupant under its key: an eviction already won the race against the wrapper's construction and a hold on a detached record shields nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(model cache): withhold the post-shutdown grace and recover from the worker's own death Two follow-ups from review. put() after shutdown() no longer arms the post-admission grace. That flag's backstop releaser is the sweep at the top of the next put(), and after shutdown no further put() is guaranteed: a load cancelled between put() and the LoadedModel's construction leaves no wrapper (hence no finalizer either), so an armed flag would stand for the life of the process, hiding the record from every asynchronous eviction path while its bytes stayed charged to the shared budget. Withholding it costs only the shield -- the record stays stale at birth, so its eventual release still evicts it, and a loader that does come back gets the ordinary first_use_holds shield. The deferred worker now runs stranded-shield recovery from inside its own dying frame. Previously recovery depended on something else happening first -- the next admission, or shutdown() -- and neither is guaranteed when the worker dies *after* shutdown()'s liveness check: the records the shutdown sweep retained for a live holder were left shielded by holds nothing could release. The recovery is scoped by thread identity (a replacement worker's shields are its own) and retires the worker slot before sweeping, so a concurrent admission cannot arm a shield the recovery is about to zero. It also drains the queue the dead worker left behind, whose _AbandonedHolderRelease items pin their models' CPU weights. _ensure_deferred_worker and shutdown() now share the same recovery, which also lifts orphaned admission graces and evicts whatever that leaves unshielded. Three tests, each reverted-and-confirmed-failing against the code it guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjAw9hpJe8d1GyvpdKJFGw * fix(model cache): narrow the dead-worker recovery and stop it pinning records Adversarial review of the previous commit found three problems with the shared recovery it introduced. The recovery cleared the put()-set admission grace unconditionally. On a live cache that is a new failure mode, not a fix: the dying worker is is_alive() for as long as it unwinds, so a cold load landing in that window starts no replacement worker and is admitted with the ordinary grace, which the recovery then zeroed while the loader was still between put() and get() -- a reconcile could evict the record and the loader's get() would raise IndexError. The grace only actually loses a releaser once the cache is shut down (its backstop is the next put()'s sweep, not the worker), so it is now lifted only then. The recovery also evicted stale-unshielded records from _ensure_deferred_worker, which register_first_use_hold calls before arming -- so a second wrapper's construction could detach the very record it was about to shield, releasing shared-store ownership while live wrappers still held the tensors. That is the accounting lie shutdown() itself refuses to make. The eviction moved to _evict_stale_unshielded_entries, called only from the dying worker and only on a shut-down cache, where nothing else can ever run it; it now also collects and empties the device cache the way the other abandonment path does. Keeping shutdown()'s call to pure field assignments restores its old property that the branch cannot raise before the resident-record sweep. The queue drain the previous commit added did not close the pin it targeted: _dispatch_deferred's liveness gate is unsynchronized, so a finalizer that read the worker slot just before it was retired still enqueues after the drain. The drain is gone; _AbandonedHolderRelease now holds its record weakly instead, so a stranded item pins nothing, and the worker clears the strong reference it resolves before parking on the next get(). Also moves _reconcile_budget_if_pending's lock acquisition adjacent to its try: a BaseException between the two leaked the cache RLock to an unwinding thread, blocking every other thread for the life of the process. Five tests, each reverted-and-confirmed-failing against the code it guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjAw9hpJe8d1GyvpdKJFGw * fix(model cache): key the dead-worker backstops on worker liveness, not the slot A second adversarial pass found that retiring the worker slot from inside the dying worker silently disabled both remaining recovery sites, which gated on "a dead thread still occupying the slot". The dying recovery deliberately leaves a live cache's admission grace standing -- the next put()'s sweep is still its backstop -- and hands the lift to shutdown(); with the slot already empty, shutdown() skipped it and stale-retained the record, its shared-store reference and its budget bytes for the life of the process. Both gates now key on "no live worker": shutdown() lifts when the slot is empty or dead, and the worker start recovers unconditionally (it has already returned if a worker is alive). That also makes a failed recovery retryable, which matters because the recovery was not exception-safe and had already retired the slot by the time it could raise. _clear_stranded_first_use_holds now unshields every record before reporting any of them -- a logging handler that raises is one of the ways the worker dies in the first place, and logging inline let that same handler abort the sweep partway -- and the post-eviction gc/empty_cache housekeeping, which the codebase already documents can raise from a sick CUDA context, no longer takes the eviction down with it. Also corrects two overstated claims in the weakref rationale: a stranded queue item can be drained later by a replacement worker (the queue is per-cache, not per-worker), and the hold decrement in _release_abandoned_holder runs before the identity check -- it is inert on a detached record for a different reason, which the docstring now gives. Moving _reconcile_budget_if_pending's acquire adjacent to its try narrows the RLock-leak window rather than closing it; said so. Three tests, each reverted-and-confirmed-failing against the code it guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjAw9hpJe8d1GyvpdKJFGw * fix(model cache): claim the first-use window at the lookup, and let a cancelled admission release itself Two findings from review. The first-use shield was armed by LoadedModel's constructor, leaving the whole stretch between the cache lookup and that constructor unshielded -- and that stretch is not a couple of instructions: the configured loader retrieves its record inside _load_and_cache and then does the shared-store shell registration and two returns before load_model wraps it. A shutdown sweep or a peer's reconcile landing there detached the record its holder was about to lock, releasing shared-store ownership while the tensors lived on, so a peer's reload minted a duplicate canonical the budget counted once. ModelCache.get_with_first_use_claim() now arms the hold in the same lock acquisition as the lookup and hands back a FirstUseClaim that owns it: the wrapper adopts the claim and releases it at its first lock, and a claim dropped without ever being adopted -- the load raised before a wrapper existed -- releases the hold by dying. shutdown() stale-retained a record carrying only the put()-set admission grace. That grace's three releasers are the loader's own get()->lock(), the abandonment finalizer of a wrapper built from the record, and the sweep at the top of the next put(); a load cancelled between its put() and its retrieval has neither of the first two, and after shutdown no further put() is guaranteed to run the third, so the record, its shared-store reference and its budget charge stood until the cache object was collected. put(claim_admission=True) now hands the loader a claim over that window too, so such a load releases its admission by dying and the shutdown sweep finds an ordinary idle record. Retiring the grace at shutdown instead -- the obvious shortcut, and what the first two drafts of this commit did -- is not safe. The flag is unowned, so a standing grace does not mean nobody is working on the record: it is equally the state of a load still between its put() and its retrieval, and of a live un-entered wrapper whose hold a worker death zeroed. Both were evicted out from under their holder, with the duplicate-canonical accounting lie and an IndexError from a retrieval that no longer found its own model. For the same reason the admission window is not shielded by either flag but by a weak reference to the claim (CacheRecord.admission_claim_ref): nothing has to release it, so neither a worker death (which zeroes holds) nor another holder's abandonment (which clears the grace) can make a running load look finished. _recover_stranded_shields retires it once the cache is shut down, where the eviction its expiry should trigger would otherwise travel through a dead worker -- the same trade that method already makes for holds. The claim is armed only after put() has committed its accounting, so a failure to allocate it cannot leave a resident, store-owning record the budget never counted, and both hand-back guards release the hold when the object that was to carry its release cannot be built. Nine tests, each reverted-and-confirmed-failing against the code it guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FCitU6EFcp76AaauNfaWTz * fix(model cache): refuse post-shutdown prefetch admissions, and keep abandonment releases Two records could survive a shut-down cache with nothing left to retire them. A prefetch=True admission after shutdown() was marked stale and inserted anyway. prefetch is the promise that no loader will come back for the record, so there is no get() -> lock() -> unlock() to run the stale eviction, no wrapper whose finalizer could carry an abandonment release, and no claim whose expiry could stand in for either. What is left are the paths that may or may not run -- another admission's make_room, a budget reconcile, a peer's eviction request -- and after shutdown none of them is guaranteed to come. put() now refuses such an admission, the same standard the post-admission grace is already withheld under. It costs only a reload: the sole caller takes the submodel it asked for from the pipeline object, not from the cache. The refusal sits after _ensure_deferred_worker() (a post-shutdown prefetch must still revive the worker that carries the retained records' abandonment releases), after the stale-grace sweep (on a shut-down cache with a LIVE worker that sweep is the only backstop a stale grace has left, since shutdown() runs the recovery only when no worker is alive), and before _make_room_internal (nothing resident should be evicted to house a model this call is about to refuse). _dispatch_deferred dropped every item while no worker was running, which included _AbandonedHolderRelease. Its holder is already gone -- finalizers fire once -- so no lock, no unlock and no second finalizer is coming, and that item is the only thing left that can retire the record. Dropping it stranded a record the shutdown sweep had retained, resident and charged, for the life of the process; no later sweep can repair that, because once dead-worker recovery zeroes the hold, a record whose holder is gone is indistinguishable from one a live wrapper is still holding, where retention is required. Abandonment releases are now kept and drained by whichever worker runs next; reconciles are still dropped, since cached_model_keys() can request one on every call and the next cache operation's release hook re-runs it anyway. Evicting from _ensure_deferred_worker() is deliberately NOT the fix: it runs _recover_stranded_shields() immediately before, so at that instant a record a live wrapper still holds looks unshielded, and evicting it would release shared-store ownership while the tensors live on. Four tests, each checked for sensitivity by reverting the line it guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015n81CKSi7bUA217L2E9SwN * fix(model cache): bound the abandonment queue, own worker-less admissions, keep live claims through recovery JPPhoto's round-6 findings on the shutdown accounting, all three confirmed: - A grace-only abandonment enqueued one kept item per dropped wrapper while no worker could be started, and nothing short of a lock() or another put() ever cleared the grace that kept them coming, so a warm get/drop loop grew the queue without bound. release_first_use_grace now clears the grace itself, lock-free (the flag is monotonic), queues an eviction only for a stale record and only once per record (CacheRecord.abandonment_release_pending, re-opened by the worker the moment it dequeues the item), and still wakes the worker for a pending budget reconcile the drained item used to run. - A post-shutdown put(claim_admission=True) with no startable worker got no claim, so nothing owned the record once its loader died and no later admission swept it. _claim_first_use now mints a hold-less FirstUseClaim (hold_epoch=None) whenever the record is still the occupant, so the admission stays owned through CacheRecord.admission_claim_ref and its finalizer still queues the eviction a stale record owes; and put() runs _evict_stale_unshielded_entries() when the cache is shut down and no worker is alive after its revival attempt, the same terminal sweep the dying worker runs, placed after the grace sweep and before the prefetch refusal. - _recover_stranded_shields retired a live admission claim on a shut-down cache, so a worker death followed by shutdown() evicted a record whose loader was between put() and get(), and that loader's retrieval raised IndexError. A live claim now survives every recovery; the eviction owed once it dies travels through the kept queue item or the next admission's sweep. Two further defects surfaced by the adversarial passes over the fix: shutdown() now marks a record stale BEFORE consulting its shield, so a hold-less abandonment racing the sweep either sees the mark and queues the eviction or has already cleared its shield when the sweep looks; and the coalescing gate is opened at the dequeue site rather than in the handler, so a raise in the handler, a worker death inside it, or a fork cannot leave it closed with nothing queued behind it. Ten new tests, two rewritten; each production change reverted individually and confirmed to fail only the tests that guard it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JbML8Y8mHdXH72bcUry9o * fix(model cache): refuse worker-less post-shutdown admissions JPPhoto's round-7 blocker, the residual I disclosed rather than closed last round: a claimed admission into a shut-down cache with no startable worker has no releaser (no worker to drain the eviction its dropped claim's finalizer queues) and no guaranteed future cache operation to stand in for one, so a load that dies before retrieving it pins the record, its shared-store reference and its budget bytes for the life of the cache object. put() now refuses every admission -- claimed, plain, and prefetch -- once the cache is shut down and no worker is alive after its revival attempt, the same standard the post-shutdown prefetch was already refused under, generalized. Nothing that could be stranded is admitted. The refusal is narrowly scoped: a live cache still admits worker-less (a future op cleans up), and a normal shutdown keeps its worker alive so the graceful retain-and-reclaim path is unchanged -- only thread exhaustion reaches the refusal, where a load racing shutdown is failing regardless (its retrieval raises IndexError, as a refused prefetch's does; both loaders already handle a None put()). The terminal sweep (_evict_stale_unshielded_entries) still runs before the refusal returns, so a record already orphaned by a worker death is reclaimed even though this admission -- possibly the last cache operation -- is refused. Three tests whose orphan came from the now-refused admission are removed; their mechanism coverage survives elsewhere. Two added: the refusal with its clean loader-style IndexError, and the terminal-sweep-still-runs case. Both new production lines mutated and confirmed to fail only their guarding tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JbML8Y8mHdXH72bcUry9o * fix(model cache): clean shutdown-retained records --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com> Co-authored-by: JPPhoto <jpollack@jpollackphoto.com> | 1 天前 | |
test: clean up & fix tests - Deduplicate the mock invocation services. This is possible now that the import order issue is resolved. - Merge `DummyEventService` into `TestEventService` and update all tests to use `TestEventService`. | 2 年前 | |
Rename default schedulers across the app | 3 年前 | |
Feat[model support]: Qwen Image — full pipeline with edit, generate LoRA, GGUF, quantization, and UI (#9000) | 4 个月前 | |
prevent vae: '' from crashing model | 3 年前 | |
parent 9eed1919c2071f9199996df747c8638c4a75e8fb author Kyle Schouviller <kyle0654@hotmail.com> 1669872800 -0800 committer Kyle Schouviller <kyle0654@hotmail.com> 1676240900 -0800 Adding base node architecture Fix type annotation errors Runs and generates, but breaks in saving session Fix default model value setting. Fix deprecation warning. Fixed node api Adding markdown docs Simplifying Generate construction in apps [nodes] A few minor changes (#2510) * Pin api-related requirements * Remove confusing extra CORS origins list * Adds response models for HTTP 200 [nodes] Adding graph_execution_state to soon replace session. Adding tests with pytest. Minor typing fixes [nodes] Fix some small output query hookups [node] Fixing some additional typing issues [nodes] Move and expand graph code. Add base item storage and sqlite implementation. Update startup to match new code [nodes] Add callbacks to item storage [nodes] Adding an InvocationContext object to use for invocations to provide easier extensibility [nodes] New execution model that handles iteration [nodes] Fixing the CLI [nodes] Adding a note to the CLI [nodes] Split processing thread into separate service [node] Add error message on node processing failure Removing old files and duplicated packages Adding python-multipart | 3 年前 | |
feat: add System Prompts library for Expand Prompt button (#9152) * feat: add System Prompts library for Expand Prompt button - Add system_prompts SQLite table (migration 32) seeded with 6 curated default prompts adapted from FLUX.2, HunyuanImage 3.0, Qwen-Image, Z-Image and HiDream - Add CRUD service layer + REST router at /api/v1/system_prompts - Add RTK Query endpoints, management modal (list/create/edit/delete) and a system-prompt picker in the Expand Prompt popover - Persist last picked system prompt + text-LLM model via Redux * feat(system-prompts): scope CRUD to owner/admin for multi-user installs - Migration 32 now adds user_id + is_public columns and seeds the 6 default prompts as user_id='system', is_public=TRUE; - Storage layer gains optional user_id scoping on get_many/update/delete, and create requires user_id + is_public - Router uses CurrentUserOrDefault: list scopes to own+public, GET returns 403 for foreign private prompts, PATCH/DELETE require owner or admin - Frontend adds useCanEditSystemPrompt hook, hides edit/delete on prompts the user does not own, shows System/Shared badges in the list, and exposes a 'Share with everyone' toggle in the form when multiuser is on * Add Default System Prompt as DB row * fix(system-prompts): unbreak migration import + cover ownership in tests - Critical: migration_32.py had a 7-space indent on the second cursor.execute(), raising IndentationError on import and blocking server startup. Re-indent and restore the ALTER TABLE backfill block lost in the previous edit. - Medium: drop the heavy import of invokeai.backend.text_llm_pipeline from the migration (which would pull torch+transformers into the migrator import path). Inline DEFAULT_SYSTEM_PROMPT verbatim and rename the seeded row to "Default"; the value still mirrors text_llm_pipeline.DEFAULT_SYSTEM_PROMPT. - Medium: add 7 storage-layer tests covering own/public/admin scoping and the no-mutate guarantees on non-owner update/delete, plus 9 router tests with JWT auth covering 401/403/404 paths, owner is_public flip, and admin override. - Conftest and the existing workflows-multiuser test fixtures now wire a real SqliteSystemPromptRecordsStorage so InvocationServices construction succeeds with the new required parameter. * Chores Ruff + typegen * test(system-prompts): wire system_prompt_records in multiuser_authorization fixture The new required InvocationServices parameter broke 122 unrelated tests in tests/app/routers/test_multiuser_authorization.py because that file builds its own InvocationServices. Add SqliteSystemPromptRecordsStorage to its fixture the same way the workflows-multiuser fixture and the global conftest were updated in the previous commit. * feat(system-prompts): add Text LLM (with System Prompt Preset) workflow node Adds a sibling node to TextLLMInvocation that takes a SystemPromptField (a DB-backed preset reference) instead of a free-text system prompt. Selecting a preset in the workflow editor pulls its content from the System Prompts library at run time. The original TextLLMInvocation is unchanged, so users keep the free-text option and can pick the appropriate node per workflow. - New SystemPromptField primitive in app/invocations/fields.py - Shared _run_text_llm helper extracted from TextLLMInvocation; both nodes use it - Frontend wires SystemPromptField as a new stateful field type analogous to StylePresetField (zod schemas, type guards, builders, slice action, color, Combobox renderer backed by useListSystemPromptsQuery) - Pytest covers both behaviours: DB lookup happens with the configured id and forwards the resolved content; SystemPromptNotFoundError short-circuits the pipeline call so the LLM is not invoked * Chore Ruff + Typegen * chore: regenerate openapi schema for system prompts endpoints * Chore fix Path * test: pass system_prompt_records to InvocationServices in merged-in tests Main's image-move and workflow-call tests construct InvocationServices directly and predate the required system_prompt_records service, so they failed after the merge. Add the argument at the three construction sites. * fix(tests): add missing video/gallery services to system prompts test fixture The mock_services() fixture in test_system_prompts_multiuser.py predates the video generation merge, which added five required InvocationServices args (videos, video_files, video_records, board_video_records, gallery). All nine tests in the file errored at setup, failing every python-tests CI job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(system-prompts): address review feedback on #9152 - TextLLMWithPresetInvocation now enforces the same access rules as the REST API before resolving a preset. The record store is unscoped, so a user could previously read another user's private prompt by enqueueing a graph that references its id -- the content becomes the LLM's system message and is recoverable from the output. Mirrors call_saved_workflow's ownership check. - Bump the migration id (and module name) to 2026_07_10_create_system_prompts. The migrator runs each id once, so the ADD COLUMN backfill for dev databases created from an earlier revision of this branch was unreachable; those DBs hit "no such column: user_id" on every list and create with no self-repair. The earlier id was never released, so a new id is free. Also corrects the module docstring: INSERT OR IGNORE is not what keeps deleted defaults deleted. - delete() raises SystemPromptNotFoundError when nothing was deleted, and the router maps that to 404. Single-user installs skipped the existence check, so DELETE reported 200 for ids GET 404s on and deleting a row twice succeeded twice -- contradicting the PR's own QA contract. This also removes the update()/delete() asymmetry. - Drop the stale manual type augmentation in endpoints/systemPrompts.ts; schema.ts already carries user_id/is_public. Nits: move systemPrompts after stylePresets in en.json; drop the boolean index idx_system_prompts_is_public; document the SystemPromptField id-portability limitation in the node docstring. Tests: node-level permission tests for the escalation path and the allowed cases; single-user router tests for the delete contract; migration tests for the backfill, idempotency and id/module-name consistency. * feat(system-prompts): add Krea 2 expansion prompt, fix node/backfill visibility Seed the Krea 2 prompt-expansion system message (krea-ai/krea-2, docs/expansion.txt) as an eighth default, verbatim from upstream. Also address review feedback on #9152: Drop the `is_default` clause from TextLLMWithPresetInvocation's ownership check. SYSTEM_PROMPT_DEFAULT_USER_ID ("system") is not only the seeded defaults' owner but also the synthetic user id every request carries in single-user mode, so the clause made every prompt created before an install switched to multiuser readable by anyone via a graph, while GET /system_prompts/i/{id} correctly 403s on it. The seeded defaults are is_public=TRUE, so is_public already covers them and the node's rule is now identical to the router's by construction. Re-share the seeded defaults in the multiuser backfill. ADD COLUMN stamps is_public=FALSE onto pre-existing rows and the seed is INSERT OR IGNORE, so the defaults stayed private and get_many (own OR public) returned an empty list for every non-admin. Scoped to the seeded ids and to the backfill branch so a default a user deliberately made private is never re-shared. Correct the SYSTEM_PROMPT_DEFAULT_USER_ID docstring and the user_id field description, which described the id as meaning "built-in default" - the reading the bypass was built on. Tests: the parametrized node case asserted a private "system"-owned prompt was allowed; corrected and paired with a regression test for the single-user -> multiuser upgrade path. The backfill test now seeds the defaults first and asserts they end up public, plus a test that a re-run leaves privatization alone. --------- Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 1 个月前 | |
tests: add util to run a function in separate process This allows our tests to run in an isolated environment. For tests taht implicitly depend on import behaviour, this can prevent side-effects. The function should only be used for tests. | 1 年前 | |
Added linux to the workflows (#463) * Added linux to the workflows - rename workflow files Signed-off-by: Ben Alkov <ben.alkov@gmail.com> * fixes: run on merge to 'main', 'dev'; - reduce dev merge test cases to 1 (1 takes 11 minutes 😯) - fix model cache name Signed-off-by: Ben Alkov <ben.alkov@gmail.com> * add test prompts to workflows Signed-off-by: Ben Alkov <ben.alkov@gmail.com> Signed-off-by: Ben Alkov <ben.alkov@gmail.com> Co-authored-by: James Reynolds <magnsuviri@me.com> Co-authored-by: Ben Alkov <ben.alkov@gmail.com> Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com> | 3 年前 | |
Add back old `dream.py` as `legacy_api.py` This commit "reverts" the new API changes by extracting the old functionality into new files. The work is based on the commit `803a51d5adca7e6e28491fc414fd3937bee7cb79` PngWriter regained PromptFormatter as old server used that. `server_legacy.py` is the old server that `dream.py` used. Finally `legacy_api.py` is what `dream.py` used to be at the mentioned commit. One manually run test has been added in order to be able to test compatibility with the old API, currently just testing that the API endpoint works the same way + the image hash is the same as it used to be before. | 3 年前 | |
Fix: Shut down the server with one keyboard interrupt (#94) (#8936) * Fix: Kill the server with one keyboard interrupt (#94) * Initial plan * Handle KeyboardInterrupt in run_app to allow single Ctrl+C shutdown Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * Force os._exit(0) on KeyboardInterrupt to avoid hanging on background threads Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> Fix graceful shutdown to wait for download/install worker threads (#102) * Initial plan * Replace os._exit(0) with ApiDependencies.shutdown() on KeyboardInterrupt Instead of immediately force-exiting the process on CTRL+C, call ApiDependencies.shutdown() to gracefully stop the download and install manager services, allowing active work to complete or cancel cleanly before the process exits. Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * Make stop() idempotent in download and model install services When CTRL+C is pressed, uvicorn's graceful shutdown triggers the FastAPI lifespan which calls ApiDependencies.shutdown(), then a KeyboardInterrupt propagates from run_until_complete() hitting the except block which tries to call ApiDependencies.shutdown() a second time. Change both stop() methods to return silently (instead of raising) when the service is not running. This handles: - Double-shutdown: lifespan already stopped the services - Early interrupt: services were never fully started Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> Fix shutdown hang on session processor thread lock (#108) * Initial plan * Fix shutdown hang: wake session processor thread on stop() and mark daemon Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * Fix: shut down asyncio executor on KeyboardInterrupt to prevent post-generation hang (#112) Fix: cancel pending asyncio tasks before loop.close() to suppress destroyed-task warnings Fix: suppress stack trace when dispatching events after event loop is closed on shutdown Fix: cancel in-progress generation on stop() to prevent core dump during mid-flight Ctrl+C Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> | 6 个月前 | |
Add opt-in low-VRAM mode for Wan generation (#9462) * feat(video): optimize Wan memory usage * chore: openapi schema * Added additional optimizations * fix(wan): address video memory optimization review * test(cache): keep VRAM budget test CPU-safe * fix(wan): address memory optimization review * fix(wan): calibrate VAE from checkpoint files * feat(wan): add tiled VAE calibration mode * fix(wan): align VAE calibration with estimator | 22 天前 | |
ci(pins): make the python-classifier check advisory, not fatal (#9466) * ci(pins): make the python-classifier check advisory, not fatal The pin check gated on `project.classifiers`, which is optional in PEP 621 and purely informational on PyPI. That made a legal pin fail: with `requires-python = ">=3.11, <3.13"`, setting pins.json's python to 3.11 exited 1 solely because no `Programming Language :: Python :: 3.11` classifier existed, even though the package metadata permits 3.11. `requires-python` is what actually gates installation, so it goes back to being the only authority the script fails on. A pin the classifiers don't mention is now a warning naming the classifier to add, and their absence, malformation or deferral to `project.dynamic` is not a finding at all. This gives up one thing, and the docstring now says so: a version that satisfies an open-ended `requires-python` but that no interpreter has (">=3.11" with a "3.99" pin) is no longer caught. Catching it meant gating on non-normative metadata, and a checker that rejects a legal pin is worse than one that misses an implausible typo. * ci(pins): tighten the classifier advisory after self-review Found by attacking the previous commit: - '... :: Python :: 3.012' counted as declaring 3.12, because the comparison normalized both sides through _parse_version. That is not a trove classifier, so PyPI shows no 3.12 support - silencing the advisory in exactly the case it exists to name. The classifier pattern now rejects leading zeros like the pin pattern does, which also makes the normalization redundant: with one spelling per version on either side, a string comparison is exact. - The warning loop ran before the errors were printed, so a raise while computing advice would have discarded the error list - the failure mode check_python's "never raises" contract exists to prevent. It now runs after, and a test asserts the advisory still appears on a failing run. - Three comments still described classifiers as authoritative or named check_python as the code that reads them. --------- Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com> | 30 天前 | |
Add opt-in low-VRAM mode for Wan generation (#9462) * feat(video): optimize Wan memory usage * chore: openapi schema * Added additional optimizations * fix(wan): address video memory optimization review * test(cache): keep VRAM budget test CPU-safe * fix(wan): address memory optimization review * fix(wan): calibrate VAE from checkpoint files * feat(wan): add tiled VAE calibration mode * fix(wan): align VAE calibration with estimator | 22 天前 | |
tests: add util to run a function in separate process This allows our tests to run in an isolated environment. For tests taht implicitly depend on import behaviour, this can prevent side-effects. The function should only be used for tests. | 1 年前 | |
Docs Overhaul (#8896) * feat(docs): new docs scaffold * feat(docs): update alternate launchers section * feat(docs): add contributor section * fix(docs): update description of lynxhub launcher mention * feat(docs): add more docs * feat(docs): setup index page * feat(docs): add more docs, rewrote a few pages * feat(docs): add todo * feat(docs): set up internationalization * fix(docs): admonition typo * feat(docs): add invoke styles * feat(docs): add more invoke styling, revamp splash page, remove theme switcher * fix(docs): expressive code sh styles without title * chore(docs): cleanup readme * chore(docs): add new github pages workflow * fix(docs): remove base path * chore(docs): add initial translations CI, powered by Crowdin * feat(docs): upgrade astro * feat(docs): enhance new contributor guide * feat(docs): various enhancements - improve homepage; - enhance some docs pages; - override some layout components; - enhance interactivity and qol styling; - create new download page + component; - add llms.txt; - remove unused logo component; * feat(docs): isolate new docs * style(docs): use md reference links over utility links * chore(docs): specify package manager * feat(docs): releases page * feat(docs): add page context menus * feat(docs): sort workflows sidebar items * fix(docs): relative links on homepage * feat(docs): add text tool and recall params api guides * feat(docs): fix faq links, create models concept page * chore(docs): set CI to new dir, update deployment url * feat(docs): generate settings and api json for pages - update deploy script - add api and settings component to render generated json - increase page content width * style(docs): remove relative path for component import * fix(docs): resolve tests by regenerating json * fix(docs): fixing the test for real this time - sorts openapi output map required field - missing `__name__` attributes - resolved components name keyerror * feat(docs): finish 'adding nodes' page * feat(docs): upgrade astro + starlight, add link tester * chore(docs): upgrade astro * feat(docs): add prompting guides * fix(docs): generated openapi * fix(docs): ci node version * fix(docs): invalid links * fix(docs): md aside formatting * feat(docs): reorder 'configuration' category * feat(docs): change contributor checklist to steps list * chore(docs): upgrade deps * feat(docs): splash page image styling * feat(docs): add gallery marquee to homepage * feat(docs): add splash page marquee gallery * feat(docs): remove openapi generation * fix(docs): regenerate settings json * fix(docs): json generation test --------- Co-authored-by: joshistoast <me@joshcorbett.com> Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com> | 4 个月前 | |
Fix collector scoping and invocation validation (#9483) * Fix collector scoping and invocation validation * chore: ruff * Harden collector scope and optimized validation * Preserve nested collector scopes at arbitrary depth | 24 天前 | |
Declare networkx as a runtime dependency (#9457) * Defer networkx import for schema generation * Declare networkx runtime dependency | 30 天前 | |
feat: Video generation (#9163) * feat(model): add Wan 2.2 image generation support (Phases 0-2) Foundation + TI2V-5B MVP + A14B dual-expert MoE for Wan 2.2 image generation. Wan was trained on video but is competitive with leading open-source image models when run at num_frames=1; this commit wires that path into InvokeAI. Phase 0 — Foundation: - BaseModelType.Wan + WanVariantType {T2V_A14B, TI2V_5B} - SubModelType.Transformer2 for the dual-expert MoE - MainModelDefaultSettings per variant - step_callback Wan branch (16-channel preview; 48-channel TI2V-5B falls back to slicing first 16 channels until proper factors land) - Frontend enums + node colour Phase 1 — TI2V-5B Diffusers MVP: - Main_Diffusers_Wan_Config probe (variant from transformer_2/ + vae/config.json::z_dim, with filename heuristic fallback) - WanDiffusersModel loader (subclasses GenericDiffusersLoader) - WanT5EncoderField, WanTransformerField (with dual-expert slots), WanConditioningField, WanConditioningInfo - New invocations: wan_model_loader, wan_text_encoder, wan_denoise, wan_image_to_latents, wan_latents_to_image - FlowMatchEulerDiscreteScheduler integration with on-disk config load - RectifiedFlowInpaintExtension reused for inpaint - 5D <-> 4D shape juggling: latents stay 4D in InvokeAI's pipeline, re-add T=1 only inside the transformer call / VAE encode-decode Phase 2 — A14B dual-expert MoE: - Probe reads boundary_ratio from model_index.json - Loader emits both transformer (high-noise) and transformer_low_noise (low-noise expert at transformer_2/) for A14B - _ExpertSwapper in wan_denoise drives GPU residency between experts: high-noise for t >= boundary_ratio * num_train_timesteps, low-noise below. Only one expert locked at a time so the cache can evict the other - relies on existing CachedModelWithPartialLoad to handle oversized models on lower-VRAM GPUs. - guidance_scale_low_noise field for separate low-noise CFG override Tests: - 24 passing tests covering probe variant detection, default settings, noise sampling, end-to-end denoise on a synthetic transformer (CPU), dual-expert boundary swap, CFG branch - 1 heavy-test placeholder gated by INVOKEAI_HEAVY_TESTS=1 for the real-weights smoke test Phase 3+ deferred: standalone VAE/encoder configs, GGUF, LoRA, ControlNet, ref image, inpaint UI, frontend wiring, starter models. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(model): Wan 2.2 Phase 3 + tokenizer-load fix Phase 3 adds standalone VAE and UMT5-XXL encoder configs so users can run GGUF-quantized Wan transformers (Phase 4) without installing the full ~30 GB Diffusers pipeline. VAE configs: - VAE_Checkpoint_Wan_Config + VAE_Diffusers_Wan_Config (16-channel A14B vs 48-channel TI2V-5B, distinguished by decoder.conv_in z_dim). - 16-channel files share the AutoencoderKLWan architecture with Qwen Image; disambiguated via filename heuristic ("wan" in name -> Wan, otherwise -> Qwen Image). Mirror exclusion in QwenImage's probe. - VAELoader gets a Wan branch that builds AutoencoderKLWan(z_dim=...) via init_empty_weights, mirroring the QwenImage single-file pattern. - Existing standard VAE probe excludes both QwenImage- and Wan-style state dicts. UMT5-XXL encoder: - New ModelType.WanT5Encoder + ModelFormat.WanT5Encoder. - WanT5Encoder_WanT5Encoder_Config probes the diffusers folder layout (text_encoder/config.json with model_type=umt5, or flat layout with config.json at root). Refuses full Wan pipelines. - WanT5EncoderLoader handles both layouts and loads UMT5EncoderModel + AutoTokenizer. Component-source plumbing: - WanModelLoaderInvocation now exposes wan_t5_encoder_model and component_source pickers (mirrors QwenImage pattern). Resolution order: standalone > main (if Diffusers) > component_source. Required when the main model is a single-file format in Phase 4. Bug fix in wan_text_encoder: - Tokenizer was loading via AutoTokenizer.from_pretrained(<root>) directly, which fails for nested layouts where files live in <root>/tokenizer/. Now routed through the model cache so the registered loaders handle layout differences correctly. Frontend: - New type guards (isWanVAEModelConfig, isWanT5EncoderModelConfig, isWanMainModelConfig, isWanDiffusersMainModelConfig) and hooks/ selectors (useWanVAEModels, useWanT5EncoderModels, useWanDiffusersModels). New zSubModelType / zModelType / zModelFormat enum entries for transformer_2 and wan_t5_encoder. Tests: - 16 new tests covering z_dim detection, VAE checkpoint/diffusers probes, the bidirectional Qwen-vs-Wan filename deferral, and the UMT5 encoder probe (nested + flat + T5 + full-pipeline rejection). - Total Wan test count: 41 passing, 1 heavy-test placeholder skipped. - Full config test suite (63 tests) still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): unbreak frontend lint after Wan additions Five issues turned up running `make frontend-lint`: 1. wan_denoise.py used `from __future__ import annotations`, which made the `invoke()` return annotation a string ('LatentsOutput'). The InvocationRegistry's `get_output_annotation()` returns the raw annotation, so OpenAPI generation crashed with `'str' object has no attribute '__name__'`. Removed the future-import and added `Any` to the typing imports. 2. ModelRecordChanges.variant didn't list WanVariantType, so the generated schema's install/update endpoints rejected `t2v_a14b` and `ti2v_5b`. Added it. 3. Regenerated frontend/web/src/services/api/schema.ts from the live backend so it now includes BaseModelType.wan, ModelType.wan_t5_encoder, SubModelType.transformer_2, ModelFormat.wan_t5_encoder, the Wan variants, all Wan invocation types and their conditioning/transformer field types. 4. modelManagerV2/models.ts: added `wan_t5_encoder` to the category map, `wan` to the base color/long-name/short-name maps, the two Wan variants to the variant-name map, and `wan_t5_encoder` to the format-name map. 5. ModelManagerPanel/ModelFormatBadge.tsx: added `wan_t5_encoder` to FORMAT_NAME_MAP and FORMAT_COLOR_MAP. `make frontend-lint` now passes cleanly (tsc, dpdm, eslint, prettier). All 41 Wan Python tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> chore(wan): drop unused FE exports flagged by knip These were forward-compatibility wiring for Phase 9 (the FE graph builder) that has no consumers yet; knip rightly flagged them. Removed or de-exported. They'll come back when the graph builder lands and needs them. - common.ts: zWanVariantType drops `export` (still used internally by zAnyModelVariant). - types.ts: drop isWanMainModelConfig, isWanDiffusersMainModelConfig, isWanVAEModelConfig (no callers). The remaining isWanT5EncoderModelConfig is used by models.ts. WanT5EncoderModelConfig type drops `export` (still used as the type guard's narrowing target). - modelsByType.ts: drop the six unused useWan*/selectWan* hooks + selectors and their type-guard imports. `make frontend-lint` (tsc + dpdm + eslint + prettier + knip) now green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> docs(wan): use *-Diffusers HF repo names in plan The Wan-AI org publishes two flavours of each release: * Wan-AI/Wan2.2-{TI2V-5B,T2V-A14B,I2V-A14B} ← upstream native * Wan-AI/Wan2.2-{TI2V-5B,T2V-A14B,I2V-A14B}-Diffusers ← convertible The native release has _class_name=WanModel in config.json and ships weights flat at the repo root with no transformer/, vae/, text_encoder/ subdirs. It is not loadable by Diffusers' WanPipeline.from_pretrained. Update plan doc to reference the -Diffusers repos throughout (probe notes, starter-model entries) so the plumbing path matches what the Diffusers loader actually expects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): accept 0 as 'unset' sentinel for guidance_scale_low_noise The frontend renders Optional[float] inputs with default 0 in the numeric input rather than passing null/unset. Combined with ge=1.0, this caused every wan_denoise invocation to fail Pydantic validation with "Input should be greater than or equal to 1" until the user manually entered a value (or knew to leave the field disconnected). The validation error was rejected before invocation logging, so it never showed up in the server log either - making the failure hard to diagnose. Relaxing the constraint to ge=0.0 and treating values below 1.0 as the "fall back to primary Guidance Scale" sentinel. The user's natural FE default (0) now works as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): correct preview dimensions and colors for TI2V-5B Two bugs in the Wan branch of the diffusion step callback: 1. Wrong dimensions. The reported preview size hardcoded `* 8` for the spatial downscale ratio, but TI2V-5B's Wan2.2-VAE uses 16x. A 1024x1024 target was being announced to the FE as 512x512. 2. Wrong colors. The previous fallback for 48-channel TI2V-5B latents sliced the first 16 channels and applied the standard 16-channel Wan-VAE projection. Those channel layouts are unrelated, so the projection produced meaningless colors. Adding the proper Wan2.2-VAE 48-channel RGB projection matrix (and bias) from ComfyUI's Wan22 latent format, and selecting the right matrix + spatial-scale by latent channel count: 16 → A14B (Wan VAE, 8x), 48 → TI2V-5B (Wan2.2-VAE, 16x). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): honor model's _class_name when building scheduler TI2V-5B's scheduler_config.json declares _class_name=UniPCMultistepScheduler with flow_shift=5.0. The previous code hardcoded FlowMatchEulerDiscreteScheduler.from_pretrained(...), which silently constructed a default-config FlowMatch instead of the UniPC the model expects. The mismatched noise schedule manifests as soft / under-denoised faces and global graininess in the final images. Now: read scheduler_config.json, look up the named class on the diffusers module, and instantiate that class via from_pretrained. UniPC and FlowMatch share the same step()/set_timesteps()/sigmas/num_train_timesteps interfaces, so the denoise loop works transparently for either. A14B continues to use FlowMatchEulerDiscreteScheduler when its scheduler config says so (its reference is FlowMatchEuler with shift=8.0). Falls back to FlowMatchEulerDiscreteScheduler defaults when no on-disk config is available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): match diffusers WanPipeline tokenizer length and latent dtype Two divergences from the Diffusers reference that were hurting image quality (soft / grainy / distorted faces at default settings): 1. Tokenizer max_sequence_length was 226 in wan_text_encoder, but the model was trained with 512-token sequences. The upstream native config.json has text_len: 512, and Diffusers' WanPipeline.__call__ default is 512 (overriding _get_t5_prompt_embeds's stale 226 default). Wan's cross-attention sees padded zeros past the prompt's actual length but expects to be looking at a 512-position context window. 2. Latents were stored in bf16 throughout the denoise loop. Diffusers' WanPipeline.prepare_latents explicitly uses dtype=torch.float32 and only casts to the transformer's dtype right at the forward call: latent_model_input = latents.to(transformer_dtype) Storing in bf16 between steps accumulates ~40 steps of bf16 quantization on the scheduler's small per-step deltas. Now latent_dtype = torch.float32 throughout, with a per-step cast for the transformer forward pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> chore(wan): add diffusers reference comparison script scripts/wan_diffusers_reference.py runs a Diffusers-format Wan 2.2 checkpoint directly via WanPipeline.from_pretrained, with the same arguments InvokeAI's wan_denoise uses. Use to A/B against InvokeAI output when image quality is questionable. Defaults to enable_model_cpu_offload so the script fits on 16 GB cards where the full pipeline (transformer + UMT5-XXL + VAE) would otherwise OOM. --offload {model,sequential,none} controls the strategy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(model): Wan 2.2 Phase 4 - GGUF transformer support Adds single-file GGUF support for Wan 2.2 transformers, the path that makes A14B usable on consumer GPUs (~7 GB/expert at Q4_K_M instead of ~28 GB at bf16). Probe (configs/main.py): - New helpers: _has_wan_keys (Wan vs Qwen/FLUX/Z-Image fingerprint via condition_embedder.text_embedder.linear_1 + patch_embedding); _detect_wan_gguf_variant (16ch -> A14B, 48ch -> TI2V-5B from patch_embedding.weight.shape[1]); _detect_wan_gguf_expert (filename heuristic for high_noise / low_noise / none). - Main_GGUF_Wan_Config(base=Wan, format=GGUFQuantized, variant, expert). Tolerates the ComfyUI 'model.diffusion_model.' / 'diffusion_model.' prefixes via _has_wan_keys' multi-prefix scan. - Registered in factory.py. Loader (model_loaders/wan.py): - WanGGUFCheckpointModel mirrors the QwenImage GGUF pattern: gguf_sd_loader -> strip ComfyUI prefix -> auto-detect arch from state dict shapes (num_layers, inner_dim, ffn_dim, text_dim, in_channels, num_heads = inner_dim/128) -> init_empty_weights + load_state_dict(strict=False, assign=True). Loader invocation (wan_model_loader.py): - New 'Transformer (Low Noise)' picker: optional second GGUF for the A14B dual-expert MoE. Auto-swaps if the user wired the experts in the wrong order. Warns when an A14B GGUF is loaded without a paired low-noise expert (single-expert run, degraded quality). - GGUF mains require either a standalone VAE+encoder or a Diffusers Component Source (which can also supply boundary_ratio). - Diffusers main path unchanged (still pulls both experts from transformer/ + transformer_2/). Tests (tests/.../test_wan_gguf_config.py): - 14 tests across key fingerprint, variant detection, expert filename heuristic, and the full probe (A14B high/low, TI2V-5B, GGUF rejection, unrecognised state-dict rejection, explicit override). Total Wan tests: 55 passing (no regressions). FE lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): support QuantStack-style GGUFs and standalone Diffusers VAE The city96 Wan 2.2 GGUF repos have been removed from Hugging Face, leaving QuantStack as the surviving distributor. QuantStack ships the native upstream Wan key layout (text_embedding.0/2, self_attn/cross_attn, ffn.0/2, head.head, head.modulation, ...) rather than the diffusers naming city96 used; biases are stored as F16 rather than BF16; and the standalone Wan VAE installs as a flat AutoencoderKLWan folder which the generic loader rejects. Three fixes: 1. Probe now recognises both diffusers and native key layouts via a new _is_native_wan_layout helper; _has_wan_keys accepts either text-proj fingerprint. 2. GGUF loader converts native -> diffusers keys (mirroring diffusers' convert_wan_transformer_to_diffusers) and unwraps non-quantized GGMLTensors to plain tensors at compute_dtype. The unwrap is needed because conv3d isn't in GGMLTensor's dispatch table, so the F16 patch_embedding bias would otherwise hit conv3d against bf16 latents. 3. VAELoader gains a VAE_Diffusers_Wan_Config branch that loads AutoencoderKLWan directly; the generic path can't handle a flat single-class folder when a submodel_type is provided. Adds 12 tests covering the native layout (probe + converter + unwrap). Verified end-to-end against Wan2.2-T2V-A14B-Q4_K_M from QuantStack: 1095 tensors round-trip key-for-key against WanTransformer3DModel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(model): Wan 2.2 Phase 5 - LoRA support Probe + config (LoRA_LyCORIS_Wan_Config): - Detects Wan LoRAs in three layouts: diffusers PEFT, native upstream PEFT (ComfyUI), and Kohya (both naming variants). - Anti-pattern guards prevent collisions with Anima (Cosmos DiT q_proj convention), QwenImage (transformer_blocks), Flux (double/single blocks), and Z-Image (diffusion_model.layers). - Optional ``expert: "high" | "low" | None`` field; auto-detected from filename (high_noise / low_noise / hyphenated / concatenated variants). Key conversion (wan_lora_conversion_utils): - Native upstream keys (self_attn/cross_attn, ffn.0/2) -> diffusers (attn1/attn2, ffn.net.0.proj / ffn.net.2). - Strips ``transformer.``, ``diffusion_model.``, ``base_model.model.transformer.`` prefixes from PEFT-style keys. - Kohya layer names mapped through an explicit longest-match table. - Output paths use diffusers naming so the LayerPatcher can resolve them against WanTransformer3DModel parameter paths. Loader integration: - Adds BaseModelType.Wan branch to LoRALoader._load_model. Invocation nodes (wan_lora_loader.py): - WanLoRALoaderInvocation: single LoRA with auto/both/high/low target field. - WanLoRACollectionLoader: list of LoRAs, auto-routed by each LoRA's recorded expert tag. - Output WanLoRALoaderOutput carries the WanTransformerField with updated ``loras`` / ``loras_low_noise`` lists. Denoise integration: - _ExpertSwapper now manages both the model_on_device context and the LayerPatcher.apply_smart_model_patches context per expert. LoRA patches are entered after device load and exited before device release, with fresh iterators per swap. - GGUF (quantized) experts request sidecar patching so GGMLTensor weights aren't touched directly. - Low-noise expert falls back to the primary loras list when ``loras_low_noise`` is empty (matches WanTransformerField semantics). Tests: 81 new tests covering probe accept/reject across formats, anti-pattern guards on competing architectures, converter round-trips for all three layouts, invocation target resolution + routing + duplicate guards, and the _ExpertSwapper lifecycle (lora context opens/closes in the right order around the device swap, quantized flag forwards, no-LoRA path skips the patch context, re-entering the same label is a no-op). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): probe Wan LoRA before Anima in the config union Native-PEFT Wan LoRAs (lightx2v's Lightning, most ComfyUI-trained Wan LoRAs) carry keys like ``diffusion_model.blocks.X.cross_attn.k.lora_A.weight``. Anima's probe matches on the bare ``cross_attn``/``self_attn`` substring — it does not require the Anima-specific ``_proj`` suffix nor any of the ``mlp``/``adaln_modulation`` Cosmos DiT markers — so these Wan LoRAs were classified as ``BaseModelType.Anima`` because Anima happened to run first. Reorder the LyCORIS section of ``AnyModelConfig`` so Wan probes first. Wan's probe is strictly more restrictive (it rejects Anima's ``_proj`` attention suffix via the anti-pattern guard added in the previous commit), so Anima LoRAs are still correctly classified after this reorder. Existing users with mis-tagged installs need to delete the affected LoRA records and reinstall. Adds two regression tests: a union-ordering assertion, and a sanity check that demonstrates Anima's probe *would* match Wan native keys if asked directly — pinning the constraint that motivates the ordering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> chore(i18n): add Wan2.2 T5 Encoder model-manager label The frontend source already references ``modelManager.wanT5Encoder``; the locale key was added with a casing typo (``want5Encoder``). Fix the key so the Wan T5 Encoder model type renders its display name correctly in the model manager UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(model): Wan 2.2 Phase 7 - reference-image (I2V) conditioning Re-implementation after the first attempt — which used CLIP-vision conditioning — was reverted. Wan 2.2 I2V-A14B does NOT use a CLIP-vision encoder (the Diffusers repo ships ``image_encoder: [null, null]`` in ``model_index.json``); instead it conditions on a reference image by VAE-encoding it and concatenating the resulting latents (plus a first-frame mask) to the noise latents along the channel dim. The I2V transformer therefore has ``in_channels=36`` (16 noise + 16 ref-image latents + 4 mask) vs ``in_channels=16`` for T2V. Taxonomy: - Re-adds ``WanVariantType.I2V_A14B``. Probes: - Diffusers: ``_detect_wan_variant`` reads ``transformer/config.json::in_channels``; 36 → I2V_A14B, 16 → T2V_A14B (both share the dual-expert layout). - GGUF: ``_detect_wan_gguf_variant`` recognises ``in_channels=36`` from the patch_embedding tensor shape and emits I2V_A14B. Backend extension (``backend/wan/extensions/wan_ref_image_extension.py``): - ``preprocess_reference_image`` resizes + normalises to a 5D pixel tensor. - ``encode_reference_image_to_condition`` VAE-encodes the image and stacks a 4-channel first-frame mask on top, producing the ``[1, 20, 1, H/8, W/8]`` condition tensor the denoise loop consumes. - Mirrors diffusers ``WanImageToVideoPipeline.prepare_latents`` with ``num_frames=1`` and ``expand_timesteps=False``. Invocation node (``wan_ref_image_encoder.py``): - "Reference Image - Wan 2.2": image + VAE + width/height pickers. - Output ``WanRefImageConditioningField`` carries the condition tensor name plus the dimensions used (so the denoise step can validate dim parity). Denoise integration: - ``WanDenoiseInvocation`` gains an optional ``ref_image`` field. - Variant gate: rejects ref_image on T2V_A14B and TI2V-5B with a clear error before doing any work. - Dimension gate: rejects ref-image width/height mismatch vs denoise. - At every transformer call, concatenates the 20-channel condition tensor to the 16-channel noise latents along the channel dim before passing to the transformer (giving the 36-channel input I2V expects). Tests: 14 new across the probe, the extension, and the denoise loop. The synthetic ``_ZeroTransformer`` test stand-in now mirrors the real I2V transformer's ``in_channels=36, out_channels=16`` asymmetry by slicing its zero output back to 16 channels when the input is 36-wide. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): derive GGUF out_channels from proj_out shape (I2V support) The GGUF loader was setting ``out_channels = in_channels`` which is wrong for Wan 2.2 I2V-A14B: that variant has ``in_channels=36`` (16 noise + 16 ref-image latents + 4 first-frame mask, concatenated by the denoise loop) but ``out_channels=16`` since the transformer only predicts the noise component back. Loading an I2V GGUF would build a transformer with the wrong proj_out shape and crash: RuntimeError: Error(s) in loading state_dict for WanTransformer3DModel: size mismatch for proj_out.weight: copying a param with shape torch.Size([64, 5120]) from checkpoint, the shape in current model is torch.Size([144, 5120]). (144 = 36 * 4, 64 = 16 * 4 — patch_size=(1, 2, 2) → prod=4) Read out_channels directly from the ``proj_out.weight`` shape in the state dict. This is correct for all three Wan 2.2 variants without needing to know the variant in advance. Also tighten the num_layers fallback: T2V_A14B and I2V_A14B share 40 layers; only TI2V-5B has 30. The fallback is rarely hit in practice (the per-block count comes from the state dict scan), but the previous code would have defaulted I2V_A14B to 30 layers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(model): make Anima LoRA probe mutually exclusive with Wan InvokeAI's ``Config_Base.CONFIG_CLASSES`` is a Python ``set``, so iteration order during model probing is non-deterministic across process restarts. First-match-wins ordering in ``AnyModelConfig`` is documentation only — it has no effect on which config is iterated first. Anima's previous probe accepted any state dict containing the substring ``cross_attn`` or ``self_attn``, which collides with Wan's native LoRA key layout (``diffusion_model.blocks.X.cross_attn.q.lora_down.weight``). Both probes accepted Wan native LoRAs (including lightx2v's Lightning T2V and I2V distillations), and the ``matches.sort_key`` tiebreaker only disambiguates by ModelType, not within LoRA configs. So which config "won" depended on dict hash order — sometimes Wan, sometimes Anima. The previous mitigation reordered the AnyModelConfig union to put Wan before Anima. That worked by luck and was inherently fragile. Tighten Anima's probe to require Cosmos-DiT-exclusive subcomponents: ``mlp``, ``adaln_modulation``, or ``_proj``-suffixed attention names (``q_proj``/``k_proj``/``v_proj``/``output_proj``) — none of which appear in any Wan LoRA. Wan native uses bare ``.q``/``.k``/``.v``/``.o`` on ``self_attn``/``cross_attn``, and ``ffn.N``/``ffn.net.N`` instead of ``mlp``. The new strict detectors live alongside the original loose ones so the Anima conversion utility (which runs after probing) still works. Regression tests in ``test_wan_lora_probe_independence.py`` cover: - I2V Lightning V1 (the bug-triggering LoRA), T2V Lightning V2, Wan Kohya and Wan diffusers PEFT layouts — Wan probe accepts, Anima probe rejects. - Anima PEFT and Kohya layouts — Anima accepts, Wan rejects. - A meta-test that runs every LoRA config in CONFIG_CLASSES against the Lightning state dicts and asserts exactly one accepts — this catches ANY future probe collision, not just Wan vs Anima. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): defer expert model loading in _ExpertSwapper to avoid cache thrash The swapper used to take pre-loaded ``LoadedModel`` handles at construction: high_info = context.models.load(self.transformer.transformer) low_info = context.models.load(self.transformer.transformer_low_noise) swapper = _ExpertSwapper(high_info=high_info, low_info=low_info, ...) With dual ~9 GB A14B GGUF experts plus the ~10 GB UMT5-XXL encoder competing for the same RAM cache, the LRU policy frequently dropped one expert by the time the denoise loop swapped into it. The model manager then emitted [MODEL CACHE] Locking model cache entry ... but it has already been dropped from the RAM cache. This is a sign that the model loading order is non-optimal in the invocation code (See ... #7513). and reloaded the weights from disk (~1.2s extra per swap). Refactor the swapper to take the ``ModelIdentifierField`` plus the ``InvocationContext`` and call ``context.models.load(model_id)`` lazily inside ``get()``. Each swap obtains a fresh handle, the LRU window is small, and the warning goes away. Config metadata (used to compute ``is_quantized``) is read upfront via ``context.models.get_config()`` — that's metadata, not weights, so it doesn't put pressure on the cache. Tests: existing swapper lifecycle tests refactored to use a fake context whose ``models.load`` is logged. A new ``test_lazy_load_per_swap_not_upfront`` pins the regression — it asserts ``models.load`` is NOT called at swapper construction, only at first get() per expert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(wan): add Phase 8 inpaint regression tests The denoise_mask wiring + RectifiedFlowInpaintExtension integration in wan_denoise.py was put in place during Phase 2/3 alongside the rest of the denoise loop. Phase 8 of the plan was about ensuring this path worked and is locked in by tests. Three new tests under TestWanDenoiseInpaint: 1. test_preserved_region_matches_init_exactly: builds a half/half mask (left = preserve, right = regenerate in user-side convention), runs full denoise with the synthetic zero-output transformer, and asserts the preserved half of the final latents equals the init exactly while the regenerated half does not. Pins the mask-inversion + per-step merge behavior. 2. test_inpaint_requires_init_latents: a mask without init latents must raise a clear ValueError — the merge has nothing to weld back to. 3. test_no_mask_path_is_unchanged: regression that adding the inpaint extension didn't perturb the non-inpaint codepath (with init latents + denoising_start=0.5 but no mask, the loop just runs img2img). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(frontend): add I2V_A14B to Wan variant zod enum + manager label Phase 7 added the I2V_A14B backend variant. The frontend's zod enum (features/nodes/types/common.ts:zWanVariantType) and the model manager's variant-label map (features/modelManagerV2/models.ts) were still on the two-variant list, so: - ModelIdentifierField inputs with ui_model_variant filters on Wan couldn't list I2V models. - The model manager UI showed a raw 'i2v_a14b' string instead of the human label. Phase 9 (full linear-view wiring — type guards, hooks, params slice, graph builder, tab UI) is in progress on a follow-up commit; this lands the two small enum fixes first so the I2V probe / install paths work correctly end-to-end with the existing FE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): Phase 9 piece #1 - linear-view T2V txt2img graph builder Adds the minimum frontend wiring needed to generate Wan 2.2 images from the linear view: - buildWanGraph.ts (new): text-to-image graph (model_loader → text_encoder × 2 → denoise → l2i). Diffusers main model only — transformer, VAE and UMT5 encoder all resolve from the same repo, so no Wan-specific params slice fields are required yet. CFG-skip branch when guidance_scale ≤ 1.0. - useEnqueueGenerate / useEnqueueCanvas dispatchers: route base === 'wan' to buildWanGraph. - graph/types.ts: add wan_l2i / wan_i2l / wan_denoise / wan_model_loader to the relevant node-type unions. - addTextToImage / addImageToImage: include wan_denoise / wan_l2i so width/height are wired correctly and the txt2img helper accepts the Wan l2i node. - isMainModelWithoutUnet: include wan_model_loader (Wan has no UNet, same as the other modern bases). - metadata.py: add wan_txt2img / wan_img2img / wan_inpaint to the generation_mode enum (img2img / inpaint pieces land next). - schema.ts: regenerated to pick up the metadata enum + new Wan invocations. Pieces left in Phase 9: params slice (standalone VAE / T5 / GGUF low-noise / LoRA / ref-image fields + selectors), img2img + I2V + inpaint branches in the graph builder, and Wan-specific UI components. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #2 - GGUF support and CFG-Low control in linear view Adds the three Wan-specific params + UI controls that gate GGUF workflows plus a separate low-noise CFG slider for A14B users. Params slice: - wanTransformerLowNoise (the second-expert GGUF for A14B) - wanComponentSource (Diffusers Wan model providing VAE + UMT5-XXL when the main is a GGUF) - wanGuidanceScaleLowNoise (optional separate CFG for the low-noise expert; null = fall back to the primary CFG) Plus a `selectIsWan` selector for accordion gating. UI components: - ParamWanModelSelects.tsx (Advanced accordion): two model pickers — Transformer (Low Noise) filtered to Wan GGUF mains, and VAE/Encoder Source filtered to Wan Diffusers mains. Mirrors the ParamQwenImageComponentSourceSelect structure. - ParamWanGuidanceScaleLowNoise.tsx (Generation accordion): slider + number input with an "auto" indicator when cleared. Default 3.5 matches the diffusers reference 4.0 / 3.0 split. Wiring: - Generation accordion: ParamWanGuidanceScaleLowNoise shown when base is wan, scheduler excluded for wan (same pattern as Anima/Qwen). - Advanced accordion: ParamWanModelSelects shown when base is wan, and Wan excluded from the SD-family VAE/CFG-rescale blocks. - buildWanGraph.ts: forwards the three new params to the model loader and denoise nodes (transformer_low_noise_model, component_source, guidance_scale_low_noise) and adds them to the graph metadata. Hooks/types: - useWanDiffusersModels + useWanGGUFModels in modelsByType.ts. - isWanDiffusersMainModelConfig + isWanGGUFMainModelConfig type guards. - Three new locale strings (wanComponentSource, wanTransformerLowNoise, wanGuidanceScaleLowNoise[Auto]). GGUF workflow now works end-to-end in the linear view: pick a Wan GGUF main, set Transformer (Low Noise) to the paired second-expert GGUF, set VAE/Encoder Source to any Diffusers Wan repo (TI2V-5B is convenient at ~12 GB) — generate produces an image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): UX polish on the Wan linear-view controls Bundles four small fixes applied during a usability review of the Wan linear-view section (piece #2): 1. **Filter Main vs Transformer (Low Noise) dropdowns by expert tag.** The Wan GGUF probe records each file's ``expert`` field (``"high"`` / ``"low"`` / ``"none"``) via filename heuristic. - ``MainModelPicker``: hides ``expert === 'low'`` Wan GGUFs so users can't accidentally wire a low-noise expert as the primary main. - Transformer (Low Noise) picker (``useWanGGUFLowNoiseModels``): shows ``expert === 'low'`` Wan GGUFs only. Diffusers Wan mains and TI2V-5B aren't affected — they don't carry the ``expert`` field on their config schema. The backend's auto-swap safety net stays in place. 2. **Match the primary CFG slider's range.** The Wan low-noise CFG slider was constrained to 1–10 while the primary CFG ranges 1–20. With the diffusers reference 4/3 split, the low-noise slider thumb sat noticeably further right than the primary — visually misleading. Both sliders now share the 1–20 range with marks at [1, 10, 20]. 3. **Label fits the form column.** "CFG (Low Noise)" → "CFG (Low)" so the slider fits cleanly next to its label instead of overlapping. 4. **Indicator state for the low-noise CFG slider.** Replaced the inline "(auto)" / "(same as cfg)" text — which kept overlapping the slider regardless of how short the label got — with an X-only reset button that's only visible when the user has set an explicit value. Absence of the X conveys auto/fallback state without any text overhang. 5. **Friendlier Transformer (Low Noise) placeholder.** "Second-expert GGUF for A14B (pair with the high-noise main)" → "Add for full detail" — concise nudge for users who haven't paired the second expert yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #3 - linear-view img2img branch Adds Wan 2.2 image-to-image to the linear view, mirroring the Qwen Image pattern. The mode switches on the canvas state — pure-prompt runs go through addTextToImage as before; canvas runs with an init image go through addImageToImage which wires a fresh wan_i2l (Image to Latents - Wan 2.2) node between the init image and the denoise's `latents` input, honoring the existing denoise_start slider. buildWanGraph: - Drops the txt2img-only guard, branches on generationMode. - img2img: spins up a wan_i2l node and hands it to addImageToImage alongside the existing denoise / l2i / modelLoader (as vaeSource). - inpaint / outpaint still fail loudly — pieces #4-#6. graphBuilderUtils.getDenoisingStartAndEnd: - Adds 'wan' to the simple-linear case (denoising_start = 1 - denoisingStrength). Note: Wan's flow-matching schedule is "sticky" on the init compared to SDXL — users will likely need denoisingStrength ≥ 0.7 to see substantial change, matching the user-found 0.15-0.3 denoising_start sweet spot from earlier img2img testing. We may revisit this with an exponent rescale (like FLUX uses) if the response curve feels off. addImageToImage: - Adds 'wan_i2l' to the i2l-node-type union so the Wan i2l can be threaded through the shared helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): add wan_denoise to addImageToImage/addInpaint/addOutpaint type checks Three sibling graph-helper utilities had the same modern-base list as addTextToImage did, and the buildWanGraph img2img branch tripped one of them at canvas-Generate time: error [generation]: Failed to build graph {name: 'Error', message: 'Wrong assertion encountered'} The else-branch in each helper assumes 'denoise_latents' (the SD1.5/SDXL legacy path) and asserts that — failing for any modern base not listed above the branch. addTextToImage was already updated in Phase 9 piece #1; this catches the parallel cases that the img2img/inpaint/outpaint flows go through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #4 - linear-view inpaint and outpaint branches Wires Wan 2.2 inpaint and outpaint through the existing addInpaint / addOutpaint helpers. The backend's RectifiedFlowInpaintExtension was plumbed into wan_denoise.py back in Phase 8 (commit ab54617173); this just connects the FE. buildWanGraph: - generationMode === 'inpaint' → spin up a wan_i2l, call addInpaint with denoise + l2i + modelLoader (used as both vaeSource and modelLoader since the Wan model loader carries the VAE). - generationMode === 'outpaint' → parallel branch with addOutpaint. addInpaint: - i2l-node-type union now includes 'wan_i2l' (the addImageToImage and addOutpaint type unions already do — different union shapes). metadata.py: - generation_mode literal adds "wan_outpaint" alongside the existing wan_txt2img / wan_img2img / wan_inpaint entries. isMainModelWithoutUnet already includes wan_model_loader (Phase 9 piece create_gradient_mask when Wan is the main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #5 - linear-view I2V branch (raster as reference image) Wan 2.2 I2V-A14B models condition on a reference image whose VAE-encoded latents are concatenated to the noise along the channel dim each step (in_channels=36 on the I2V transformer). In the linear view this maps cleanly onto the existing canvas raster layer: pick an I2V model, drag an image to raster, generate. buildWanGraph: - Fetch the modelConfig early so the variant gate (i2v_a14b vs the rest) can drive the branch shape instead of being a post-hoc check. - I2V + txt2img: fail loudly ("Switch to the canvas tab and drag an image to the raster layer"). I2V models won't produce useful output without a reference, and the backend would crash trying to concatenate a missing condition tensor. - I2V + img2img: pull the raster image via the canvas compositor, wire it through a wan_ref_image_encoder (which VAE-encodes it and builds the 4-mask + 16-latent condition tensor backend-side), then feed the result into denoise.ref_image. Denoise runs from fresh noise (denoising_start=0, no init_latents) — the ref image is cross-attention/concat conditioning, not a noise-trajectory anchor. - I2V + inpaint/outpaint: fail clearly. Combining ref-image conditioning with a denoise mask is conceptually possible but the backend interaction hasn't been validated end-to-end. metadata.py: - Adds "wan_i2v" to the generation_mode literal so the metadata field on I2V renders correctly. T2V flows (txt2img / img2img / inpaint / outpaint) are unchanged for non-I2V Wan variants (T2V-A14B and TI2V-5B). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): enforce multiple-of-16 dimensions to match transformer patch grid Wan 2.2's transformer has ``patch_size=(1, 2, 2)``: it patch-embeds with stride 2 then un-patches by 2. Combined with the VAE's 8x spatial scale, canvas H/W must be a multiple of ``8 * 2 = 16`` — not just 8 — for the patch round-trip to land exactly. Otherwise the latents and noise prediction disagree by one in the spatial dim and the scheduler step fails: RuntimeError: The size of tensor a (147) must match the size of tensor b (146) at non-singleton dimension 3 (here latent_w=147 → patch_w=73 → un-patched_w=146 ≠ 147) This was silent for T2V at 1024x1024 (already a multiple of 16) but fired for I2V at non-multiple-of-16 canvas sizes. Fixes: - ``optimalDimension.getGridSize``: Wan moves from the default 8 case to the multiple-of-16 case (alongside flux / sd-3 / qwen-image / z-image which have the same patch arithmetic). The canvas bbox UI now snaps Wan dimensions to multiples of 16. - ``wan_denoise.py`` and ``wan_ref_image_encoder.py``: bump width/height ``multiple_of`` from 8 to 16. Defense-in-depth — workflow-editor users won't be able to send a non-16-aligned dim either. Existing backend tests (23 passing) still hold — 1024 is divisible by 16 so the test fixtures didn't exercise the off-by-one path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): show negative prompt box in Wan linear-view Wan was missing from SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS, so the linear-view negative-prompt input was hidden even though the Wan denoise node already wires negative conditioning when CFG > 1 (buildWanGraph.ts:67-75). Adds 'wan' to the list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #6 - Wan LoRA collection in linear view Adds Wan LoRA wiring to buildWanGraph, mirroring the Qwen Image pattern. The shared LoRASelect / LoRAList UI in the linear view already filters LoRAs by the selected main model's base, so Wan LoRAs surface automatically when a Wan main is picked — no UI changes needed. addWanLoRAs (new): - Filters state.loras.loras to enabled Wan LoRAs. - For each LoRA: spawns a ``lora_selector`` node and threads it through a single ``collect`` collector. - Routes the collector into a ``wan_lora_collection_loader`` which sits between modelLoader and denoise — modelLoader.transformer → loader, then loader.transformer → denoise (rerouting the original modelLoader → denoise edge). - Emits per-LoRA metadata so PNG metadata + workflow restore work. The dual-expert routing (high-noise vs low-noise vs untagged) is handled entirely on the backend by ``WanLoRACollectionLoader`` based on each LoRA's recorded ``expert`` tag (set by the probe from the filename heuristic in piece #5 of Phase 5). The FE just hands over the bag of LoRAs; no per-list FE plumbing needed. buildWanGraph: - Calls addWanLoRAs(state, g, denoise, modelLoader) after the base transformer edge is in place. The helper is a no-op when no Wan LoRAs are enabled, so it's safe to call unconditionally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): detect LoRA variant and filter by main model Wan 2.2 A14B (inner_dim=5120) and TI2V-5B (inner_dim=3072) LoRAs are not interchangeable — applying one against the wrong main model crashes the layer patcher with a tensor-shape error (e.g. A14B Lightning on TI2V-5B mains produced ``shape '[3072, 3072]' is invalid for input of size 26214400``). Probe Wan LoRAs' inner-dim at install time and record the family on a new ``variant`` field (``a14b`` / ``5b`` / null). The LoRA picker in the linear view hides incompatible variants when the user selects a main, and the graph builder filters any still-enabled mismatches at submit time with a warning. Untagged LoRAs (probe couldn't identify) pass through so they aren't silently hidden. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): ref-image panel, GGUF readiness, and auto-default sources Wan 2.2 I2V now uses the global Reference Images panel (same UX as Qwen Image Edit and FLUX.2 Klein) instead of pulling the conditioning image from a canvas raster layer. Adds: - WanReferenceImageConfig zod type + isWanReferenceImageConfig guard; integrated into the ref-image discriminated union, settings panel, layer hooks, and validators. - 'wan' added to SUPPORTS_REF_IMAGES_BASE_MODELS, but the panel only shows for the i2v_a14b variant (T2V and TI2V-5B don't consume ref images, so the panel is hidden for them). - buildWanGraph I2V branch reads the first enabled wan_reference_image from refImagesSlice; the canvas-raster-as-ref path is removed. I2V now only supports txt2img mode (canvas img2img/inpaint/outpaint assert with a clear message). GGUF Wan readiness check: GGUF mains carry only the transformer, so the loader needs a Diffusers Component Source (or standalone VAE + UMT5-XXL encoder) to resolve the VAE and text encoder. Without one, enqueue is now blocked with a clear reason. The low-noise A14B partner expert remains optional (loader falls back to the high-noise expert when it's missing). Adds standalone Wan VAE and Wan T5 Encoder selectors to the Advanced accordion (Qwen pattern). Wires them as vae_model / wan_t5_encoder_model on the wan_model_loader node — backend priority is standalone > diffusers main > component source. Auto-default on Wan selection (so GGUF users don't have to fiddle with Advanced): when the new main is a Wan GGUF, fill the Component Source, standalone VAE, and standalone T5 encoder with first available matches if not already set. Component Source is matched by variant family (A14B GGUF prefers an A14B Diffusers; TI2V-5B prefers a TI2V-5B Diffusers) since the two families use different VAE channel counts (16 vs 48); within A14B, T2V and I2V share VAE/encoder so they're interchangeable as a source. Runs on every Wan selection (including Diffusers -> GGUF switches), only fills empty slots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 starter models and bundle Wan 2.2 starter pack (selected when the user picks the Wan 2.2 bundle) brings up the minimal-cost path to running A14B T2V end-to-end: - Standalone UMT5-XXL encoder and A14B VAE (so GGUF mains don't need a full Diffusers download for their VAE/encoder sources). - T2V A14B Q4_K_M and Q8_0 GGUF expert pairs (high + low noise). - T2V Lightning V1.1 Seko rank-64 LoRA pair (4-step inference). Additional Wan 2.2 starter models browseable from the model manager: - Full Diffusers T2V A14B, I2V A14B, and TI2V-5B. - I2V A14B Q4_K_M and Q8_0 GGUF expert pairs + Lightning V1 LoRA pair. - TI2V-5B Q4_K_M and Q8_0 GGUFs + the 48-channel TI2V-5B VAE. Each "high noise" GGUF lists its low-noise partner plus the shared VAE and UMT5-XXL encoder as dependencies, so installing one of them pulls in everything the loader needs. QuantStack's HighNoise/LowNoise file naming and lightx2v's high_noise_model/low_noise_model.safetensors are both picked up by the existing filename heuristic in the GGUF probe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> docs(wan): add Wan 2.2 hardware requirements Adds Wan 2.2 A14B (T2V/I2V) and TI2V-5B rows to the hardware requirements table with rough VRAM/RAM guidance per quantization. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): recall low-noise transformer, component source, and standalone VAE/T5 Wan-specific metadata fields embedded by the graph builder (wan_transformer_low_noise, wan_component_source, wan_vae_model, wan_t5_encoder_model, wan_guidance_scale_low_noise) had no recall handlers in features/metadata/parsing.tsx, so recalling an image's parameters would leave these fields empty. Adds a handler for each that dispatches the matching paramsSlice action and renders a row in the metadata viewer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add default Wan 2.2 T2V and I2V workflows Ships two default workflows in the library, tagged so they appear in "Browse Workflows" under the wan2.2 / text to image / image to image tags: - Text to Image - Wan 2.2: full T2V/TI2V-5B graph (model loader, positive + negative encoders, denoise, l2i). Exposes the five model slots, prompts, steps, dual CFG, and dimensions. - Image to Image - Wan 2.2: I2V A14B graph that adds a wan_ref_image_encoder. Exposes the reference image input plus the standard fields. Both follow default-workflow rules: IDs prefixed with default_, meta.category = "default", and no references to user-installed resources. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 1 - backend video storage, records, REST API Adds a parallel video pipeline alongside the existing image pipeline so the gallery can host MP4 alongside PNGs. Implements: - New service modules (parallel to image equivalents): video_records/ record store + sqlite impl video_files/ disk file store (mp4 + first-frame webp thumb) videos/ orchestrating service board_video_records/ board <-> video association - migration_32 creates `videos` and `board_videos` tables - /api/v1/videos/ router: upload, list, get DTO, /full (with HTTP Range so HTML5 <video> seek/scrub works), /thumbnail, /metadata, star/unstar, delete, batch delete, board add/remove - LocalUrlService.get_video_url and SimpleNameService.create_video_name - imageio[ffmpeg] dep for video encode (used in later phases) - Wires all four new services into InvocationServices, dependencies.py, api_app.py, and three test fixtures Verified end-to-end against an in-memory db + tmp output dir: upload, probe, save (file + thumbnail + record), DTO build, list, delete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 2 - polymorphic gallery list endpoint Adds /api/v1/gallery/items/ and /api/v1/gallery/items/names returning a unified time-sorted stream of images + videos so the frontend can render them interleaved with a single virtualized query. - gallery_common: GalleryItem discriminated union (kind + name + shared fields + nullable video duration/fps), GalleryItemRef, names result - gallery_default: SqliteGalleryService implements UNION ALL across the images and videos tables, applying identical filters (origin/category/ is_intermediate/board_id/search) to each half; pagination via outer ORDER BY + LIMIT/OFFSET; counts are summed across the two halves - URLs are resolved at row -> DTO conversion time so each item routes to the correct /api/v1/images or /api/v1/videos endpoint - Wired into InvocationServices, dependencies.py, api_app.py, and the three test fixtures Existing /api/v1/images endpoints are unchanged so any non-gallery consumers (queue, recall, metadata workflows) continue to work as-is. Verified e2e: 2 images + 2 videos inserted in alternating order, both list_items and list_item_names return the correct interleaved order; category filter narrows to a single kind; starring an item bumps it to the top when starred_first=True. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 3 - frontend RTK endpoints + MP4 upload routing Adds the typed API surface and upload integration so videos can be uploaded through the same gallery upload button that handles images. Schema: re-ran pnpm typegen against the running backend to pick up VideoDTO, VideoRecordChanges, GalleryItem, GalleryItemKind, GalleryItemRef, GalleryItemNamesResult and the two new paginated result types. RTK Query (services/api/endpoints/videos.ts) - parallel to images.ts: listVideos, getVideoDTO, getVideoMetadata, getVideoNames, uploadVideo, deleteVideo / deleteVideos, changeVideoIsIntermediate, starVideos / unstarVideos, addVideoToBoard / removeVideoFromBoard. Imperative helpers (getVideoDTO, getVideoDTOSafe, uploadVideo, uploadVideos) and the useVideoDTO convenience hook ride alongside, mirroring the image side. Tag types and invalidation: added Video / VideoList / VideoMetadata / VideoNameList / BoardVideosTotal / GalleryItemList / GalleryItemNameList to the api root. Board-affecting mutations now invalidate the polymorphic gallery list/name caches so videos and images stay coherent once the gallery wiring lands in Phase 4. Added a sibling getTagsToInvalidateForVideoMutation helper. Upload UX: useImageUploadButton.tsx's dropzone now accepts video/mp4, video/webm, video/quicktime alongside the existing image MIMEs. The drop handler splits files into image/video sets and routes each through its own mutation; a new onUploadVideo callback parallels the existing onUpload. Existing image-only callers pass through unchanged. Polymorphic gallery query endpoints + the useGalleryItemDTO hook will land with Phase 4 where they have actual consumers; the schema types they'll need are already in place under @knipignore tags. Verified: pnpm lint (knip, dpdm, eslint, prettier, tsc) all green; pnpm test 1103/1103 pass; live curl against the running dev server uploads an MP4 and serves both the webp thumbnail and the MP4 with a working HTTP Range response (206 + Content-Range). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 4 - mixed gallery grid with video play badge Videos now appear in the same gallery grid as images, interleaved by created_at. Video thumbnails get a centered play-button badge so they read as videos at a glance; everything else (selection, virtualization, search, paged/virtual gallery views, keyboard nav) is unchanged. Approach: selection state stays `string[]` of names. The kind is recovered from the filename extension (.mp4 = video, anything else = image), which is reliable because the backend's SimpleNameService always emits `<uuid>.png` for images and `<uuid>.mp4` for videos. This sidesteps a 32-file cross-cut from changing the selection shape to a discriminated union, and selection is persist-denylisted so no migration is needed. Frontend: - new isVideoName helper in features/gallery/store/types - new endpoints/gallery.ts (deferred from Phase 3): useGetGalleryItemNamesQuery - new ImageGrid/GalleryItemPlayBadge: centered triangular badge over thumbnail - new ImageGrid/GalleryItemVideoStarIconButton: video-typed star toggle - new ImageGrid/GalleryVideoItem: counterpart to GalleryImage; reuses galleryItemContainerSX, GalleryItemSizeBadge (width/height-only stand-in), selection handling (single/shift/ctrl/cmd); alt-click falls through to a normal select since comparison is image-only - use-gallery-image-names now calls the polymorphic gallery names endpoint and exposes a mixed flat name list (existing callers - paged grid, search, navigation hotkeys - get the same shape) - useRangeBasedImageFetching partitions visible names by extension; images bulk-fetch via the existing getImageDTOsByNames mutation, videos dispatch individual getVideoDTO queries (no batch endpoint yet) - GalleryImageGrid's ImageAtPosition dispatches on isVideoName to render GalleryImage or GalleryVideoItem; star hotkey dispatches to the right star/unstar mutation based on kind - pruned the now-unused useGetImageNamesQuery / isImageName exports Verified: pnpm lint (knip, dpdm, eslint, prettier, tsc) all green; pnpm test 1103/1103 pass; live curl of /api/v1/gallery/items returns 57 polymorphic items with video duration populated and image duration null, /api/v1/gallery/items/names returns matching {kind, name} refs. The useGalleryItemDTO hook is intentionally deferred to Phase 5 where the polymorphic viewer is its first real consumer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 5 - inline video player in the image viewer Selecting a video now renders a polymorphic preview inside the existing viewer panel: thumbnail with a centered play button by default; clicking play swaps in an HTML5 <video controls autoplay>. Switching to a different item drops the video element back to idle (auto-pauses) and selecting an image again returns to the normal image preview. New components (features/gallery/components/ImageViewer/): - VideoPlayButtonOverlay: large centered play button with hover/shadow, used over the thumbnail in the idle state. - CurrentVideoPreview: idle/playing state machine. Resets on video_name change. The <video> src points at /api/v1/videos/i/.../full which supports HTTP Range, so seek/scrub work natively in the browser. New hook: - common/hooks/useGalleryItemDTO: polymorphic DTO resolver that dispatches between useImageDTO and useVideoDTO based on filename extension (isVideoName). Centralizes the kind-dispatch the viewer and toolbar both need. Wiring: - ImageViewer dispatches on galleryItem.kind to render CurrentImagePreview or CurrentVideoPreview. The compare-image DnD drop target is hidden when a video is selected (comparison is image-only). - ImageViewerToolbar hides the image-specific action row (CurrentImageButtons - load workflow, recall metadata, edit, etc.) and the metadata viewer toggle when a video is selected. The general-purpose ToggleProgressButton stays. Out of scope (per the plan): video deletion from the viewer (use gallery hover icons), video-specific metadata viewer, comparison-mode support for videos. Verified: pnpm lint (knip, dpdm, eslint, prettier, tsc) all green; pnpm test 1103/1103 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): accept MP4 (and other video) drops on the fullscreen dropzone The gallery-wide drag-and-drop target lives in FullscreenDropzone, not in useImageUploadButton (which only powers the upload button). It had its own hardcoded image-only zod allowlist that rejected MP4 files with "File type / extension is not supported". - Broaden the zod refines to accept video/mp4, video/webm, video/quicktime, video/x-matroska and the matching extensions - Add isVideoFile helper, split dropped files into image/video sets, and route each set through its own uploader (uploadImages / uploadVideos). Both update their respective RTK caches and invalidate the polymorphic gallery list/names. - Skip the canvas-paste fast-path for single-video drops — the canvas doesn't host videos as layers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): right-click context menu on video items Adds a three-item context menu (delete, change board, download) on right-click / long-press of any gallery video item. Mirrors the image context menu's singleton-portal architecture so re-renders stay cheap. New files: - features/gallery/contexts/VideoDTOContext: small React context that scopes the active video DTO to the menu items (parallels ImageDTOContext). - features/gallery/components/ContextMenu/MenuItems/ ContextMenuItemDeleteVideo: window.confirm + deleteVideo mutation. Videos can't be referenced from canvas/nodes/refs, so the image modal's usage analysis is unnecessary; a one-step confirm matches the "minimal" scope. ContextMenuItemDownloadVideo: reuses the existing useDownloadItem hook against videoDTO.video_url / video_name. ContextMenuItemChangeBoardVideo: dispatches videosToChangeSelected and opens the (now polymorphic) ChangeBoardModal. - features/gallery/components/ContextMenu/VideoContextMenu: singleton pattern lifted from ImageContextMenu — registers gallery video elements via a Map; right-click looks up the target node and opens the menu at the cursor. Extended files: - features/changeBoardModal/store/slice: added video_names alongside image_names plus a videosToChangeSelected action. The two arrays are mutually exclusive — setting one clears the other. - features/changeBoardModal/components/ChangeBoardModal: now dispatches the matching video board mutations (add/removeVideoToBoard, plural endpoints don't exist yet so videos move one at a time — the menu acts on a single selection so this is a one-iteration loop). - features/gallery/components/ImageGrid/GalleryVideoItem: registers itself with useVideoContextMenu. - app/components/GlobalModalIsolator: mounts the singleton. Verified: pnpm lint (knip, dpdm, eslint, prettier, tsc) all green; pnpm test 1103/1103 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): Phase 6 - Wan 2.2 T2V/I2V workflow nodes Adds two new invocation nodes that produce MP4 videos from a Wan 2.2 A14B transformer + VAE, plus the supporting plumbing. New invocations: - WanVideoDenoise (wan_video_denoise) — multi-frame counterpart to WanDenoise. Same per-step logic (CFG, MoE expert swap at the boundary timestep, LoRA patching, scheduler dispatch) — reuses _ExpertSwapper, _resolve_variant, and the scheduler/LoRA helpers from wan_denoise. Difference: the noise tensor has a real temporal dim built from num_frames, and the I2V condition is built across all latent frames (frame 0 conditioned, rest zero). Defaults match the Wan 2.2 reference: 832x480 / 81 frames / 40 steps / CFG 5.0 (high) + 4.0 (low). Inpaint / img2img are out of scope for this first cut. TI2V-5B is rejected; T2V/I2V A14B only. - WanLatentsToVideo (wan_l2v) — VAE-decodes 5D latents to RGB frames via AutoencoderKLWan (T_pixel = (T_lat - 1) * 4 + 1), then encodes an MP4 with imageio[ffmpeg] (libx264, yuv420p for browser compatibility). The temp file is moved into outputs/videos/ via context.videos.save(). Backend shared pieces: - make_noise gains num_latent_frames (default 1, backward compatible). - Added num_latent_frames_for(num_frames, scale=4) helper. - New encode_reference_image_to_video_condition mirrors diffusers' WanImageToVideoPipeline.prepare_latents with last_image=None and expand_timesteps=False: pads the reference image with zero pixel-frames, VAE-encodes the full pseudo-video, normalises, and builds the 4-channel temporal-rearranged first-frame mask. Verified numerically: 21 latent frames for num_frames=81, first latent frame's 4 mask channels = 1, rest = 0. - The existing single-frame encoder is left untouched. Schema / context: - New VideoField primitive (parallel to ImageField) and VideoOutput invocation output (width/height/num_frames/fps/duration/video). - New VideosInterface on InvocationContext with .save(source_path, width, height, duration, fps, ...) returning VideoDTO. Mirrors ImagesInterface — falls back to WithBoard / WithMetadata mixins and embeds the queue item's workflow/graph as a JSON sidecar. - WanRefImageConditioningField now carries num_frames so the denoise nodes can sanity-check the I2V condition. WanRefImageEncoder bumps to v1.1.0 and gains num_frames=1 input (use 81+ for video I2V; the encoder dispatches between the single- and multi-frame helpers). - Image WanDenoise now rejects multi-frame conditions with a clear message pointing at WanVideoDenoise. Verified: pnpm lint (5/5) green; pnpm tests (multiuser auth 122/122 + broader suite via prior runs); numerical shape checks for noise and ref-image condition; end-to-end smoke via VideoService.create. A restart of the InvokeAI server is required to pick up the new invocations in the workflow editor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 T2V and I2V starter video workflows Two new default workflows for the workflow editor 'Browse' modal: - 'Text to Video - Wan 2.2' — model loader -> two text encoders -> wan_video_denoise -> wan_l2v. Exposes prompt, model picks, CFG (high + low), dimensions, frames, fps, and steps. - 'Image to Video - Wan 2.2' — same shape plus a wan_ref_image_encoder feeding the denoise node's ref_image input. Exposes the reference image and the frames field on the ref-image node (must match the denoise node's frames — there is a clear validation error if they diverge, but the starter has them in sync at 81). Both default to the Wan 2.2 reference settings: 832x480, 81 frames @ 16 FPS (~5 s), 40 steps, CFG 5.0 (high expert) + 4.0 (low expert), seeded by a rand_int. Pass the existing _sync_default_workflows validator (id starts with default_, meta.category=default). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): startup crash from stringified VideoOutput annotation run_app.py validates every invocation's return-type annotation against the output-class registry. wan_latents_to_video.py had a stray 'from __future__ import annotations' which made the `invoke()` return annotation a string ('VideoOutput') at runtime. The registry mismatch triggered the unregistered-output warning path, which itself crashed on output_annotation.__name__ because the annotation was a str: AttributeError: 'str' object has no attribute '__name__' The other Wan invocations don't use future annotations — drop the import to match. Verified post-fix: api_app import populates 95 output classes, wan_l2v annotation resolves to the real VideoOutput class and is in the registry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 Lightning T2V starter workflow Same graph as 'Text to Video - Wan 2.2' but with two Apply LoRA - Wan 2.2 nodes chained between the model loader and the denoise node, and defaults retuned for the Lightning distillation: 4 steps and CFG 1.0 on both experts (CFG=1 skips the negative-conditioning forward pass entirely, ~20x faster than the 40-step / CFG-5.0 baseline at similar quality). Adapted from a user-saved workflow; cleaned for distribution by stripping the install-specific model/LoRA key bindings (defaults should not bake in local UUIDs), bumping to a fresh default_-prefixed id with meta.category=default, exposing the two LoRA fields (lora + weight) so users can swap LoRAs without diving into the canvas, and flagging the negative-prompt node as unused at CFG=1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 Lightning T2V and I2V starter workflows Two new default workflows that wire the Lightning LoRA pair into the T2V and I2V video pipelines for a ~20x speedup: - 'Text to Video - Wan 2.2 Lightning' — model loader -> apply LoRA (high) -> apply LoRA (low) -> text encoders -> wan_video_denoise -> wan_l2v. Defaults to 4 steps and CFG 1.0 (no negative branch). Cleaned-up version of Lincoln's saved Lightning workflow: stripped per-install model/LoRA keys, switched meta.category to 'default' with a default_ id, and exposed both LoRA loaders' lora/weight/ target fields so users can swap LoRAs without diving into the canvas. - 'Image to Video - Wan 2.2 Lightning' — same chain plus a wan_ref_image_encoder (v1.1.0 with num_frames) feeding the denoise ref_image input. Defaults match the non-Lightning I2V starter (832x480, 81 frames @ 16 FPS) but with 4 steps / CFG 1.0. LoRA target defaults to 'auto' so properly-tagged Lightning LoRAs route themselves; both workflow descriptions tell users to set explicit 'high'/'low' targets if their LoRAs are untagged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): use FFMPEG plugin (not pyav) for MP4 encode wan_latents_to_video was passing plugin='pyav' to iio.imwrite, but the runtime only has imageio-ffmpeg installed (no PyAV). The encode step at the very end of generation crashed with: ImportError: The `pyav` plugin is not installed. Use `pip install imageio[pyav]` to install it Switch to plugin='FFMPEG' — backed by the bundled imageio-ffmpeg binary that pyproject already requires via imageio[ffmpeg]. libx264 yuv420p is the FFMPEG plugin's default for .mp4, so the explicit pixel_format is dropped (specifying it just produced a "Multiple -pix_fmt options" warning). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): log VAE decode and MP4 encode milestones in wan_l2v The video VAE decode + MP4 encode tail can take 30-90s on top of the denoise loop, and the toast-style signal_progress() messages don't land in the server log. Add context.logger.info() at: - VAE decode start: latent frame count -> pixel frame count + resolution - MP4 encode start: frames, fps, duration, dimensions - MP4 encode complete: encoded file size - Video saved: final video_name Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): switch video thumbnail/probe to imageio[ffmpeg] backend After wan_l2v wrote a successful libx264 MP4 to disk, the invocation would hang in DiskVideoFileStorage.save() during the cv2.VideoCapture thumbnail-extraction step. cv2 wheels on this build can't reliably decode our libx264/yuv420p output (most often the wheel was compiled without an h264 decoder, but the failure mode is silent hang rather than a clear error). The net effect: the MP4 ends up in outputs/videos but the queue item never completes, so the frontend spinner spins forever and the gallery doesn't pick up the new entry. Fix: rewrite extract_video_frame and probe_video to try imageio's FFMPEG plugin first (same backend that did the encoding — so reading our own output is guaranteed to work), with cv2 retained only as a fallback for uploaded videos in formats imageio can't decode. Also add fine-grained log lines + exception guards inside DiskVideoFileStorage.save() so a future thumbnail failure can no longer hang the whole save — it now logs a warning and continues, leaving the video record in place even if the thumbnail step errored. With logging at each step (video written, thumbnail written, sidecar written) any future hang will be obvious from the last log line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): handle VideoField outputs in invocation_complete After wan_l2v wrote its MP4 successfully, the gallery and viewer were never updated: the new video didn't appear and the viewer stayed stuck on the previous "Saving video" progress spinner indefinitely. Root cause: onInvocationComplete.tsx only inspected results for isImageField / isImageFieldCollection. VideoField outputs were silently dropped, so the polymorphic gallery list never invalidated and no auto-switch happened. The viewer therefore kept rendering CurrentImagePreview, whose ImageViewerContext-local $progressEvent / $progressImage atoms intentionally aren't cleared on queue completion when autoSwitch is on — they rely on the new image's DndImage onLoad to clear them, which never fires for a video. Fix: add isVideoField (mirrors isImageField against {video_name}) and plumb video outputs through onInvocationComplete: - getResultVideoDTOs pulls VideoDTOs via getVideoDTOSafe - addVideosToGallery invalidates GalleryItemNameList / GalleryItemList so the polymorphic gallery refetches and the new video shows up - auto-switch dispatches the video name into selection (selection is a polymorphic string[]; useGalleryItemDTO already discriminates by filename extension) The selection change swaps CurrentImagePreview for CurrentVideoPreview, which unmounts the stale progress overlay along with it — so the stuck spinner clears as a side-effect of the auto-switch. Also drops the now-stale @knipignore on getVideoDTOSafe, which has a real consumer now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add 'Frame from Video' invocation Extracts a single frame from a VideoField input and saves it as a regular ImageDTO via context.images.save, so it appears in the gallery like any other generated image. Primary use case is I2V "shot extension": take the last frame of a Wan-generated clip (default frame_index=-1) and feed it back as the reference image for the next clip, then stitch the MP4s to get videos longer than the model's single-shot frame budget at a given VRAM. Negative frame_index is resolved against the actual decoded frame count via probe_video() rather than passed through to imageio — not all imageio plugins handle index=-1 uniformly, and being explicit lets us emit a precise out-of-range error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add 'Concatenate Videos' invocation Joins two or more videos into a single MP4 with one of three transition modes between consecutive clips: - cut: hard splice, no blending. Total length = sum of inputs. - crossfade: linear A→B dissolve over transition_frames. Each boundary consumes N frames from both surrounding clips, shrinking total length by N per boundary. - fade_through_black: A fades to black, then B fades in. Each boundary consumes N/2 from each side and emits N output frames — total length is preserved. Implementation decodes via imageio's FFMPEG plugin (matching wan_l2v on the encode side) and runs the blends in numpy. All decoded frames are kept in memory at once; fine for the few-hundred-frame I2V chains that motivated this, would want streaming if anyone ever feeds in hour-long uploads. Up-front validation enforces matching dimensions across inputs and checks that each clip has enough frames to spare from its head and tail for the requested transitions — saves a wasted decode pass when the transition window is too wide for one of the clips. Pairs with 'Frame from Video' for I2V shot extension: generate N clips chained via last-frame-as-ref-image, then glue them with a crossfade. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): show full-resolution first frame in viewer The viewer used a chakra <Image src={thumbnail_url}> in the idle (not- playing) state, so once a clip auto-selected after generation the preview snapped from the full-resolution denoise progress image to the small WebP gallery thumbnail upscaled to fit — visibly soft compared to what the user was watching seconds earlier. Switch to a single <video> element that spans both states: - idle: muted, no controls, preload="metadata". With no `poster` attr the browser decodes and shows the video's actual first frame at full resolution (this is the documented HTMLVideoElement default). - playing: same DOM node with controls+audio toggled on, kicked off via ref.play(). No reload between states — the decoded buffer carries over. `key={videoName}` swaps the element cleanly when the user moves to a different clip, dropping any in-progress playback state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(nodes): show 'Save in gallery' on video-output nodes The footer checkbox was gated on useNodeHasImageOutput, which only matched ImageField outputs. wan_l2v and video_concat produce VideoField and so had no toggle — users had no UI path to flip is_intermediate on them, even though VideoOutput goes through context.videos.save and lands in the gallery the same way ImageOutput does. Rename the hook to useNodeHasGalleryOutput and extend it to match VideoField as well. Update the three call sites (the hook itself, the checkbox, and the footer wrapper) so the toggle and the footer render whenever a node produces something destined for the gallery. The image primitive ('image' type) is still excluded since it doesn't save a new image; no equivalent video primitive exists yet, so no analogous exclusion is needed for VideoField. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: remove unwanted planning documents * chore: fix ruff I001 import-order violations Auto-fix from `ruff check --select I001 --fix`. Touches 10 files across the Wan and videos changes where added imports landed out of order. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): restrict uploads to MP4 only The upload allowlist previously included .mov/.webm/.mkv, but the names service (create_video_name) unconditionally emits {uuid}.mp4 and we don't transcode on upload. The result: non-MP4 containers were stored under a .mp4 name and served with the .mp4 MIME type, which silently broke playback in browsers when the container didn't match. Drop the non-MP4 extensions from ACCEPTED_VIDEO_EXTENSIONS and tighten the accepted MIME prefix to "video/mp4". Wan-generated output is MP4 anyway, so this matches current reality. If we want to support more containers later, the right move is to extend the names service to preserve the source extension, then re-add the formats here. Also drops the now-dead suffix-detection block in upload_video and the os import it required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(videos): clean up stale @knipignore on consumed hooks useDeleteVideoMutation, useAddVideoToBoardMutation, and useRemoveVideoFromBoardMutation are now consumed by Phase 4 components (context menu, change-board modal) but were still annotated with the multi-phase @knipignore tag — that generated false-positive knip warnings and misrepresented the implementation status. Move those three into the unconditional export block. The remaining five hooks (useListVideosQuery, useGetVideoMetadataQuery, useGetVideoNamesQuery, useDeleteVideosMutation, useChangeVideoIsIntermediateMutation) are still unused in the current codebase and stay under a narrower @knipignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(videos): document invalidate/select race in addVideosToGallery The video gallery path uses tag invalidation rather than an optimistic insert (the image path's `insertImageIntoNamesResult` doesn't have a polymorphic equivalent yet). Because invalidation kicks off an async refetch, the `imageSelected` dispatch below it fires before the new video name is in `imageNames`, so the gallery grid's `useKeepSelectedImageInView` no-ops on its first pass. The scroll self-corrects on the next pass when the refetch lands and the `imageNames` dep updates. The user-visible effect is just a small lag on gallery scroll-to- selection — the viewer selection applies immediately — so this is a documented limitation rather than a bug. Worth a follow-up if the lag becomes noticeable in practice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add Video Primitive invocation and VideoField input UI Mirrors the Image Primitive flow end-to-end for videos. Users can now drag a video from the gallery onto a "Video Primitive" node and feed its output into downstream nodes like Frame from Video or Concatenate Videos — exactly the way Image Primitive feeds the rest of the image pipeline. Backend (invokeai/app/invocations/primitives.py): - New VideoInvocation, declared *after* VideoOutput so the return annotation is a real class (not a forward-ref string). Stringified output annotations crashed startup before — see cac366229a — so the ordering matters. Frontend: - Register VideoField as a stateful field type in types/field.ts: zVideoFieldType, zVideoFieldValue, zVideoFieldInputInstance/Template, output template + type guards, plus entries in the four stateful unions (FieldType, FieldValue, InputInstance, InputTemplate). - buildFieldInputTemplate / buildFieldInputInstance gain VideoField branches so OpenAPI-derived templates resolve correctly. - nodesSlice: fieldVideoValueChanged reducer + export. - imageActions/actions.ts: setNodeVideoFieldVideo helper. - dnd.ts: singleVideoDndSource + setNodeVideoFieldVideoDndTarget, wired into the dndTargets array. - GalleryVideoItem: register itself as a drag source so videos in the gallery actually drag (previously they were click-only). - VideoFieldInputComponent: parallel to ImageFieldInputComponent — shows the WebP thumbnail with a dimensions badge, accepts video DnD, drops stale references on reconnect if the underlying video was deleted. - InputFieldRenderer: dispatch VideoField templates to the new component (placed right after the ImageField branch). - useNodeHasGalleryOutput: also exclude the new `video` primitive type so the "Save in gallery" toggle does not render on the pass-through node (same treatment the `image` primitive already gets). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(video): allow video drops to reach DnD target handlers useDndMonitor is the global drop monitor that actually invokes each target's handler() — DndDropTarget only does enter/leave bookkeeping. Its canMonitor gate explicitly allowlists source types and only listed singleImageDndSource + multipleImageDndSource. So when a video was dragged from the gallery onto a VideoField input, the drop was visible to the DOM but the monitor silently filtered it out, the handler never ran, and fieldVideoValueChanged was never dispatched. Add singleVideoDndSource to the allowlist. Dropping a video onto a Video Primitive (or any other VideoField input) now wires the asset into the field as intended. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(backend): ruff * chore(frontend): typegen * feat(wan): split Wan 2.2 starter bundle into T2V and I2V Replaces the single ~63 GB Wan 2.2 bundle with two smaller bundles so users only pay for the capability they need. T2V (~36 GB) covers text-to-video plus a low-VRAM image-to-video option via TI2V-5B; I2V (~32 GB) adds the heavier I2V-A14B path. Drops the Q8 T2V pair from the default bundle — both Q8 variants and full Diffusers builds remain available as a-la-carte starters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): tighten multiuser isolation in list and board-move endpoints Three related fixes flagged in code review (PR #9163, JPPhoto): 1. Video and gallery list/name SQL paths only filtered by user_id when board_id was the literal "none" sentinel. When the URL parameter was omitted entirely, no user filter applied and non-admin callers could enumerate every user's videos / mixed gallery items. Added an explicit per-user isolation branch for the omitted case. 2. /v1/videos/ and /v1/videos/names accepted explicit board IDs with no read-access check; the route now mirrors the images and gallery routers and calls _assert_board_read_access for non-"none" values. 3. add_video_to_board and remove_video_from_board only validated video ownership, not destination/source board write access — a caller could move their video into someone else's private board. Added _assert_board_write_access and a strict _assert_video_direct_owner helper (no board-owner / public-board fallback) for board-move ops, mirroring _assert_image_direct_owner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(boards): cascade video deletion when deleting a board with media Previously delete_board only handled images. With include_images=true the backend would delete images on the board but the videos would silently cascade out of board_videos and survive as uncategorized records — almost certainly not what the caller intended. Without include_images, the same mismatch meant the response could not report affected videos. Now: - include_images=true also calls videos.delete_videos_on_board - include_images=false collects the soon-to-be-uncategorized video names - DeleteBoardResult gains deleted_board_videos and deleted_videos fields (default empty so existing clients are unaffected) Frontend deleteBoard / deleteBoardAndImages mutations gain the matching VideoList / VideoNameList / GalleryItem* tag invalidations so the polymorphic gallery and video list views refresh. Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): return affected_boards from board move/remove endpoints removeVideoFromBoard previously returned VideoDTO; the frontend then read result.board_id (null after removal) and only invalidated the 'none' board cache — the previous board's list stayed stale until refetch. addVideoToBoard had the same problem (the route never knew the source board, so the old-board cache was never invalidated). Mirror the image equivalents (board_images.py): the routes now return AddVideosToBoardResult / RemoveVideosFromBoardResult with the moved video name(s) and the full set of affected board IDs. Both old and new boards get invalidated atomically. Frontend mutations updated to consume the new shape via getTagsToInvalidateForBoardAffectingMutation on result.affected_boards. The auto-generated schema.ts will need a typegen pass after the dev server restart to pick up the new response types. Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): stream uploads, FileResponse for full video + thumbnail Three perf items from the code review (PR #9163, JPPhoto): - upload_video read the entire UploadFile into a Python bytes object before writing to the temp file. Multi-GB videos allocated multi-GB buffers. Now chunk-stream into the temp file with a 1 GB per-upload cap (HTTP 413 on overflow). Cap is intentionally generous — the goal is RAM-exhaustion protection, not content policy. - get_video_full read the whole MP4 into RAM when no Range header was present. Browsers usually send Range, but curl / direct downloads / CDN edge fetches do not, and a multi-GB load per such request is a trivial DoS vector. Replaced with FileResponse (sendfile). - get_video_thumbnail similarly buffered the WebP. Thumbnails are tiny so this was minor, but FileResponse is idiomatic and shaves the unnecessary copy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): localize video UI strings - Add gallery.deleteVideo_one / deleteVideo_other, deleteVideoConfirmation, and playVideo to en.json - ContextMenuItemDeleteVideo: drop the inline English defaultValue (the translation key now exists) and use gallery.deleteVideo for aria/tooltip (was reusing gallery.deleteImage so it rendered "Delete Image") - VideoPlayButtonOverlay: replace the hardcoded "Play video" aria with t('gallery.playVideo') Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(video-invocations): exact frame counts and odd-tf transitions video_frame_extract: resolving frame_index=-1 previously computed n_frames as round(duration * fps). For VFR uploads or containers with approximate metadata that can overshoot the actual decoded frame count, making the last-frame extraction fail. Use iio.improps(plugin='FFMPEG') for the exact decoder count when available; fall back to the duration * fps estimate only if the props query fails. video_concat fade_through_black: with an odd transition_frames the symmetric half = tf // 2 split emitted tf - 1 frames per boundary, violating the documented "emits transition_frames" contract. Split asymmetrically (tail_half = tf // 2, head_half = tf - tail_half) so the emitted count equals tf exactly for both even and odd values. Validation and docstring updated to match. Verified with manual cases: tf=1, tf=4, tf=5 all emit the documented total length for two 10-frame inputs. Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(gallery): describe video gallery items, upload, and deletion Update the Gallery Panel docs to reflect the polymorphic gallery added in the Wan 2.2 video feature branch: - Gallery intro now mentions images + videos coexist on boards. - Board deletion warning clarified to cover both kinds of media. - New "Videos in the Gallery" section covering: how video items appear (first-frame thumbnail + play badge), MP4-only upload constraint with the typical re-encode command, the video context menu, and that videos count toward board totals. Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(videos): regression coverage for PR #9163 review fixes Adds tests that pin the behaviour fixed in the JPPhoto review and would catch a recurrence: - tests/app/services/video_records/test_video_records_sqlite.py get_many / get_video_names: non-admin callers only see their own videos when board_id is omitted; admins see all; the "none" branch still filters by user. - tests/app/services/gallery/test_gallery_default.py Same multiuser isolation guarantee through the polymorphic gallery union for both images and videos. - tests/app/routers/test_videos_multiuser.py /v1/videos/ and /v1/videos/names: 403 when a non-owner passes an explicit private board_id; 200 for owners, admins, "none", and omitted board_id (the auth-required smoke tests pin the 401 paths too). - tests/app/routers/test_boards_multiuser.py Adds two delete-board cases proving the video cascade: include_images invokes delete_videos_on_board and reports deleted_videos; the no-include path reports deleted_board_videos without calling the destructive service. Existing fixture extended to stub the video services that the new route logic now touches. - tests/app/invocations/test_video_concat.py Parametric coverage that fade_through_black emits exactly tf frames for both even and odd tf, plus three-clip chains, plus the crossfade and cut/zero-tf cases as guards. - tests/app/invocations/test_video_frame_extract.py _decoder_frame_count returns the exact count via the cv2 fallback for several clip lengths and gracefully returns None for missing / non-video inputs (caller falls back to duration * fps). Bug found during test authoring: _decoder_frame_count over-flowed int() on iio's "inf" nframes for libx264 streams, and improps never returns a real count for that codec anyway. Helper now ignores non-finite shapes and falls back to cv2's CAP_PROP_FRAME_COUNT, which gives the exact value for libx264. schema.ts regenerated to pick up the AddVideosToBoardResult / RemoveVideosFromBoardResult / extended DeleteBoardResult types added in earlier commits in this series. All 70 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add 'Wan 2.2 I2V Ideal Dimensions' invocation Computes Wan I2V-compatible (width, height) for a source W×H at a target short-side resolution (e.g. 720 for "720p"), snapping each output to a multiple of 16 (Wan's transformer patch_size × VAE 8x pixel-grid constraint enforced by wan_ref_image_encoder). Replaces the 6-node math chain (Float Math × 4 + Float To Integer × 2) that was otherwise required to compute these dimensions from an arbitrary input image. Wire the Image Primitive's width/height outputs into this node, and feed its (width, height) outputs into both wan_ref_image_encoder and wan_denoise (they must match). Three rounding modes: - nearest (default): minimizes aspect-ratio drift - floor: guaranteed not to exceed unsnapped target (safer for VRAM) - ceiling: rounds up Output schema reuses IdealSizeOutput so it slots into existing pipes that already consume Ideal Size — SD1.5, SDXL. Includes regression tests covering the documented common-case table, all three rounding modes, postcondition invariants (multiple of 16, aspect ratio within 1.2%, never zero), and input validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): swap target_short_side int for 480p/720p/1080p preset dropdown Wan 2.2 was trained at 480p and 720p; a free integer encouraged users to pick noncanonical short sides that the model handles poorly. Replace the int field with a Literal dropdown of "480p" / "720p" / "1080p" (via ui_choice_labels) so the UI surfaces the canonical choices. 1080p is included with a label noting it's extrapolated from training (not a Wan native size) — useful for users with VRAM headroom but shouldn't be the default. Version bumped to 1.1.0 since the field schema changed (the node was only committed locally; no published workflow needs migrating). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): empty_cache around the I2V reference-image VAE encode Two-sided fix to avoid VRAM allocator fragmentation that was causing the subsequent denoise-transformer partial load to OOM: - Before vae.encode(): clears blocks left over from earlier nodes (the denoise expert swap especially leaves the cache fragmented). - After the condition tensor is on CPU: returns the VAE encode's intermediates so the next partial_load_to_vram sees a real free contiguous range. Mirrors the same pattern in wan_latents_to_image.py and wan_latents_to_video.py — those are the existing precedent. The cost is a handful of microseconds per encoder invocation and only the cache state is touched; model weights stay resident. Observed-by symptom from a workflow review: at encoder=480x720 and a source image of 880x1184, the encoder ran fine but the I2V high-noise expert failed to partial-load with a cryptic CUDA OOM at _load_state_dict_with_fast_device_conversion. Pre-resizing the source to 80% incidentally cleared the allocator state and let the run succeed; this fix removes the incidental dependency on source size. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): support TI2V-5B in the video denoise node (T2V mode) The video denoise node previously hard-errored on TI2V-5B with "not supported." Most of the surrounding machinery (variant-aware spatial scale, variant-aware scheduler, single-expert ExpertSwapper path) was already in place — the gate just needed lifting and the hard-coded A14B latent channel count needed to follow the variant. Changes: - Drop the upfront "TI2V-5B is not supported" raise. - Use get_default_latent_channels(variant) so latents are 48-channel for TI2V-5B and 16-channel for the A14B family (matches the image denoise node's existing logic). - For TI2V-5B with a Reference Image input, raise a sharper, accurate error that explains TI2V-5B's I2V uses diffusers' expand_timesteps path (first-frame-mask blend + per-position timestep gating) which this node does not implement yet — pointing the user at the working T2V path or the I2V-A14B model. - Update the transformer field description to reflect what's now supported. Image-to-video with TI2V-5B remains a follow-up; the conditioning math is genuinely different from A14B (no 36-channel concat) and warrants a separate code path rather than parameterising this one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): instantiate TI2V-5B VAE with the right architectural config The single-file Wan VAE loader was always calling ``AutoencoderKLWan(z_dim=config.latent_channels)`` and relying on diffusers' constructor defaults for every other parameter — but those defaults match the Wan 2.1 / A14B VAE (base_dim=96, in/out=3, 8x spatial, no patchify). For TI2V-5B's Wan 2.2-VAE the architecture is materially different: - base_dim=160, decoder_base_dim=256 - in_channels=12, out_channels=12 (3 RGB x 2x2 patch) - patch_size=2 - scale_factor_spatial=16 - is_residual=True - 48-vector latents_mean / latents_std (required for the model's encode/decode normalisation to produce non-garbage outputs) Loading the TI2V-5B VAE state_dict into the default-constructed model failed with shape mismatches throughout the encoder + decoder, surfaced in wan_l2v as "Error(s) in loading state_dict for AutoencoderKLWan." This commit routes z_dim=48 to a verbatim copy of the TI2V-5B VAE config (from vae/config.json in Wan-AI/Wan2.2-TI2V-5B-Diffusers); z_dim=16 keeps the previous A14B / Wan 2.1 default behaviour. Verified end-to-end: both kwargs construct cleanly and produce the expected layer shapes (decoder.conv_out emits 12 channels for TI2V-5B, 3 channels for A14B). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): variant-aware default scheduler for standalone installs When the main model has no on-disk ``scheduler/`` directory (every standalone GGUF / single-file install), ``_build_scheduler`` previously fell back to ``FlowMatchEulerDiscreteScheduler()`` for every variant. That's correct for the A14B family but wrong for TI2V-5B, which ships ``UniPCMultistepScheduler`` with ``flow_shift=5.0`` + ``prediction_type="flow_prediction"`` + ``use_flow_sigmas=True``. The mismatch produces drifty samples on TI2V-5B. Add a ``_default_scheduler_for_variant`` helper that reconstructs the right scheduler from the variant tag (values verbatim from each variant's ``scheduler/scheduler_config.json`` in the matching Wan-AI/Wan2.2-*-Diffusers repo). The on-disk-config-present path is unchanged — if the model ships a scheduler dir, that wins. Full scheduler-selection UI is deferred to a future PR per discussion; this special-case keeps the standalone TI2V-5B path producing the right sampler without surfacing a new field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): TI2V-5B image-to-video support TI2V-5B I2V uses a fundamentally different conditioning scheme from A14B I2V. Implement diffusers' ``expand_timesteps`` path so the same ``Reference Image - Wan 2.2`` node and ``Denoise Video - Wan 2.2`` node work for both variants, dispatched by VAE z_dim / transformer variant. Encoder side (wan_ref_image_extension.py / wan_ref_image_encoder.py) - Add ``encode_reference_image_to_ti2v_condition`` that VAE-encodes a single image frame to ``[1, 48, 1, H/16, W/16]`` with the Wan2.2-VAE normalisation, no mask channels. - ``WanRefImageEncoderInvocation`` dispatches on ``vae.config.z_dim``: z_dim=48 → TI2V-5B path, z_dim=16 → existing A14B path. - Enforce ``multiple_of=32`` for width/height in the TI2V-5B case (16x VAE * 2 transformer patch = pixel dims must divide by 32) with a clear error message pointing at the constraint. Denoise side (wan_video_denoise.py) - Replace the "TI2V-5B I2V not supported" raise with a variant-aware dispatch on ``ref_condition.shape`` and ``variant``. - For TI2V-5B I2V build a ``first_frame_mask`` once (0 at frame 0, 1 elsewhere). At each step: latent_model_input = (1 - mask) * condition + mask * latents temp_ts = (mask[0,0,:,::2,::2] * t).flatten() timestep = temp_ts.unsqueeze(0).expand(B, -1) Per-token timesteps gate the model: frame 0 sees t=0 (locked to condition), other frames see t (normal denoise). - After the denoise loop, re-clamp frame 0 to the clean condition so the locked first frame doesn't show scheduler drift in the final VAE decode. Mirrors WanImageToVideoPipeline:813-814. - Skip the encoder-num_frames-must-match check for TI2V-5B (its condition is always single-frame regardless of output length). Tests - Three new tests on encode_reference_image_to_ti2v_condition covering output shape at small and Wan-realistic dims plus the no-mask-channels invariant. Full video-denoise integration tests would need a new fixture stack (none exist for wan_video_denoise yet) — deferred. A14B I2V is unchanged. TI2V-5B T2V (added in the previous commit) is unchanged. Verified at the import + encoder-shape level; end-to-end verification requires a TI2V-5B I2V workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): show denoise progress overlay over the video viewer CurrentVideoPreview rendered only the <video> element, so when the last-selected gallery item was a video, a freshly-started render's denoise preview images had nowhere to display — the user saw the static first-frame still of the previously-loaded video until the new render's final video swapped in. Mirror CurrentImagePreview's progress-overlay pattern: subscribe to $progressImage / $progressEvent, gate on selectShouldShowProgressInViewer, and render a ProgressImage stack on top of the video when a render is in progress. Hide the play-button overlay while progress is showing so it doesn't sit on top of the preview. Reported by Lincoln during TI2V-5B testing: previews started working after restarting the server only because there was no video loaded at that point; once a video was selected, the previews silently dropped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(lint): apply ruff format + isort across recent Wan video work ruff check found one I001 (import order) in ``invokeai/backend/model_manager/load/model_loaders/vae.py`` and ruff format flagged five files. All cosmetic; no behaviour changes. - vae.py: import reorder - video_concat.py: minor reflow - test_wan_ideal_dimensions.py / test_boards_multiuser.py / test_videos_multiuser.py: prettier-style wrapping Verified: full ruff check + ruff format --check clean, 141 backend tests pass, and ``pnpm lint`` (knip + dpdm + eslint + prettier + tsc) all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(features): add user guide for Wan 2.2 video generation Comprehensive guide covering: - The three Wan 2.2 variants (T2V-A14B, I2V-A14B, TI2V-5B), their conditioning differences, and the dual-expert MoE explanation - Lightning LoRA distillation for 4-step A14B inference - Starter bundles (Text-to-Video and Image-to-Video splits) - Workflow setup for T2V and I2V with the constraint matrix: * frame count: (num_frames - 1) % 4 == 0 * pixel dims: multiple of 16 for A14B, 32 for TI2V-5B * encoder + denoise must agree on width/height - The chain-and-concat trick for making longer videos, with the bridge-frame degradation mitigations - Troubleshooting: OOM, late-frame artifacts, dim mismatches, VAE load errors, scheduler issues, preview-not-appearing, MP4 glitches Lands under Features → Video Generation (experimental). Astro auto-generates the sidebar from features/ so no nav config change needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(nodes): skip image-DTO fetch for videos in Current Image node CurrentImageNode unconditionally called useImageDTO(lastSelectedItem) even when the selected gallery item was a video, firing GET /api/v1/images/i/<uuid>.mp4 on every video thumbnail click. The endpoint 404s and the backend logged "Image record not found" each time — benign but noisy. Apply the same null-skip pattern useGalleryItemDTO uses: pass the name only when it's not a video, so RTK Query skips the request for video selections. Current Image is image-only by design, so videos rendering the empty fallback matches existing behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): clear stale progress overlay + force first-frame paint Two viewer bugs after auto-switching to a freshly-rendered video: - The denoise progress overlay never cleared. CurrentImagePreview clears the ImageViewerContext $progressImage/$progressEvent atoms via DndImage's onLoad callback; the video viewer had no analog, so the last progress still sat on top of the new video forever — clicking other video thumbnails did nothing visible, and only selecting an image (which fires onLoadImage via DndImage) cleared it. - Even with the overlay gone, the <video> element rendered its black background instead of the first frame. preload="metadata" loads dimensions/duration but doesn't guarantee a decoded first frame on all browsers; an explicit seek is needed to force a paint. Wire onLoadedMetadata to (1) call onLoadImage() — mirroring DndImage's onLoad — and (2) nudge currentTime to 0.0001 so the decoder paints the first frame without measurably advancing playback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(hotkeys): skip image-DTO fetch for videos in GlobalImageHotkeys Companion to a3bdc3304e (CurrentImageNode). GlobalImageHotkeys is a mounted-everywhere singleton that wires recall hotkeys (seed, prompts, remix, etc.) to whatever item is currently selected. It was passing the raw selection name through to useImageDTO unconditionally, so every video thumbnail click fired GET /api/v1/images/i/<uuid>.mp4 → 404 and the "Image record not found" log line. Gate on isVideoName(), mirroring the polymorphic null-skip pattern in useGalleryItemDTO. Recall hotkeys don't apply to videos anyway, so this just suppresses the noise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): empty CUDA cache between A14B expert swaps The dual-expert swapper releases the active expert via its context manager exit, but PyTorch's caching allocator retains the freed blocks as reserved-not-yet-claimable space until empty_cache runs. The next partial_load_to_vram for the incoming expert then sees a fragmented free pool and offloads layers it could otherwise have kept on device. Users running A14B observed the low-noise expert ending up far more CPU-resident than the high-noise one on otherwise identical settings — that was the leftover reservation from the high-noise expert masking real free VRAM. Call TorchDevice.empty_cache() between the release and the next load. Same pattern as the VAE-encode fix earlier in this branch. Regression test in test_wan_expert_swapper.py mocks empty_cache and asserts it fires on every actual swap but not on a same-label re-get. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): allow drag-and-drop to change a video's board Dropping a video thumbnail onto a board in the boards list was a no-op (the dnd target only accepted image sources). Extend addImageToBoardDndTarget and removeImageFromBoardDndTarget to also accept SingleVideoDndSourceData and dispatch the corresponding video mutations. Permission UX mirrors the image path: - Same canMoveFromSourceBoard gate (owner / public source board) - Same "do nothing if dropping on the current board" early-out Backend enforcement on /api/v1/videos/board already mirrors the image endpoints — _assert_board_write_access on the destination plus _assert_video_direct_owner on the video. The frontend gate intentionally mirrors only the source-board part of that, leaving the direct-owner check to surface as a 403 on attempt (same compromise as images, where the client doesn't have per-item owner info to gate cleanly). Multi-video drag is not supported yet (the gallery only registers a single-video draggable per item, no multi-select bundle), so this only wires the SingleVideoDndSourceData path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): force outgoing A14B expert off GPU on swap The previous empty_cache() fix (53b2f4d4c7) was insufficient. unlock() only decrements the cache record's lock counter — the weights stay on GPU until the cache's automatic offload decides to free them on the next lock(). That heuristic uses ``torch.cuda.memory_allocated() - working_mem`` to estimate free space, which under-frees when the previous denoise step's workspace activations are still allocated alongside the just-unlocked expert. The user-visible symptom was a log line like Loaded model '...:transformer' onto cuda device in 0.37s. Total model size: 9203.13MB, VRAM: 2381.18MB (25.9%) for the incoming low-noise expert, while the high-noise expert continued to hold ~9 GB of VRAM. The swapper now stashes the LoadedModel info handle and, on each swap, explicitly invokes ``cached_model.full_unload_from_vram()`` on the outgoing expert before locking the incoming one. This sidesteps the heuristic and guarantees the previous expert's weights leave GPU before partial_load_to_vram measures available room. The access path ``info._cache_record.cached_model`` reaches into a private attribute — there is no public LoadedModel API for "unload from VRAM but keep in RAM" today, and a broader backend refactor felt out of scope. The call is wrapped in getattr/try-except and pinned by a regression test so a future refactor breaks the test, not the swap. Tests: - Updated existing dual-expert lifecycle test to expect the new full-unload step in the swap log sequence. - New test_outgoing_expert_force_unloaded_from_vram covers the per-swap behavior (outgoing only, no initial unload). - New test_force_unload_failure_does_not_break_swap pins the defensive fallback so swap reliability survives a future LoadedModel refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gallery): restore shift/ctrl-click range selection in image grid GalleryImage's modifier-key click handler was reading the legacy imagesApi getImageNames cache to compute range-selection indices, but the gallery grid was switched to the polymorphic galleryApi getGalleryItemNames endpoint (the only source that includes videos). The legacy cache is no longer populated for the grid, so the ordered-name list came back empty and the handler fell into its "no names cached" early-return: if (imageNames.length === 0) { if (!shiftKey && !ctrlKey && !metaKey && !altKey) { dispatch(selectionChanged([imageName])); } return; } making shift- and ctrl-click no-ops. GalleryVideoItem already had the correct reader inlined as a private helper. Hoist it to a shared module (features/gallery/store/selectCachedGalleryItemNames) so both grids use the polymorphic cache, and update GalleryImage to call it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(typegen): regenerate schema.ts Refresh of the OpenAPI-derived TypeScript bindings against the current backend. No hand edits — this is the output of the typegen step re-run against the Wan video routes and recent backend changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(startup): silence HF tokenizers fork-after-parallelism warning Set TOKENIZERS_PARALLELISM=false at startup (via os.environ.setdefault so users can override) before any HF library is imported. The Rust ``tokenizers`` library warms a thread pool the first time a tokenizer runs — for us that's UMT5 / T5 text encoding during Wan / FLUX / SD3 conditioning. Every subsequent fork() then logs huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks... In video generation we fork on every MP4 encode (imageio's FFMPEG plugin uses subprocess.Popen → fork+exec), so this warning lands once per generation in the server log. The advisory is benign — the child correctly falls back to single-threaded tokenization before exec(), and the parent's thread pool is unaffected — but the noise obscures real warnings. Setting the env var before any HF import prevents the thread pool from warming up at all, so the fork detector stays quiet without sacrificing anything: tokenization happens once per generation and isn't a hot path for us. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(startup): hoist TOKENIZERS_PARALLELISM=false to module level Follow-up to 2106f10ec4 — the previous attempt set the env var inside ``run_app()``, which races against any transitive HF import triggered by the console-script's from invokeai.app.run_app import run_app If ``tokenizers`` is imported anywhere in that import chain (directly or via diffusers/transformers re-exports), the library's fork detector registers before our setdefault runs and the warning still fires. Move the setdefault to module level so it executes the instant ``run_app.py`` is loaded — i.e. before the function defs are even parsed, and well before any HF library has a chance to import. Note for testing: jurigged hot-reload only re-runs function bodies, so picking up this fix requires a full server restart, not just a file save under ``--dev-reload``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): replace window.confirm with ConfirmationAlertDialog Use the in-app delete-confirmation dialog (the same Chakra ConfirmationAlertDialog the image flow uses) instead of the browser's window.confirm() prompt. Matches the visual + interaction language of the rest of the gallery and picks up the shared ``shouldConfirmOnDelete`` system preference — flipping the "Don't ask me again" toggle now silences the prompt for both images and videos. Implementation mirrors features/deleteImageModal/ but trimmed: the image dialog computes "usage" (canvas layers, node fields, reference images, upscale source) so the user knows what they'll break. Videos have no analogous attachment points, so the video state machine is a straight confirm-then-delete with no usage analysis. - features/deleteVideoModal/store/state.ts — nanostores atom + an awaitable ``deleteVideosWithDialog`` that opens the dialog and resolves/rejects on confirm/cancel. Skips the dialog entirely when shouldConfirmOnDelete is off. - features/deleteVideoModal/components/DeleteVideoModal.tsx — ConfirmationAlertDialog with the new deleteVideoPermanent message and the shared "Don't ask me again" switch. - GlobalModalIsolator.tsx — mount the new modal alongside DeleteImageModal. - ContextMenuItemDeleteVideo.tsx — call useDeleteVideoModalApi().delete instead of window.confirm + useDeleteVideoMutation. - en.json — added gallery.deleteVideoPermanent, dropped the now-unused gallery.deleteVideoConfirmation. - videos.ts — useDeleteVideoMutation moves into the @knipignore export group since the only call site now uses videosApi.endpoints.deleteVideo.initiate via the modal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gallery): refetch polymorphic gallery cache on image completion The gallery grid subscribes to the polymorphic ``getGalleryItemNames`` RTK Query endpoint (so images and videos interleave by created_at). But ``onInvocationComplete``'s image path only did an optimistic insert into the image-only ``getImageNames`` cache, leaving the polymorphic cache stale — a freshly-generated image landed correctly in board totals and the per-DTO cache, but never showed up in the grid until the user reloaded the page. Mirror the videos path (which has invalidated these tags since the polymorphic endpoint was introduced) and dispatch ``galleryApi.util.invalidateTags(['GalleryItemNameList', 'GalleryItemList'])`` after image outputs are processed. The cost is one extra HTTP round-trip per generation; a future optimization could optimistically splice the new entry into the polymorphic shape, but that requires a different ``insertImageIntoNamesResult`` for the ``GetGalleryItemNamesResult`` shape and is a bigger change. Regression test in onInvocationComplete.test.ts pins the behavior: verifies the invalidation fires on a fake image complete event, and verifies it does NOT fire for denylisted passthrough node types (load_image, image). Confirmed test correctly fails when the fix is reverted via git stash. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: address 2nd-pass code review findings Self-review pass before re-pinging external reviewers. Five fixes; the three medium ones have user-visible consequences, the two low ones are guard + docstring. 1. videos.py: delete_video no longer swallows service errors into a misleading HTTP 200. Missing DTO -> 404, delete failure -> 500. The prior shape returned 200 with an empty deleted_videos list, which the frontend treated as success, dropped from cache, and left the video on disk — silent data-consistency failure visible only on next page reload. 2. videos.ts: starVideos / unstarVideos invalidate the LIST_TAG-scoped { type: 'VideoList' } entry alongside the per-video and board-affecting tags. Without this, starred_first=true gallery queries kept the just-starred video in its old position until the next list-affecting mutation. Mirrors the delete + upload pattern. 3. wan_denoise.py: _ExpertSwapper.get() stashes _active_device_ctx right after device_ctx.__enter__() succeeds, before attempting the LoRA patcher's __enter__. If the LoRA enter raises, _release() can now actually find the device context and exit it — previously the ctx was unreachable and 8-9 GB of GGUF expert weights stayed pinned to GPU until the model cache LRU evicted them. 4. wan_ideal_dimensions.py: reject sources whose longer side is below the 16-px Wan grid. The downstream max(w, 16) clamp would otherwise silently disconnect the output from the requested aspect ratio (returning 16×16 regardless of the source's actual shape). 6. wan_video_denoise.py: docstring now explains the deliberate absence of denoising_start / denoising_end / initial-latents inputs (video i2v uses reference-frame conditioning, not noise injection; the image denoise node still handles still-image img2img). Tests: - test_device_context_released_when_lora_enter_raises pins #3. - test_input_smaller_than_pixel_grid_rejected pins #4. - test_output_dims_never_zero renamed to test_smallest_valid_input_still_snaps_to_16_grid (now exercises 16×16 rather than 8×8 since the latter is now correctly rejected). All 58 affected backend tests pass, frontend lint clean. Audit note for the PR description (NOT a fix): delete_video's _assert_video_owner permits write access on public boards (mirroring the image router's _assert_image_owner — intentional symmetry). The stricter _assert_video_direct_owner is reserved for board-move ops. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(frontend): typegen * fix(gallery): multi-select context menu actions for videos The video gallery context menu only operated on the single right-clicked item, so selecting multiple videos and hitting the trash icon deleted just the first one. Adds a video-side multi-selection menu mirroring the image one for star/unstar/download/change-board/delete, switched in on selectionCount > 1. Each menu now filters the polymorphic selection to its own kind and labels the action with an explicit count + kind (e.g. "Delete 3 Videos", "Move 2 Images to Board"). The destructive items disable when the kind-filtered subset is empty, so a video-only selection greys out the image menu and vice versa. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(multiuser): address Pfannkuchensack PR #9163 review findings Finding 1 (Medium): delete_board cascade ignored per-video / per-image ownership, letting a board owner destroy other users' contributions to a public/shared board just by deleting the board with include_images=true. Adds user_id filtering through get_all_board_*_names_for_board and delete_*_on_board (base + sqlite + image wrapper). Non-admin requests pass the requester's id so the SQL WHERE clause narrows the cascade to that user's rows; admins still pass None for the unrestricted path. Other users' content cascades to "uncategorized" via the existing FK on board_videos / board_images. Finding 2 (Low, i18n): GalleryItemStarIconButton and GalleryItemVideoStarIconButton shipped raw English "Star"/"Unstar" tooltips. Both now use the gallery.starImage / starVideo translation keys. Finding 3 (Low): delete_videos_from_list and delete_images_from_list re-raised HTTPException mid-loop, throwing away the response payload for items already deleted before the foreign name was hit. The frontend cache never learned about those partial successes, so deleted records reappeared in the UI until the next manual refresh. Both routes now skip auth-failed items in-loop and return 200 with the partial-success list. Residual: adds a test that an upload with an .mp4 extension but non-decodable bytes (a) reaches probe_video, (b) surfaces 415, (c) unlinks the streamed-to-disk temp file so the server doesn't leak storage on garbage uploads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(openapi): regenerate openapi.json The committed schema was stale relative to the current server (missing the utilities/expand-prompt and utilities/image-to-prompt endpoints, the ModelRecordOrderBy / SQLiteDirection list params, and the Wan / QwenImage / QwenVLEncoder config variants this branch adds). Regenerated via the same command the new openapi-checks workflow uses so the diff CI is empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(workflows): add "Image to Video - Add Frames" starter workflow Extends an existing video by extracting its penultimate frame, running it through Wan 2.2 I2V A14B + the Lightning LoRA pair to generate a new clip, and concatenating the result onto the source with a short crossfade. Cleaned per the default-workflows README: stripped value references on the four model loader fields and both Lightning LoRA fields so the workflow ships without keys/hashes for user-installed resources, gave the LoRA nodes "Apply LoRA (High)" / "(Low)" labels matching the existing Lightning default, remapped six stale exposedFields entries that pointed to template LoRA IDs no longer present in the graph, and synced the wan_video_denoise num_frames default to the value driven by the connected integer node. Tagged with both Text to Video and Image to Video so it surfaces under either filter in the Workflow Library. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gallery): video viewer polish and selection regressions - Restore auto-select-on-startup and on-board-switch: the polymorphic getGalleryItemNames endpoint replaced getImageNames as the grid's source of truth, so appStarted and boardIdSelected now wait on / read that cache instead of timing out forever. - Delete-then-select: video delete used to clear selection to null; image delete read a cache that's no longer warmed. Both now snapshot the gallery list before deletion and advance to the adjacent surviving item (prev > next > null) via a shared pickSelectionAfterDelete helper. - Video Viewer: right-aligned action bar with Open in new tab, Copy frame, Download, Delete, and a labelled Close video player button that only appears while playback is active. Copy uses canvas + ClipboardItem since video MIME types aren't supported cross-browser. - Next/prev arrows + galleryNav hotkeys now work when a video is in the Viewer (previously image-only). - Video context menu uses full-width text MenuItems instead of the cramped icon group, and gains an Open in new tab entry. * fix(gallery): bulk video drag-to-board and shift-click range selection - Bulk video drag: introduced multipleVideoDndSource so a multi-selection dragged from a video thumbnail moves every selected video, not just the first. The whitelist in useDndMonitor.ts also needed updating — without it the monitor's canMonitor gate silently dropped the new source type. - Mixed selections: both the multi-image and multi-video drag payloads now carry image_names + video_names side-by-side, so dragging from either kind in a mixed selection dispatches addImagesToBoard + addVideosToBoard together. Previously the image side leaked video names into image_names and the image router 404'd on each one. - Bulk video helpers: added addVideosToBoard / removeVideosFromBoard that fan out over the existing singular video router endpoint (no batch endpoint exists yet) — mirrors the change-board modal's existing loop. - Shift-click range selection: selectCachedGalleryItemNames now looks up the cache entry matching the gallery's current query args instead of taking the first entry from selectInvalidatedBy. RTK Query keeps unused entries warm for 60s after a board switch, and the old "first wins" behavior frequently landed on a stale board's name list, making shift-click silently no-op until a delete/move forced a refetch. * fix(scripts): force generate_openapi_schema.py to resolve invokeai from the repo root When the script was invoked as ``python scripts/generate_openapi_schema.py``, Python placed the script's directory at ``sys.path[0]`` rather than the repo root. ``import invokeai`` then resolved via the venv's site-packages, which on multi-worktree editable installs ends up importing ``invokeai`` as a PEP 420 namespace package that aggregates every worktree's ``invokeai/`` directory. Side-effect imports driven by submodule discovery silently miss whichever worktree isn't first on the namespace path, so the registry came up short by the invocations declared only in this worktree (the wan/video set, 15 classes). Running the same imports via ``python -c`` worked because ``sys.path[0]`` defaulted to the cwd and ``invokeai/__init__.py`` resolved cleanly to the worktree. Prepend the resolved repo root to ``sys.path`` before importing ``invokeai.*`` so the script always picks up the local sources regardless of how it was launched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add Frame Range from Video invocation with scrubbable preview New ``extract_video_range`` node trims a source video to a contiguous frame range and re-encodes it as MP4, slotting in naturally between a generated clip and Concatenate Videos for I2V chain shaping. Bounds are inclusive and support negative indices (``end_frame=-1`` keeps the final frame), matching Frame from Video. Output fps inherits from the input unless overridden. The node renders a per-type preview inside the workflow editor: two ``<video>`` tiles side by side, each driven by a CompositeSlider that scrubs the corresponding integer field. The tile uses ``currentTime = frame / fps`` so browsers display the seeked frame natively without a canvas roundtrip. Negative-index entries in the standard integer input are resolved against the source frame count for display only; the underlying field value is preserved verbatim. The custom UI is wired in via a ``CustomNodeBody`` dispatcher in ``InvocationNode.tsx`` rather than a registry — small enough to be explicit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): emit resolved frame indices and move preview to per-field renderer Three changes to the ``extract_video_range`` invocation: 1. New ``ExtractVideoRangeOutput`` mirrors ``VideoOutput`` and additionally emits the resolved (positive, 0-based) ``start_frame`` and ``end_frame`` indices. Chained workflows can feed those back into a downstream Frame from Video to extract the same boundary frame the trim landed on. 2. ``fps`` is now a plain ``int`` defaulting to 16 (was ``Optional[int]`` with an "inherit from input" fallback). Matches the default used by wan_l2v and the other Wan video producers, so chained workflows agree on framerate without each node guessing. 3. The frame preview is now a per-field widget driven by a new ``UIComponent.VideoFrameIndex`` hint. ``start_frame`` and ``end_frame`` are tagged with it; the new ``VideoFrameIndexFieldInput`` renders a number input plus a live <video> thumbnail and a scrubber slider, all writing to the same Redux field. Negative indices entered in the number input are still resolved against the source frame count for display only — the backend re-resolves at invoke time. The widget reads its companion ``VideoField`` (by convention, the sibling field named ``video`` on the same node) via direct Redux selectors, so it works wherever ``InputFieldRenderer`` is used — the workflow editor's node body AND the Form Builder's view/edit modes. The previous node-body ``ExtractVideoRangePreview`` and its ``CustomNodeBody`` dispatcher in ``InvocationNode.tsx`` are removed; the per-field widget supersedes both. In the workflow editor, side-by-side framing is lost in exchange for Form Builder support; users wanting the side-by-side layout in a form can group the two frame fields in a row container. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: ruff * fix(video): address PR #9163 review follow-ups - delete_board: include_images query description and OpenAPI schema now mention videos alongside images - get_video_thumbnail: check path existence before returning FileResponse so a missing thumbnail produces the documented 404 instead of an after-route error - delete_videos_on_board: stop deleting records for videos whose files failed to delete, so a transient FS error no longer orphans the file with no record pointing at it - DeleteBoardModal: destructive button and warning copy now mention videos * fix(video): address PR #9163 May-22 review and failing CI - remove_video_from_board now accepts either the direct video owner or a board write-access holder, so videos uploaded to a board that later flipped Public -> Shared/Private aren't stranded. - VideoService.create rolls back the DB record and board association if the underlying file save fails, preventing ghost records whose file endpoints 404. - delete_videos_on_board returns the actually-deleted names; delete_board uses that list so the response can't claim a video was destroyed when its record was preserved due to a file-delete failure. - Local test_videos_multiuser fixture now patches invokeai.app.api.routers._access so list/names route 403 checks work. - Regenerate schema.ts to pick up the CacheStats description. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(frontend): rebuild openapi * fix(video): register Viewer <video> as drag source Drag-and-drop from the Viewer pane now produces the same singleVideoDndSource (and multipleVideoDndSource for active multi-selection) as the gallery thumbnail, so a video can be dropped onto a Video Primitive's "Starting Video" field directly from the Viewer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add frame preview to Frame from Video node Tag frame_index with ui_component=VideoFrameIndex so the node renders the same live frame thumbnail + scrubber as Frame Range from Video. The widget keys off the sibling 'video' field, which this node already has, so no frontend changes are needed. Bump node version 1.0.0 -> 1.1.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(video): derive Frame Range fps from source video by default Make the fps field optional (default None). When unset, the output frame rate is inherited from the probed source video so a trimmed clip plays back at the same speed as its source, falling back to 16 fps when the source rate can't be probed. An explicit fps still overrides. Bump node version 1.0.0 -> 1.1.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video): use fps=0 sentinel for source-derived Frame Range rate The previous Optional[int]/None design had no natural way to express 'unset' in the node's number input, and the ge=1 constraint rejected the intuitive fps=0 with a validation error. Make fps a plain int defaulting to 0, allow ge=0, and treat 0 as 'match the source video's frame rate'. Keeps in-progress workflows (already saved with fps=0) working without a version bump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 TI2V Ideal Dimensions node TI2V-5B uses the 16x Wan 2.2-VAE plus a 2x transformer patch, so pixel dims must be multiples of 32 (the existing I2V node snaps to 16, which the TI2V-5B patchify step rejects). Add a wan_ti2v_ideal_dimensions node that snaps to 32. Factor the shared scale-and-snap math into _scale_and_snap(multiple=...) so both nodes derive from one implementation; the I2V node is unchanged behaviorally (its existing tests still pass). Add a mirrored TI2V test suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(wan): add A14B/5B model hints to ideal-dimensions node titles Suffix the node titles with the target model family (A14B / 5B) so they're distinguishable in the add-node search and node header, and rewrite both docstrings to lead with which Wan 2.2 model they're for and cross-reference the other node. Purely UI metadata — no behavior or schema change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(workflows): replace bundled Wan 2.2 video workflows with curated set Remove the 6 previously-bundled Wan 2.2 *video* default workflows (Text to Video, Text to Video Lightning x2, Image to Video, Image to Video Lightning, Image to Video - Add Frames) and replace them with the 8 curated starter workflows: Text/Image to Video Lightning (+ Concept LoRA variants), Extend Video Lightning (+ Concept LoRA variant), and the TI2V-5B text/image-to-video low-quality variants. Each is assigned a stable default_ id and meta.category=default. Model fields are intentionally blanked (per-install keys don't resolve cross-instance) with the required models listed in each workflow's Notes. The two Wan 2.2 *image* workflows (Image to Image, Text to Image) are retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(video): add beginner Video Workflows guide for the 8 starter workflows New Features-section page (sibling to Video Generation) describing the eight bundled Wan 2.2 video workflows in plain language: how to choose between the Text/Image/Extend families and their Lightning / Concept-LoRA / TI2V-5B variants, how to select models from each workflow's Notes, how to run one, and a quick per-GPU guide. Cross-linked both ways with the Video Generation technical reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(video): fix Concept LoRA slot guidance (slots are required) The w/ Concept LoRAs workflows wire required lora fields (lora_selector / wan_lora_loader, no default) into the graph, so an empty slot blocks invocation. Correct the earlier claim that empty concept slots behave like the base workflow: every LoRA slot must be filled, and users without concept LoRAs should use the plain variant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(video): drop '(experimental)' from Video Generation title Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: refresh uv.lock Routine lock refresh (transitive dev deps: docutils, idna, platformdirs, python_discovery, tornado). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(wan): add first-last-frame interpolation (FLF2V) to I2V-A14B The Reference Image - Wan 2.2 node gains an optional End Image input: when set, encode_reference_image_to_video_condition places the end image in the final temporal slot and anchors the mask at both the first and last latent frames, so I2V-A14B interpolates a clip from the start image to the end image. Mirrors diffusers WanImageToVideoPipeline.prepare_latents with last_image set. The denoise loop is unchanged - for A14B it just concatenates the 20-channel condition, which is agnostic to one vs two anchors. FLF2V is A14B video only (num_frames > 1); the encoder raises a clear error for TI2V-5B or single-frame. Bump wan_ref_image_encoder to 1.2.0; add mask-anchoring unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(workflows): add 'Interpolate 2 Images to Video' starter + FLF2V docs Ship a default workflow that wires the new FLF2V End Image input end to end (I2V-A14B + Lightning, two image inputs interpolated). Model fields blanked with the required models listed in Notes, default_ id + category=default. Document FLF2V in the Video Generation reference and add the workflow to the Video Workflows guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(workflows): add Text/Image/Video to Video library filters + fix video tags Add 'Text to Video', 'Image to Video', and 'Video to Video' to the Common Tasks filter list in the Workflow Library browser. Fix the tags on the nine bundled Wan 2.2 video workflows, which were all copy-pasted as 'text to video': - Text to Video: the three T2V workflows - Image to Video: the I2V workflows + Interpolate (two-image) - Video to Video: the two Extend Video workflows The TI2V-5B variants also drop the spurious lightning/lora tags (they have no LoRAs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(workflows): add 'Extend Video to Image' FLF2V starter + docs Ship a default workflow that extends a video toward a user-provided target image: the new segment interpolates (FLF2V) from the source video's last frame to the destination image, then concatenates onto the original with a cross-fade. Model fields blanked, default_ id + category=default, tagged 'video to video'. Document it (card + usage instructions) in the Video Workflows guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(video): reorganize Video Workflows guide sections Group the Interpolate section and the Concept-LoRA / TI2V-5B asides with the image workflows, keep the Extend family (including Extend Video to Image) at the end, and retitle the section to 'Bundled video workflows'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(frontend): regenerate openapi and typegen * chore(backend): ruff * fix(future): make the WAN LoRA loader compatible with LoRA picker node PR #9259 * chore(frontend): remove unused selectT5EncoderModels import * chore(frontend): remove unused export * fix(gallery): count videos and pick video covers for board tiles Gallery boards previously joined only the `images` table for their headline count and cover thumbnail, so a board containing nothing but videos rendered as empty with no preview. BoardDTO now exposes `video_count` and an optional `cover_video_name`; the boards service picks the best cover across both tables using the same (starred DESC, created_at DESC) tie-break the image path already used, and the gallery list renders `image_count + video_count` everywhere it previously rendered just images (real boards, no-board pseudo-board, and the tooltip). Adds `getBoardVideosTotal` to round out the no-board counts (the BoardVideosTotal tag was already wired into invalidation). * test(boards): wire video record storage into multiuser test fixtures After the board cover/count fix started reading from `video_records` and `board_video_records`, the multiuser test fixtures that still set both to `None` started erroring out — the boards router's catch-all turned the AttributeError into a 404, cascading through every test that PATCHes or GETs a board (auth, workflows, data-isolation suites). Swap the `None` placeholders for real SqliteVideoRecordStorage / SqliteBoardVideoRecordStorage instances (paralleling the existing image storage setup), and pin sane defaults on the MagicMocks in `test_videos_multiuser.py` so the get_dto cover/count lookups don't trip Pydantic validation. * fix(ui): widen useVideoContextMenu ref type to allow null The ref param was typed RefObject<HTMLElement>, but useRef produces RefObject<HTMLElement | null>, breaking lint:tsc in GalleryVideoItem. Match the sibling useImageContextMenu signature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(backend): ruff * fix(tests): pass video/gallery services to InvocationServices in workflow-call router tests The workflow-call router tests from main construct InvocationServices directly and predate the video/gallery services added on this branch, so every test in the file errored with missing positional arguments. Mirror tests/conftest.py: real sqlite stores for video_records and board_video_records, None for the services the tests never touch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): refresh board caches when a generated video completes Video completion previously invalidated only the polymorphic gallery list tags, so the new video appeared in the grid while the board's video_count, cover thumbnail (Board tag / listAllBoards), and BoardVideosTotal stayed stale until an unrelated mutation refetched them. Use the shared getTagsToInvalidateForBoardAffectingMutation helper over the affected boards, matching the video mutation endpoints. Reported by @JPPhoto in PR #9163 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: include videos in date-based virtual boards Date virtual boards were image-only even though the gallery grid is now polymorphic: video-only dates never appeared, and mixed dates omitted videos from counts/contents/covers. - SqliteGalleryService owns virtual-board dates now: get_dates() unions images+videos per date (video_count added to VirtualSubBoardDTO, cover is the newest item of either kind via cover_image_name/cover_video_name), and list_item_names() gained a created_date filter. - New GET /api/v1/virtual_boards/by_date/{date}/item_names returns the same polymorphic (kind, name) refs as the gallery names endpoint; the legacy image_names route is kept for API compatibility. - Frontend virtual-board selection consumes the new endpoint, so videos show up in virtual date boards; VirtualBoardItem shows video counts (localized tooltip) and falls back to the video thumbnail for video covers. - tests/conftest.py wires a real SqliteGalleryService so router tests exercise the filter SQL; service + router tests cover video-only dates, mixed dates, cover selection, and per-user isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): don't advance gallery selection for videos whose delete failed handleDeletions treated every requested video as deleted when picking the post-delete selection, so a 403/500 on deleteVideo could jump the Viewer away from a video that still exists, and a surviving neighbour was skipped as a replacement candidate. Only successfully deleted names now count: a failed displayed video keeps its selection, and failed neighbours remain valid replacements. Covered by state.test.ts with rejected deleteVideo dispatches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): count uncategorized videos when deciding the gallery has content useHasImages only looked at boards and the uncategorized *image* total, so a gallery whose only content was an uncategorized video rendered the new-user/get-started view instead of the normal no-selection state. The hook now also reads the uncategorized video total (getBoardVideosTotal('none')); the decision logic is extracted as getHasGalleryContent and unit-tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): stop labeling board video counts as images in tooltips Board tooltips folded video_count into image_count and rendered boards.imagesWithCount, so a video-only board read e.g. '1 image, 0 assets'. Tooltips now show split image/video/asset counts using the new boards.videosWithCount translation; the compact unlabeled headline count in the boards list stays combined so video-only boards don't read as empty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): restrict client-side video upload acceptance to MP4 only The dropzone accept map advertised .webm/.mov and isVideoFile treated .webm/.mov/.mkv as videos, but the upload router accepts MP4 only, so those files were accepted client-side and then rejected with 415 after the bytes were uploaded. Consolidate the accepted-media lists into common/util/uploadMediaAccept.ts (single source of truth shared by useImageUploadButton and FullscreenDropzone) and pin them to the backend contract with a regression test. Also split the accept map: image-only upload fields (board covers, style presets, model images, workflow thumbnails) no longer advertise video/mp4, which they had inherited when video entries were added to the shared map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): bulk video star/unstar returns partial successes instead of 403 mid-batch star_videos_in_list and unstar_videos_in_list re-raised the ownership HTTPException mid-loop, so a batch containing one foreign (or stale) name mutated the earlier owned videos and then returned 403 with no payload — the client never invalidated caches for the videos that did change. Skip unauthorized names and return 200 with the actually starred/unstarred videos, mirroring delete_videos_from_list. Router tests cover the mixed-ownership batch for both routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): localize virtual board section header and toggle The 'By Date' header and the Collapse/Expand aria-label in VirtualBoardSection were hardcoded English. Add boards.byDate and common.collapse/common.expand translation keys and use them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): failed video saves no longer orphan files on disk DiskVideoFileStorage.save() moves the source MP4 into permanent storage before writing the thumbnail and sidecar, so a failure in either later step used to leave the moved MP4 (and partial artifacts) on disk with no DB record through which they could be managed. save() now removes its destination files before raising, and VideoService.create()'s rollback also deletes files to cover failures after a successful file save (e.g. building the DTO). Also documents why board attachment during create is best-effort (mirroring ImageService.create: a board deleted mid-generation must not destroy the render) and pins the explicit fallback — DTO reports the actual missing board association and a warning is logged — with a service test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(app): document videos.user_id lifecycle and pin user-deletion behavior videos.user_id deliberately has no FK to users, matching images/boards/ workflows (migration_27 adds those user_id columns index-only): deleting a user leaves their media in place for admin review/cleanup rather than cascading a row delete that would strand files on disk. A migration comment now states the parallel, and a migration-backed test creates a user and a video, deletes the user, and asserts the record survives, stays attributed to the deleted owner, and remains visible only to admins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(app): support VideoField values in session queue batch data Batch.data previously allowed ImageField but not VideoField, so submitting multiple VideoField values through the generic batching capability failed Pydantic validation before enqueueing. VideoField now joins the BatchScalarDataType union; a test asserts a VideoField batch validates and expands into separate sessions. schema.ts/openapi.json regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): video uploads are opt-in per consumer; upload validation fixes - useImageUploadButton gains an allowVideos opt-in (default off). Only the gallery uploader accepts videos; image-only consumers (ref images, board covers, launchpad buttons, image-to-prompt, etc.) no longer let a selected MP4 upload into the gallery while the requested image action goes nowhere. Videos are excluded from their accept map and rejected at runtime if the file dialog bypasses it, with tests via partitionUploadFiles. - The hook's loading state now covers both the image and video mutations, so an in-flight MP4 upload shows a loading button and blocks resubmission. - The fullscreen drag-drop/paste validator accepts a file when either its MIME type or its extension is recognized — a clip.mp4 with an empty File.type used to be rejected even though the backend accepts it. The validator moved to a pure module with tests. - Failed video uploads no longer toast "Image Upload Failed": video-only batches use a new toast.videoUploadFailed key, mixed batches the neutral toast.uploadFailed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): bound untrusted video decoding with a killable subprocess timeout probe_video / extract_video_frame / decoder_frame_count now run in a short-lived child process (video_decode_worker.py) killed after a hard timeout. Previously a crafted MP4 that failed the imageio probe and then hung inside cv2.VideoCapture()/read() would pin the FastAPI request worker that called it forever; repeated uploads could exhaust the pool. The worker is run by file path (not -m) and imports only imageio/PIL/cv2 so it starts without pulling in the invokeai package or torch. Tests substitute a never-returning worker command and assert the helpers fail within a bounded interval, plus happy-path tests against a real synthetic MP4 to validate the subprocess plumbing end to end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): stream frames through the video concat/trim nodes video_concat and extract_video_range fully decoded their inputs into lists of uncompressed frames before encoding; with the 1 GB upload cap a long 1080p source can expand to tens of gigabytes of RAM, so any user able to enqueue these nodes could exhaust server memory. Frames now stream from the decoder straight into an incremental FFMPEG writer. The concat node buffers only the transition windows (bounded by transition_frames), and the range node holds one frame at a time and stops decoding at the end of the requested range. Tests use lazy frame iterators to pin that encoding begins before the inputs are exhausted and that look-ahead stays bounded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): video upload feedback parity with images - Add uploadVideo matchFulfilled/matchRejected listeners mirroring the image upload listeners: success toasts name the destination board and navigate the gallery on the first upload of a batch; failure toasts name the failed file, which is what makes partially failed Promise.allSettled batches attributable (uploadVideos and the fullscreen dropzone aggregate without rethrowing, same as images). - GalleryUploadButton now uses the hook's combined isUploading, so an in-flight MP4 shows a spinner and blocks resubmission. - Media-neutral labels on the two video-enabled surfaces: gallery uploader aria/tooltip says Upload Media, the fullscreen overlay says uploaded items (not images) will be added, and its invalid-file toast mentions MP4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: include videos in the user-deletion data-loss note The admin guide's user-deletion warning enumerated boards, images, workflows, queue items, and style presets but not videos. State that video records survive with the deleted user_id, that files remain under outputs/videos, and that administrators keep gallery visibility of the orphaned media for review/cleanup — matching the behavior pinned by the video_records user-deletion lifecycle test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix video processing resource bounds * fix video lifecycle edge cases * stabilize decoder inactivity test * chore(deps): declare psutil as a direct dependency video_thumbnails.py now imports psutil for decode-worker process-tree termination, but it was only present transitively (via accelerate and friends). Declare it so the import can't silently break when an upstream package drops it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix video security and lifecycle regressions * chore: regenerate OpenAPI schema * fix: preserve exact frame dimensions in video encoders imageio's FFMPEG writer defaults to macro_block_size=16, which makes ffmpeg silently rescale frames to the next multiple of 16 — a 1920x1080 upload trimmed by Frame Range from Video came back as 1920x1088 while the DTO recorded 1080, so concatenating the trim with its own source failed the same-dimensions check. - New invokeai/app/util/video_encoding.make_mp4_writer single-sources the encoder settings (libx264, macro_block_size=1) for wan_latents_to_video, video_concat, and video_frame_extract_range. - yuv420p requires even dimensions, so concat and extract-range now reject odd-dimension sources up front with a clear error instead of an opaque ffmpeg failure mid-encode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: correct A14B fallback scheduler and stop LoRA leakage to low-noise expert Two silent-wrong-output bugs on the GGUF A14B path: - The no-scheduler-dir fallback returned FlowMatchEulerDiscreteScheduler for A14B, but both Wan-AI/Wan2.2-{T2V,I2V}-A14B-Diffusers repos ship UniPCMultistepScheduler with flow_shift=3.0 / flow_prediction / use_flow_sigmas (verified against the upstream scheduler_config.json). Every A14B GGUF render ran an unshifted first-order Euler schedule, degrading output and skewing how many steps land above the MoE boundary. An unreadable on-disk config now also falls back to the variant default instead of bare FlowMatchEuler. - low_loras fell back to the primary list when loras_low_noise was empty, but the Wan LoRA loader deliberately routes expert-tagged LoRAs to exactly one list — so a high-noise-only LoRA (e.g. a Lightning high-noise distill) was silently applied to the low-noise expert too, and high-only targeting was impossible. An empty low list now means no LoRAs on the low expert. Also (here and in the previous commit): the Wan VAE decode nodes now raise a clear latent-channel mismatch error (16-channel A14B latents vs 48-channel TI2V-5B VAE and vice versa) instead of an opaque tensor-size RuntimeError when the wrong VAE is selected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: keep viewer selection on surviving item after image deletion The image-side handleDeletions cleared the gallery selection (imageSelected(null)) whenever the deletion intersected the multi-selection but the displayed item was not among the deleted names — e.g. a video displayed while only images were deleted from a mixed selection, or a hover-delete of a non-displayed selected image. It also treated every requested name as deleted, ignoring the server's deleted_images response, so a partial failure could jump the selection away from an image that still exists. Port the deleteVideoModal logic: only server-confirmed deletions count, a surviving displayed item stays selected, and the usage-reset sweep (nodes/canvas/ref-image layers) runs only for actually-deleted images. Regression tests mirror deleteVideoModal/store/state.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf: ~48x faster Wan VAE decode on ROCm via conv2d decomposition MIOpen has no implicit-GEMM 3D-convolution kernels for the Wan VAE's shapes on RDNA3 and falls back to Im3d2Col (61% of decode GPU time in a torch profile). An 81-frame 832x480 decode took 730s on a W7900 vs 13s on an RTX 5060; dtype changes and cudnn.benchmark kernel search were all within +/-7%. A stride-1 kTxkHxkW conv3d is exactly the sum of kT conv2d taps over shifted temporal slices, and MIOpen's conv2d kernels are well optimized. This rebinds WanCausalConv3d.forward (class-level, idempotent, ROCm builds only) to that decomposition: - same 3-latent-frame decode: 81.6s -> 1.71s (~48x), extrapolating to ~12s for the 81-frame workload — matching NVIDIA wall-clock - numerically equivalent up to summation order: ~1e-6 max error vs F.conv3d in fp32; full bf16 decode differs by <=3/255 in pixel space (0.1% of pixels by more than 1/255) - strided encoder downsample convs keep the stock F.conv3d path (temporal taps couple under stride) - applied from every AutoencoderKLWan load site (Wan checkpoint/diffusers VAE loaders, Wan main-model VAE submodel, Anima VAE), so decode, encode, and ref-image conditioning all benefit; CUDA builds are untouched Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: self-heal the media cookie for sessions that predate or outlive it Video playback authenticates via an HttpOnly cookie (media elements can't send Authorization headers) that was only issued at login. A session restored from localStorage can hold a valid JWT without the cookie — the session may predate the cookie's introduction, or the cookie may have been cleared while the JWT survived. Every API call works, but each <video> request 401s and the player silently renders black with 0:00 duration (hit during PR #9163 functional testing). - New POST /api/v1/auth/media-cookie re-issues the cookie from a valid Bearer token: same live-user check as get_current_user, cookie lifetime clamped to the token's remaining validity, successful no-op in single-user mode. Cookie attributes are shared with login via _set_media_cookie so they can't drift. - Frontend calls it once per app load when an authenticated session exists (useMediaCookieRefresh in GlobalHookIsolator); failures are left to the existing global 401 handling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: use Apply LoRA Collection node in Wan concept-LoRA workflow templates Replace the per-slot concept-LoRA plumbing in the three 'w/ Concept LoRAs' video templates (Text to Video, Image to Video, Extend Video) with the single wan_lora_collection_loader node: users now add any number of concept LoRAs through one multi-LoRA form field instead of two fixed slots (T2V/Extend) or the lora_selector + collect chain (I2V). Chain in all three: model loader -> Lightning high-noise LoRA -> Lightning low-noise LoRA -> Apply LoRA Collection (concept LoRAs, ships empty) -> denoise. Also prunes exposedFields entries that referenced nodes deleted in an earlier revision of these templates (pre-existing; the frontend ignored them, but they were dead weight). Validated: backend WorkflowValidator + default-sync asserts, node versions current, every edge/form/exposedFields reference resolves, frontend parseAndMigrateWorkflow accepts all three, no machine-specific model identifiers ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: enforce multiuser image authorization * fix: address remaining Wan video review findings * test: give closed-stream decoder test headroom for slow Windows spawn The 0.2s decode timeout raced against Python subprocess startup on the Windows CI runner: the inactivity deadline fired before the worker could close its stdout, raising the generic decode timeout instead of the expected 'decoder worker' one. A generous timeout makes the EOF path deterministic; proc.wait still bounds the test at ~1s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address adversarial Wan video review findings * fix: resolve remaining Wan video review issues * feat(ui): rename gallery/board strings from Images to Images/Videos The gallery grid, selections, board operations, and related settings now operate on both images and videos, so the user-facing strings that describe them say so. Image-only surfaces (compare, reference images, progress previews, image storage maintenance, upload-format errors) are unchanged, as are unused legacy keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): harden video/media API per PR review - Scope the media cookie (set + delete) and the sliding-token middleware's auth-route exclusions to the reverse-proxy root_path, so media auth works behind sub-path proxies and a proxied logout can't mint a replacement token. - Video upload: run filesystem writes, MP4 validation, ffmpeg probing, and create() in the thread pool; add VideoUploadLimitASGIMiddleware to bound request size before multipart spooling and cap concurrent uploads. - Add GET /videos/i/{name}/workflow (mirrors the image route) so persisted video workflows/graphs are retrievable, with read-access checks. - Add DELETE /videos/uncategorized so the "Delete All Uncategorized Images/Videos" action can cover both media kinds. - Make polymorphic gallery ordering deterministic on created_at ties with kind+name tie-breakers, and pick virtual-board covers via ROW_NUMBER instead of a bare-column MAX() aggregate. - Add cpu_only to WanT5Encoder_WanT5Encoder_Config (parity with the other standalone text-encoder configs; the loader already honors the field). - Regenerate schema.ts/openapi.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): Wan VAE effective device + generalized working-memory estimation - Move VAE inputs to get_effective_device(vae) instead of the globally selected device — a cpu_only Wan VAE previously crashed every Wan VAE invocation on GPU hosts. - Add estimate_vae_working_memory_wan (per-frame conv working set + resident RGB clip, config-driven spatial scale for TI2V's 16x compression) and reserve working memory in all four Wan VAE paths, replacing the Flux estimator / missing reservations. - Fall back to spatial tiling for video decodes whose full-frame working set exceeds the execution device's VRAM, and move the decoded clip to the CPU before MP4 encoding so VRAM isn't held for the encode's duration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): video gallery/deletion/workflow fixes per PR review - Video deletion: use the batch endpoint (one request per invocation), clear workflow-node VideoField inputs only for server-confirmed deletions, and invalidate per-video DTO/metadata/workflow caches on delete (including board-cascade deletions in the board mutations). - VideoFieldInputComponent resets its value only on a confirmed 404, not on transient auth/server/network errors. - Global Delete hotkey partitions the polymorphic selection and routes videos through the video delete flow. - "Delete All Uncategorized Images/Videos" now deletes both media kinds; "Download Board" relabeled "Download Board Images" (image-only endpoint). - Translation splits: image-only multi-select actions revert to "Images"; polymorphic gallery search + star hotkey become media-neutral; the multi- drag preview counts the whole mixed selection. - Expose video metadata + workflow in the viewer: new video details overlay (metadata/workflow/graph tabs), a Load Workflow toolbar action for videos, and a 'video' source for the load-workflow dialog. - Model Manager: wan_t5_encoder gets the encoder settings panel (Run on CPU). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: make gallery docs video-aware; fix video workflow count Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address remaining video review findings * fix(backend): Wan inference fixes from full-PR self-review - Force bfloat16 in the standalone Wan VAE checkpoint loader: `precision: auto` resolves to fp16 on CUDA, and fp16 is unstable on the Wan VAE (the diffusers folder path already forced bf16). Both starter VAEs route through this loader. - Count the decoded RGB clip twice in the Wan working-memory estimator: diffusers' frame accumulation transiently holds ~2x the clip at peak, which the tiled-decode fallback previously undercounted by up to ~2 GB. - Ignore a wired 'Transformer (Low Noise)' for TI2V-5B (warn instead of raising a misleading A14B error), matching the field's documented behavior. - Release the expert swapper's device context even when LoRA weight-restore raises, so a failed unwind can't pin an 8-9 GB expert in VRAM. - Validate LoRA variant (A14B vs 5B) against the wired transformer in both Wan LoRA loaders — a mismatch previously crashed mid-denoise with an opaque layer-patcher shape error. - Fix the WanDiffusersModel exception ladder: the old-diffusers torch_dtype retry now also gets the missing-variant OSError fallback, with the matching dtype kwarg. - Mark both Wan ideal-dimensions nodes Prototype like every other Wan node; correct the text-encoder docstring (seq_len 512, not 226). - Add CPU tests for the multi-frame WanVideoDenoise loop (T_lat>1 shapes, zero-velocity invariant, A14B I2V 36-channel concat across frames, TI2V-5B expand-timesteps mask blend incl. per-token timesteps and frame-0 restore). Node version bumps: wan_model_loader, wan_lora_loader, wan_lora_collection_loader -> 1.0.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): video service fixes from full-PR self-review - Purge cached invocation outputs on video deletion: the memory invocation cache registered images/tensors/conditioning on_deleted hooks but not videos, so re-running an identical graph after deleting its output "succeeded" with a cached VideoOutput naming a 404 video. - Add the single-user early-return to VideosInterface's read-access and board-save checks, matching ImagesInterface — after a multiuser->single-user switch, video workflows no longer fail with PermissionError where identical image operations succeed. - Restructure staged-delete recovery to match the image side: video_records .get() raises rather than returning None, so the explicit commit branch was unreachable and recovery semantics lived in the exception handler by luck. - Return 416 (not 206 with "bytes 0--1/0") for any Range request against a zero-length video file; add tests for the whole Range-parser matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): video UX fixes from full-PR self-review - Add 'video' to the invocation-complete passthrough denylist: a Video Primitive completing mid-run invalidated gallery caches and auto-switched the user's selection/board to the node's *input* video. - Show the multi-selection context menu only when the clicked item is part of the selection (both image and video menus): right-clicking a video with 2+ images selected previously produced a menu with every action disabled. - Clear workflow VideoField references when videos are cascade-deleted via board deletion or delete-uncategorized, matching the direct-delete flow. - Toast on total video-delete-batch failure (the untracked mutation was otherwise silent) and on failed logout (the button previously did nothing when the server was unreachable). - Check resp.ok in useDownloadItem so an expired media cookie can't save error bodies as .mp4/.png files. - Provide the LIST_TAG-scoped VideoList tag from listVideos so the star/board invalidations that reference it actually match; fix the misleading comment; dedupe the doubled BoardVideosTotal tag type. - Validate VideoField access on workflow load (checkVideoAccess), resetting stale refs with a warning like image fields. - Wire middle-click-open-in-new-tab for gallery videos (the setting label already promised it). - Show the effective fallback (primary CFG) in the low-noise guidance slider when unset, instead of a constant the run never uses. - "Moving 1 image/video to board:" singular form for mixed-media moves. - Regenerate schema.ts/openapi.json (node version bumps, classification, docstring fixes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: workflows/docs/deps fixes from full-PR self-review - Update all 12 bundled Wan workflows to current node versions (wan_model_loader / wan_lora_loader / wan_lora_collection_loader 1.0.1, wan_ref_image_encoder 1.2.0, backfilling the optional end_image/num_frames inputs) so fresh installs don't open with "node needs update" badges; add a registry-consistency test over the bundled Wan/video workflows so stale embeds can't recur. - Docs: the A14B auto scheduler is UniPC (not FlowMatchEuler); note that the bundled TI2V-5B workflows ship 20 steps as a speed compromise vs the 40-50 quality recommendation. - Pin imageio[ffmpeg]>=2.37 and psutil>=6 (imageio encode behavior is version-sensitive enough that we carry a regression test for it); relock. - De-flake the thumbnail worker descendant-kill test (0.5s was the only tight ceiling in the file; a loaded runner could kill the worker before the child pid file existed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: raise python-tests job timeout to 30 minutes Main already runs 9-11 min per platform and this PR pushed py3.11 windows-cpu past the 15-minute cap (cancelled mid-pytest at 15m10s on the last run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: resolve CI failures from main's route-auth audit and knip The route-authorization audit merged from main (#9367) only recognized the two Bearer-token dependencies, so it flagged the video media routes (which authenticate via get_current_media_user_or_default) and the media-cookie endpoint (which validated its Bearer token inline). Teach the audit about the media dependency, drop the image media routes from PUBLIC_ROUTES (they now carry cookie auth on this branch), and give refresh_media_cookie a CurrentUserOrDefault dependency in place of its duplicated inline validation. The media-cookie tests now patch auth_dependencies' ApiDependencies like every other auth-dependent route test. knip: getDeletedVideosFromDeleteBoardAction was exported but only used in-module; cover it in the listener unit tests like its image twin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): Wan invocation fixes from JPPhoto's 2026-07-21 review - Both Wan LoRA loaders now validate the *resolved* config (type=LoRA, base=Wan) instead of trusting the client-supplied identifier fields; a mislabeled Flux/SDXL/main key is rejected up front instead of reaching the layer patcher. - The collection loader rejects LoRAs already applied upstream on either expert list (same invariant as the single loader) instead of silently doubling their effective weight. - A LoRA routed only to the low-noise list of a TI2V-5B main now logs a warning — the single-transformer path never consumes that list, so the routing was a silent no-op. - _ExpertSwapper._release clears its slots in a nested finally, so a device-context exit failure can no longer leave stale contexts that a later close() would double-exit. - WanLatentsToImage rejects multi-frame (T>1) video latents with a clear error pointing at wan_l2v, before the VAE is even loaded — previously it ran the full multi-frame decode and died in an opaque einops rank error. - wan_ref_image_encoder docstrings now describe both the 20-channel A14B and 48-channel TI2V-5B condition paths (they claimed A14B-only and told users to omit the node for TI2V-5B, contradicting the implementation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): per-user upload slots, probe validation, stream decode fallback - VideoUploadLimitASGIMiddleware now accounts upload slots per user (cap 2) on top of the global cap, so one tenant's slow chunked uploads can no longer hold all four slots and starve other users into 429s. Single-user mode keeps the whole global capacity (no per-user quota). - probe_video validates decoder-reported metadata: non-positive or over-limit dimensions (> 64 MP) and non-finite/negative durations are rejected before the upload path persists them; garbage fps degrades to None (unknown). The decode worker refuses to decode frames from files whose probed dimensions exceed the bound — a small crafted container claiming 100k x 100k would otherwise trigger a ~30 GB allocation. - The worker's stream command falls back to cv2 like probe/frame/count do, so an MP4 accepted at upload via the cv2 path now also works in the frame-range and concat nodes. The fallback only engages before the first emitted frame; a mid-stream decoder death still surfaces as an error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): media resilience fixes from JPPhoto's 2026-07-21 review - Partial board deletion: the boardAndImagesDeleted listener now invalidates the per-item Image*/Video* tags for the confirmed-deleted names it parses out of the 500 detail — the rejected mutation runs invalidatesTags with no result, so those caches previously stayed readable. - Media-cookie refresh retries transient failures on a bounded backoff (2s, 10s) instead of latching before the request and giving up forever; 401 still bails (session genuinely expired). - Thumbnail 404s degrade gracefully: BoardTooltip, GalleryBoard, VirtualBoardItem, and VideoFieldInputComponent show an icon fallback via fallbackStrategy="onError" (thumbnail generation is best-effort server- side), and GalleryVideoThumbnail's <video> fallback does the near-zero seek on loadedmetadata so browsers that don't auto-paint the first frame no longer show a black tile. - CurrentVideoPreview handles play() rejection (rolls isPlaying back) and media element errors (drops back to the play overlay) instead of hiding the overlay over a dead element with an unhandled promise rejection. - Hardening from the disputed items: changeVideoIsIntermediate also invalidates the VideoList LIST_TAG (covers a future omitted-board_id list); logout clears gallery.selection and the logout mutation documents that resetApiState in store.ts is what actually clears cross-user caches. - Typegen regenerated for the wan_lora_loader / wan_ref_image_encoder docstring updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address final video review findings * fix: harden video workflows and auth refresh * chore: regenerate OpenAPI schema * fix: close video workflow review gaps * fix: address self-review findings on video workflows and auth refresh Fixes the confirmed findings from the 2026-07-22 self-review round (github.com/invoke-ai/InvokeAI/pull/9163#issuecomment-5051225515): Frontend: - DeleteVideoModal: detach the dialog promise callbacks before the async deletion so the accept path's synchronous onClose (bound to cancel) no longer rejects a confirmed deletion as "User canceled"; dismissal still rejects. Adds behavioral tests for both paths. - Sliding-window refresh: bound the media-cookie sync fetch with a 10s AbortSignal timeout so a stalled request can't hold the exclusive cross-tab media-auth lock (shared with login/logout) forever; commit the refreshed token even when the cookie sync fails with a 5xx/network error (only a 401/403 rejection of the token blocks the commit); throttle acceptance to once per minute so bulk mutations don't pay a serialized cookie round trip per request. - Fallback media-auth lock: waiters renew their ticket lease while queueing so a >30s wait no longer lets a later ticket enter concurrently. - useMediaCookieRefresh: a pause() abort now resumes the same attempt instead of consuming a retry slot (and no longer permanently disables self-heal when the final attempt was paused); effect cleanup aborts in-flight refreshes so every logout path (sessionExpiredLogout, direct logout) stops a pending refresh from re-minting the cookie post-logout. - CurrentVideoPreview: a benign AbortError from play() rolls back silently, and load errors during the pending media-cookie self-heal window no longer raise a spurious "Unable to Play Video" toast. Backend: - Decode-worker memory bounds resized for legal near-cap frames: worker RLIMIT_AS headroom 1->4 GiB, parent RSS kill threshold 1->3 GiB (with keep-in-sync cross-references), monitor poll 50->250 ms. - Upload probe: a decode-worker timeout is now inconclusive (upload proceeds) instead of a 415; the probe's decoded first frame is reused as the thumbnail source, dropping one worker subprocess per upload. - delete_images_on_board / delete_videos_on_board return (deleted, failed) and delete_board reports the services' ground truth instead of a racy router-side listing diff (which also doubled the DB work). - Video list/uncategorized delete endpoints skip HTTPException (ownership skips, 404 races) silently instead of reporting them as failures, matching the image endpoints; delete_images_from_list now populates failed_images for genuine failures, matching the video path. - video_concat: an unknown probed fps mixed with agreeing known rates uses the known rate again instead of hard-erroring; disagreeing known rates still require an explicit Output FPS. - SlidingWindowTokenMiddleware runs its synchronous SQLite user lookup via run_in_threadpool so a contended DB lock can't stall the event loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): deflake drip-feed upload timeout test on Windows The test gave the middleware a 20 ms absolute upload deadline and delivered a chunk every 5 ms — but Windows event-loop timers have ~15.6 ms granularity, so the deadline could expire before the first chunk was ever delivered. The request then ended at receive_calls == 1 and the `receive_calls > 1` assertion failed (py3.12 windows-cpu CI). Widen the margins so the scenario the test describes actually occurs on coarse timers: 250 ms absolute deadline (many chunks flow first on every platform) with a 1 s idle timeout that never fires between 5 ms chunks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): sort imports in test_video_upload_limits (ruff I001) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address in-PR items from JPPhoto's non-merge-blocker list Fixes the subset of JPPhoto's 2026-07-22 "Still Open, Non-Merge Blockers" that are small, self-contained, and scoped to surfaces this PR introduced; the rest are deferred to a follow-on PR (triage rationale posted on the PR). - video_thumbnails._run_worker: an unexpected exception from communicate() (e.g. OSError) now terminates the worker process tree unconditionally — previously the finally stopped the RSS-monitor backstop while the except path left the worker and its ffmpeg child running forever. Adds the injected-OSError test JPPhoto asked for. - reduxRemember driver: client-state persistence POSTs now commit X-Refreshed-Token via the same acceptance flow as dynamicBaseQuery (extracted as acceptRefreshedToken, sharing the cross-tab lock, cookie sync, throttle, and generation guards), so persistence-only sessions no longer hard-expire mid-activity. - delete_videos_from_list / delete_images_from_list: dedup request names — a repeated name was processed twice and landed in both deleted_* and failed_* under the admin ownership bypass, toasting a spurious partial failure. Regression test added. - gallery + videos list endpoints: bound offset (ge=0) and limit (ge=0, le=MAX_PAGE_SIZE=1000) — these flowed verbatim into SQL, where a negative LIMIT means unlimited in SQLite, so one request could materialize the entire gallery. openapi.json regenerated (schema.ts is unchanged — constraints don't alter the generated types). - get_video_full: open the file once and serve HEAD/range/full from the fd (full downloads now stream chunked from the handle instead of FileResponse's lazy path-based open), eliminating the delete-race 500; deletion's atomic rename can no longer invalidate a path between check and open. - upload_video: close the multipart spool immediately after the body copy, shrinking the double-temp-disk window (2 x 1 GiB x 4 concurrent worst case) to the copy loop itself. - docs/gallery.mdx: document shared-board deletion semantics (only your own media is permanently deleted; admins delete everything) and the kept-on-failure -> Uncategorized behavior with its UI warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix video gallery review findings * fix Windows video thumbnail handling * test: cover remaining video review findings * test: cover adversarial video and Wan findings * fix: address remaining video and Wan review findings * test: call now-sync star/unstar routes directly 11b38696bf converted the video batch routes from async def to sync def (so FastAPI offloads them to its threadpool), but the star/unstar dedupe test still drove them through asyncio.run(), which requires a coroutine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover Wan conditioning and video link regressions * fix: validate Wan conditions and video links --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com> Co-authored-by: JPPhoto <jpollack@jpollackphoto.com> | 1 个月前 | |
chore(item-storage): improve types Provide type args to the generics. | 2 年前 | |
feat(mm): rename "blake3" to "blake3_multi" Just make it clearer which is which. | 2 年前 | |
tidy(mm): ModelSearch cleanup - No need for it to by a pydantic model. Just a class now. - Remove ABC, it made it hard to understand what was going on as attributes were spread across the ABC and implementation. Also, there is no other implementation. - Add tests | 2 年前 | |
Fix collector scoping and invocation validation (#9483) * Fix collector scoping and invocation validation * chore: ruff * Harden collector scope and optimized validation * Preserve nested collector scopes at arbitrary depth | 24 天前 | |
Add chaining to Collect node (#8933) * Add chained collect node * test(frontend): align parseSchema fixtures with collect v1.1 and normalize undefined fields in assertions * fix(nodes): block collect-to-collect links when inferred item types differ --------- Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com> | 5 个月前 | |
Fix stale serializer cache IDs after deletion (#9390) Co-authored-by: liucl <liu_cl8888@163.com> Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev> | 20 天前 | |
chore: ruff | 2 年前 | |
feat: add profiler util (#5601) * feat(config): add profiling config settings - `profile_graphs` enables graph profiling with cProfile - `profiles_dir` sets the output for profiles * feat(nodes): add Profiler util Simple wrapper around cProfile. * feat(nodes): use Profiler in invocation processor * scripts: add generate_profile_graphs.sh script Helper to generate graphs for profiles. * pkg: add snakeviz and gprof2dot to dev deps These are useful for profiling. * tests: add tests for profiler util * fix(profiler): handle previous profile not stopped cleanly * feat(profiler): add profile_prefix config setting The prefix is used when writing profile output files. Useful to organise profiles into sessions. * tidy(profiler): add `_` to private API * feat(profiler): simplify API * feat(profiler): use child logger for profiler logs * chore(profiler): update docstrings * feat(profiler): stop() returns output path * chore(profiler): fix docstring * tests(profiler): update tests * chore: ruff | 2 年前 | |
feat: Video generation (#9163) * feat(model): add Wan 2.2 image generation support (Phases 0-2) Foundation + TI2V-5B MVP + A14B dual-expert MoE for Wan 2.2 image generation. Wan was trained on video but is competitive with leading open-source image models when run at num_frames=1; this commit wires that path into InvokeAI. Phase 0 — Foundation: - BaseModelType.Wan + WanVariantType {T2V_A14B, TI2V_5B} - SubModelType.Transformer2 for the dual-expert MoE - MainModelDefaultSettings per variant - step_callback Wan branch (16-channel preview; 48-channel TI2V-5B falls back to slicing first 16 channels until proper factors land) - Frontend enums + node colour Phase 1 — TI2V-5B Diffusers MVP: - Main_Diffusers_Wan_Config probe (variant from transformer_2/ + vae/config.json::z_dim, with filename heuristic fallback) - WanDiffusersModel loader (subclasses GenericDiffusersLoader) - WanT5EncoderField, WanTransformerField (with dual-expert slots), WanConditioningField, WanConditioningInfo - New invocations: wan_model_loader, wan_text_encoder, wan_denoise, wan_image_to_latents, wan_latents_to_image - FlowMatchEulerDiscreteScheduler integration with on-disk config load - RectifiedFlowInpaintExtension reused for inpaint - 5D <-> 4D shape juggling: latents stay 4D in InvokeAI's pipeline, re-add T=1 only inside the transformer call / VAE encode-decode Phase 2 — A14B dual-expert MoE: - Probe reads boundary_ratio from model_index.json - Loader emits both transformer (high-noise) and transformer_low_noise (low-noise expert at transformer_2/) for A14B - _ExpertSwapper in wan_denoise drives GPU residency between experts: high-noise for t >= boundary_ratio * num_train_timesteps, low-noise below. Only one expert locked at a time so the cache can evict the other - relies on existing CachedModelWithPartialLoad to handle oversized models on lower-VRAM GPUs. - guidance_scale_low_noise field for separate low-noise CFG override Tests: - 24 passing tests covering probe variant detection, default settings, noise sampling, end-to-end denoise on a synthetic transformer (CPU), dual-expert boundary swap, CFG branch - 1 heavy-test placeholder gated by INVOKEAI_HEAVY_TESTS=1 for the real-weights smoke test Phase 3+ deferred: standalone VAE/encoder configs, GGUF, LoRA, ControlNet, ref image, inpaint UI, frontend wiring, starter models. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(model): Wan 2.2 Phase 3 + tokenizer-load fix Phase 3 adds standalone VAE and UMT5-XXL encoder configs so users can run GGUF-quantized Wan transformers (Phase 4) without installing the full ~30 GB Diffusers pipeline. VAE configs: - VAE_Checkpoint_Wan_Config + VAE_Diffusers_Wan_Config (16-channel A14B vs 48-channel TI2V-5B, distinguished by decoder.conv_in z_dim). - 16-channel files share the AutoencoderKLWan architecture with Qwen Image; disambiguated via filename heuristic ("wan" in name -> Wan, otherwise -> Qwen Image). Mirror exclusion in QwenImage's probe. - VAELoader gets a Wan branch that builds AutoencoderKLWan(z_dim=...) via init_empty_weights, mirroring the QwenImage single-file pattern. - Existing standard VAE probe excludes both QwenImage- and Wan-style state dicts. UMT5-XXL encoder: - New ModelType.WanT5Encoder + ModelFormat.WanT5Encoder. - WanT5Encoder_WanT5Encoder_Config probes the diffusers folder layout (text_encoder/config.json with model_type=umt5, or flat layout with config.json at root). Refuses full Wan pipelines. - WanT5EncoderLoader handles both layouts and loads UMT5EncoderModel + AutoTokenizer. Component-source plumbing: - WanModelLoaderInvocation now exposes wan_t5_encoder_model and component_source pickers (mirrors QwenImage pattern). Resolution order: standalone > main (if Diffusers) > component_source. Required when the main model is a single-file format in Phase 4. Bug fix in wan_text_encoder: - Tokenizer was loading via AutoTokenizer.from_pretrained(<root>) directly, which fails for nested layouts where files live in <root>/tokenizer/. Now routed through the model cache so the registered loaders handle layout differences correctly. Frontend: - New type guards (isWanVAEModelConfig, isWanT5EncoderModelConfig, isWanMainModelConfig, isWanDiffusersMainModelConfig) and hooks/ selectors (useWanVAEModels, useWanT5EncoderModels, useWanDiffusersModels). New zSubModelType / zModelType / zModelFormat enum entries for transformer_2 and wan_t5_encoder. Tests: - 16 new tests covering z_dim detection, VAE checkpoint/diffusers probes, the bidirectional Qwen-vs-Wan filename deferral, and the UMT5 encoder probe (nested + flat + T5 + full-pipeline rejection). - Total Wan test count: 41 passing, 1 heavy-test placeholder skipped. - Full config test suite (63 tests) still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): unbreak frontend lint after Wan additions Five issues turned up running `make frontend-lint`: 1. wan_denoise.py used `from __future__ import annotations`, which made the `invoke()` return annotation a string ('LatentsOutput'). The InvocationRegistry's `get_output_annotation()` returns the raw annotation, so OpenAPI generation crashed with `'str' object has no attribute '__name__'`. Removed the future-import and added `Any` to the typing imports. 2. ModelRecordChanges.variant didn't list WanVariantType, so the generated schema's install/update endpoints rejected `t2v_a14b` and `ti2v_5b`. Added it. 3. Regenerated frontend/web/src/services/api/schema.ts from the live backend so it now includes BaseModelType.wan, ModelType.wan_t5_encoder, SubModelType.transformer_2, ModelFormat.wan_t5_encoder, the Wan variants, all Wan invocation types and their conditioning/transformer field types. 4. modelManagerV2/models.ts: added `wan_t5_encoder` to the category map, `wan` to the base color/long-name/short-name maps, the two Wan variants to the variant-name map, and `wan_t5_encoder` to the format-name map. 5. ModelManagerPanel/ModelFormatBadge.tsx: added `wan_t5_encoder` to FORMAT_NAME_MAP and FORMAT_COLOR_MAP. `make frontend-lint` now passes cleanly (tsc, dpdm, eslint, prettier). All 41 Wan Python tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> chore(wan): drop unused FE exports flagged by knip These were forward-compatibility wiring for Phase 9 (the FE graph builder) that has no consumers yet; knip rightly flagged them. Removed or de-exported. They'll come back when the graph builder lands and needs them. - common.ts: zWanVariantType drops `export` (still used internally by zAnyModelVariant). - types.ts: drop isWanMainModelConfig, isWanDiffusersMainModelConfig, isWanVAEModelConfig (no callers). The remaining isWanT5EncoderModelConfig is used by models.ts. WanT5EncoderModelConfig type drops `export` (still used as the type guard's narrowing target). - modelsByType.ts: drop the six unused useWan*/selectWan* hooks + selectors and their type-guard imports. `make frontend-lint` (tsc + dpdm + eslint + prettier + knip) now green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> docs(wan): use *-Diffusers HF repo names in plan The Wan-AI org publishes two flavours of each release: * Wan-AI/Wan2.2-{TI2V-5B,T2V-A14B,I2V-A14B} ← upstream native * Wan-AI/Wan2.2-{TI2V-5B,T2V-A14B,I2V-A14B}-Diffusers ← convertible The native release has _class_name=WanModel in config.json and ships weights flat at the repo root with no transformer/, vae/, text_encoder/ subdirs. It is not loadable by Diffusers' WanPipeline.from_pretrained. Update plan doc to reference the -Diffusers repos throughout (probe notes, starter-model entries) so the plumbing path matches what the Diffusers loader actually expects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): accept 0 as 'unset' sentinel for guidance_scale_low_noise The frontend renders Optional[float] inputs with default 0 in the numeric input rather than passing null/unset. Combined with ge=1.0, this caused every wan_denoise invocation to fail Pydantic validation with "Input should be greater than or equal to 1" until the user manually entered a value (or knew to leave the field disconnected). The validation error was rejected before invocation logging, so it never showed up in the server log either - making the failure hard to diagnose. Relaxing the constraint to ge=0.0 and treating values below 1.0 as the "fall back to primary Guidance Scale" sentinel. The user's natural FE default (0) now works as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): correct preview dimensions and colors for TI2V-5B Two bugs in the Wan branch of the diffusion step callback: 1. Wrong dimensions. The reported preview size hardcoded `* 8` for the spatial downscale ratio, but TI2V-5B's Wan2.2-VAE uses 16x. A 1024x1024 target was being announced to the FE as 512x512. 2. Wrong colors. The previous fallback for 48-channel TI2V-5B latents sliced the first 16 channels and applied the standard 16-channel Wan-VAE projection. Those channel layouts are unrelated, so the projection produced meaningless colors. Adding the proper Wan2.2-VAE 48-channel RGB projection matrix (and bias) from ComfyUI's Wan22 latent format, and selecting the right matrix + spatial-scale by latent channel count: 16 → A14B (Wan VAE, 8x), 48 → TI2V-5B (Wan2.2-VAE, 16x). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): honor model's _class_name when building scheduler TI2V-5B's scheduler_config.json declares _class_name=UniPCMultistepScheduler with flow_shift=5.0. The previous code hardcoded FlowMatchEulerDiscreteScheduler.from_pretrained(...), which silently constructed a default-config FlowMatch instead of the UniPC the model expects. The mismatched noise schedule manifests as soft / under-denoised faces and global graininess in the final images. Now: read scheduler_config.json, look up the named class on the diffusers module, and instantiate that class via from_pretrained. UniPC and FlowMatch share the same step()/set_timesteps()/sigmas/num_train_timesteps interfaces, so the denoise loop works transparently for either. A14B continues to use FlowMatchEulerDiscreteScheduler when its scheduler config says so (its reference is FlowMatchEuler with shift=8.0). Falls back to FlowMatchEulerDiscreteScheduler defaults when no on-disk config is available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): match diffusers WanPipeline tokenizer length and latent dtype Two divergences from the Diffusers reference that were hurting image quality (soft / grainy / distorted faces at default settings): 1. Tokenizer max_sequence_length was 226 in wan_text_encoder, but the model was trained with 512-token sequences. The upstream native config.json has text_len: 512, and Diffusers' WanPipeline.__call__ default is 512 (overriding _get_t5_prompt_embeds's stale 226 default). Wan's cross-attention sees padded zeros past the prompt's actual length but expects to be looking at a 512-position context window. 2. Latents were stored in bf16 throughout the denoise loop. Diffusers' WanPipeline.prepare_latents explicitly uses dtype=torch.float32 and only casts to the transformer's dtype right at the forward call: latent_model_input = latents.to(transformer_dtype) Storing in bf16 between steps accumulates ~40 steps of bf16 quantization on the scheduler's small per-step deltas. Now latent_dtype = torch.float32 throughout, with a per-step cast for the transformer forward pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> chore(wan): add diffusers reference comparison script scripts/wan_diffusers_reference.py runs a Diffusers-format Wan 2.2 checkpoint directly via WanPipeline.from_pretrained, with the same arguments InvokeAI's wan_denoise uses. Use to A/B against InvokeAI output when image quality is questionable. Defaults to enable_model_cpu_offload so the script fits on 16 GB cards where the full pipeline (transformer + UMT5-XXL + VAE) would otherwise OOM. --offload {model,sequential,none} controls the strategy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(model): Wan 2.2 Phase 4 - GGUF transformer support Adds single-file GGUF support for Wan 2.2 transformers, the path that makes A14B usable on consumer GPUs (~7 GB/expert at Q4_K_M instead of ~28 GB at bf16). Probe (configs/main.py): - New helpers: _has_wan_keys (Wan vs Qwen/FLUX/Z-Image fingerprint via condition_embedder.text_embedder.linear_1 + patch_embedding); _detect_wan_gguf_variant (16ch -> A14B, 48ch -> TI2V-5B from patch_embedding.weight.shape[1]); _detect_wan_gguf_expert (filename heuristic for high_noise / low_noise / none). - Main_GGUF_Wan_Config(base=Wan, format=GGUFQuantized, variant, expert). Tolerates the ComfyUI 'model.diffusion_model.' / 'diffusion_model.' prefixes via _has_wan_keys' multi-prefix scan. - Registered in factory.py. Loader (model_loaders/wan.py): - WanGGUFCheckpointModel mirrors the QwenImage GGUF pattern: gguf_sd_loader -> strip ComfyUI prefix -> auto-detect arch from state dict shapes (num_layers, inner_dim, ffn_dim, text_dim, in_channels, num_heads = inner_dim/128) -> init_empty_weights + load_state_dict(strict=False, assign=True). Loader invocation (wan_model_loader.py): - New 'Transformer (Low Noise)' picker: optional second GGUF for the A14B dual-expert MoE. Auto-swaps if the user wired the experts in the wrong order. Warns when an A14B GGUF is loaded without a paired low-noise expert (single-expert run, degraded quality). - GGUF mains require either a standalone VAE+encoder or a Diffusers Component Source (which can also supply boundary_ratio). - Diffusers main path unchanged (still pulls both experts from transformer/ + transformer_2/). Tests (tests/.../test_wan_gguf_config.py): - 14 tests across key fingerprint, variant detection, expert filename heuristic, and the full probe (A14B high/low, TI2V-5B, GGUF rejection, unrecognised state-dict rejection, explicit override). Total Wan tests: 55 passing (no regressions). FE lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): support QuantStack-style GGUFs and standalone Diffusers VAE The city96 Wan 2.2 GGUF repos have been removed from Hugging Face, leaving QuantStack as the surviving distributor. QuantStack ships the native upstream Wan key layout (text_embedding.0/2, self_attn/cross_attn, ffn.0/2, head.head, head.modulation, ...) rather than the diffusers naming city96 used; biases are stored as F16 rather than BF16; and the standalone Wan VAE installs as a flat AutoencoderKLWan folder which the generic loader rejects. Three fixes: 1. Probe now recognises both diffusers and native key layouts via a new _is_native_wan_layout helper; _has_wan_keys accepts either text-proj fingerprint. 2. GGUF loader converts native -> diffusers keys (mirroring diffusers' convert_wan_transformer_to_diffusers) and unwraps non-quantized GGMLTensors to plain tensors at compute_dtype. The unwrap is needed because conv3d isn't in GGMLTensor's dispatch table, so the F16 patch_embedding bias would otherwise hit conv3d against bf16 latents. 3. VAELoader gains a VAE_Diffusers_Wan_Config branch that loads AutoencoderKLWan directly; the generic path can't handle a flat single-class folder when a submodel_type is provided. Adds 12 tests covering the native layout (probe + converter + unwrap). Verified end-to-end against Wan2.2-T2V-A14B-Q4_K_M from QuantStack: 1095 tensors round-trip key-for-key against WanTransformer3DModel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(model): Wan 2.2 Phase 5 - LoRA support Probe + config (LoRA_LyCORIS_Wan_Config): - Detects Wan LoRAs in three layouts: diffusers PEFT, native upstream PEFT (ComfyUI), and Kohya (both naming variants). - Anti-pattern guards prevent collisions with Anima (Cosmos DiT q_proj convention), QwenImage (transformer_blocks), Flux (double/single blocks), and Z-Image (diffusion_model.layers). - Optional ``expert: "high" | "low" | None`` field; auto-detected from filename (high_noise / low_noise / hyphenated / concatenated variants). Key conversion (wan_lora_conversion_utils): - Native upstream keys (self_attn/cross_attn, ffn.0/2) -> diffusers (attn1/attn2, ffn.net.0.proj / ffn.net.2). - Strips ``transformer.``, ``diffusion_model.``, ``base_model.model.transformer.`` prefixes from PEFT-style keys. - Kohya layer names mapped through an explicit longest-match table. - Output paths use diffusers naming so the LayerPatcher can resolve them against WanTransformer3DModel parameter paths. Loader integration: - Adds BaseModelType.Wan branch to LoRALoader._load_model. Invocation nodes (wan_lora_loader.py): - WanLoRALoaderInvocation: single LoRA with auto/both/high/low target field. - WanLoRACollectionLoader: list of LoRAs, auto-routed by each LoRA's recorded expert tag. - Output WanLoRALoaderOutput carries the WanTransformerField with updated ``loras`` / ``loras_low_noise`` lists. Denoise integration: - _ExpertSwapper now manages both the model_on_device context and the LayerPatcher.apply_smart_model_patches context per expert. LoRA patches are entered after device load and exited before device release, with fresh iterators per swap. - GGUF (quantized) experts request sidecar patching so GGMLTensor weights aren't touched directly. - Low-noise expert falls back to the primary loras list when ``loras_low_noise`` is empty (matches WanTransformerField semantics). Tests: 81 new tests covering probe accept/reject across formats, anti-pattern guards on competing architectures, converter round-trips for all three layouts, invocation target resolution + routing + duplicate guards, and the _ExpertSwapper lifecycle (lora context opens/closes in the right order around the device swap, quantized flag forwards, no-LoRA path skips the patch context, re-entering the same label is a no-op). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): probe Wan LoRA before Anima in the config union Native-PEFT Wan LoRAs (lightx2v's Lightning, most ComfyUI-trained Wan LoRAs) carry keys like ``diffusion_model.blocks.X.cross_attn.k.lora_A.weight``. Anima's probe matches on the bare ``cross_attn``/``self_attn`` substring — it does not require the Anima-specific ``_proj`` suffix nor any of the ``mlp``/``adaln_modulation`` Cosmos DiT markers — so these Wan LoRAs were classified as ``BaseModelType.Anima`` because Anima happened to run first. Reorder the LyCORIS section of ``AnyModelConfig`` so Wan probes first. Wan's probe is strictly more restrictive (it rejects Anima's ``_proj`` attention suffix via the anti-pattern guard added in the previous commit), so Anima LoRAs are still correctly classified after this reorder. Existing users with mis-tagged installs need to delete the affected LoRA records and reinstall. Adds two regression tests: a union-ordering assertion, and a sanity check that demonstrates Anima's probe *would* match Wan native keys if asked directly — pinning the constraint that motivates the ordering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> chore(i18n): add Wan2.2 T5 Encoder model-manager label The frontend source already references ``modelManager.wanT5Encoder``; the locale key was added with a casing typo (``want5Encoder``). Fix the key so the Wan T5 Encoder model type renders its display name correctly in the model manager UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(model): Wan 2.2 Phase 7 - reference-image (I2V) conditioning Re-implementation after the first attempt — which used CLIP-vision conditioning — was reverted. Wan 2.2 I2V-A14B does NOT use a CLIP-vision encoder (the Diffusers repo ships ``image_encoder: [null, null]`` in ``model_index.json``); instead it conditions on a reference image by VAE-encoding it and concatenating the resulting latents (plus a first-frame mask) to the noise latents along the channel dim. The I2V transformer therefore has ``in_channels=36`` (16 noise + 16 ref-image latents + 4 mask) vs ``in_channels=16`` for T2V. Taxonomy: - Re-adds ``WanVariantType.I2V_A14B``. Probes: - Diffusers: ``_detect_wan_variant`` reads ``transformer/config.json::in_channels``; 36 → I2V_A14B, 16 → T2V_A14B (both share the dual-expert layout). - GGUF: ``_detect_wan_gguf_variant`` recognises ``in_channels=36`` from the patch_embedding tensor shape and emits I2V_A14B. Backend extension (``backend/wan/extensions/wan_ref_image_extension.py``): - ``preprocess_reference_image`` resizes + normalises to a 5D pixel tensor. - ``encode_reference_image_to_condition`` VAE-encodes the image and stacks a 4-channel first-frame mask on top, producing the ``[1, 20, 1, H/8, W/8]`` condition tensor the denoise loop consumes. - Mirrors diffusers ``WanImageToVideoPipeline.prepare_latents`` with ``num_frames=1`` and ``expand_timesteps=False``. Invocation node (``wan_ref_image_encoder.py``): - "Reference Image - Wan 2.2": image + VAE + width/height pickers. - Output ``WanRefImageConditioningField`` carries the condition tensor name plus the dimensions used (so the denoise step can validate dim parity). Denoise integration: - ``WanDenoiseInvocation`` gains an optional ``ref_image`` field. - Variant gate: rejects ref_image on T2V_A14B and TI2V-5B with a clear error before doing any work. - Dimension gate: rejects ref-image width/height mismatch vs denoise. - At every transformer call, concatenates the 20-channel condition tensor to the 16-channel noise latents along the channel dim before passing to the transformer (giving the 36-channel input I2V expects). Tests: 14 new across the probe, the extension, and the denoise loop. The synthetic ``_ZeroTransformer`` test stand-in now mirrors the real I2V transformer's ``in_channels=36, out_channels=16`` asymmetry by slicing its zero output back to 16 channels when the input is 36-wide. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): derive GGUF out_channels from proj_out shape (I2V support) The GGUF loader was setting ``out_channels = in_channels`` which is wrong for Wan 2.2 I2V-A14B: that variant has ``in_channels=36`` (16 noise + 16 ref-image latents + 4 first-frame mask, concatenated by the denoise loop) but ``out_channels=16`` since the transformer only predicts the noise component back. Loading an I2V GGUF would build a transformer with the wrong proj_out shape and crash: RuntimeError: Error(s) in loading state_dict for WanTransformer3DModel: size mismatch for proj_out.weight: copying a param with shape torch.Size([64, 5120]) from checkpoint, the shape in current model is torch.Size([144, 5120]). (144 = 36 * 4, 64 = 16 * 4 — patch_size=(1, 2, 2) → prod=4) Read out_channels directly from the ``proj_out.weight`` shape in the state dict. This is correct for all three Wan 2.2 variants without needing to know the variant in advance. Also tighten the num_layers fallback: T2V_A14B and I2V_A14B share 40 layers; only TI2V-5B has 30. The fallback is rarely hit in practice (the per-block count comes from the state dict scan), but the previous code would have defaulted I2V_A14B to 30 layers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(model): make Anima LoRA probe mutually exclusive with Wan InvokeAI's ``Config_Base.CONFIG_CLASSES`` is a Python ``set``, so iteration order during model probing is non-deterministic across process restarts. First-match-wins ordering in ``AnyModelConfig`` is documentation only — it has no effect on which config is iterated first. Anima's previous probe accepted any state dict containing the substring ``cross_attn`` or ``self_attn``, which collides with Wan's native LoRA key layout (``diffusion_model.blocks.X.cross_attn.q.lora_down.weight``). Both probes accepted Wan native LoRAs (including lightx2v's Lightning T2V and I2V distillations), and the ``matches.sort_key`` tiebreaker only disambiguates by ModelType, not within LoRA configs. So which config "won" depended on dict hash order — sometimes Wan, sometimes Anima. The previous mitigation reordered the AnyModelConfig union to put Wan before Anima. That worked by luck and was inherently fragile. Tighten Anima's probe to require Cosmos-DiT-exclusive subcomponents: ``mlp``, ``adaln_modulation``, or ``_proj``-suffixed attention names (``q_proj``/``k_proj``/``v_proj``/``output_proj``) — none of which appear in any Wan LoRA. Wan native uses bare ``.q``/``.k``/``.v``/``.o`` on ``self_attn``/``cross_attn``, and ``ffn.N``/``ffn.net.N`` instead of ``mlp``. The new strict detectors live alongside the original loose ones so the Anima conversion utility (which runs after probing) still works. Regression tests in ``test_wan_lora_probe_independence.py`` cover: - I2V Lightning V1 (the bug-triggering LoRA), T2V Lightning V2, Wan Kohya and Wan diffusers PEFT layouts — Wan probe accepts, Anima probe rejects. - Anima PEFT and Kohya layouts — Anima accepts, Wan rejects. - A meta-test that runs every LoRA config in CONFIG_CLASSES against the Lightning state dicts and asserts exactly one accepts — this catches ANY future probe collision, not just Wan vs Anima. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): defer expert model loading in _ExpertSwapper to avoid cache thrash The swapper used to take pre-loaded ``LoadedModel`` handles at construction: high_info = context.models.load(self.transformer.transformer) low_info = context.models.load(self.transformer.transformer_low_noise) swapper = _ExpertSwapper(high_info=high_info, low_info=low_info, ...) With dual ~9 GB A14B GGUF experts plus the ~10 GB UMT5-XXL encoder competing for the same RAM cache, the LRU policy frequently dropped one expert by the time the denoise loop swapped into it. The model manager then emitted [MODEL CACHE] Locking model cache entry ... but it has already been dropped from the RAM cache. This is a sign that the model loading order is non-optimal in the invocation code (See ... #7513). and reloaded the weights from disk (~1.2s extra per swap). Refactor the swapper to take the ``ModelIdentifierField`` plus the ``InvocationContext`` and call ``context.models.load(model_id)`` lazily inside ``get()``. Each swap obtains a fresh handle, the LRU window is small, and the warning goes away. Config metadata (used to compute ``is_quantized``) is read upfront via ``context.models.get_config()`` — that's metadata, not weights, so it doesn't put pressure on the cache. Tests: existing swapper lifecycle tests refactored to use a fake context whose ``models.load`` is logged. A new ``test_lazy_load_per_swap_not_upfront`` pins the regression — it asserts ``models.load`` is NOT called at swapper construction, only at first get() per expert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(wan): add Phase 8 inpaint regression tests The denoise_mask wiring + RectifiedFlowInpaintExtension integration in wan_denoise.py was put in place during Phase 2/3 alongside the rest of the denoise loop. Phase 8 of the plan was about ensuring this path worked and is locked in by tests. Three new tests under TestWanDenoiseInpaint: 1. test_preserved_region_matches_init_exactly: builds a half/half mask (left = preserve, right = regenerate in user-side convention), runs full denoise with the synthetic zero-output transformer, and asserts the preserved half of the final latents equals the init exactly while the regenerated half does not. Pins the mask-inversion + per-step merge behavior. 2. test_inpaint_requires_init_latents: a mask without init latents must raise a clear ValueError — the merge has nothing to weld back to. 3. test_no_mask_path_is_unchanged: regression that adding the inpaint extension didn't perturb the non-inpaint codepath (with init latents + denoising_start=0.5 but no mask, the loop just runs img2img). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(frontend): add I2V_A14B to Wan variant zod enum + manager label Phase 7 added the I2V_A14B backend variant. The frontend's zod enum (features/nodes/types/common.ts:zWanVariantType) and the model manager's variant-label map (features/modelManagerV2/models.ts) were still on the two-variant list, so: - ModelIdentifierField inputs with ui_model_variant filters on Wan couldn't list I2V models. - The model manager UI showed a raw 'i2v_a14b' string instead of the human label. Phase 9 (full linear-view wiring — type guards, hooks, params slice, graph builder, tab UI) is in progress on a follow-up commit; this lands the two small enum fixes first so the I2V probe / install paths work correctly end-to-end with the existing FE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): Phase 9 piece #1 - linear-view T2V txt2img graph builder Adds the minimum frontend wiring needed to generate Wan 2.2 images from the linear view: - buildWanGraph.ts (new): text-to-image graph (model_loader → text_encoder × 2 → denoise → l2i). Diffusers main model only — transformer, VAE and UMT5 encoder all resolve from the same repo, so no Wan-specific params slice fields are required yet. CFG-skip branch when guidance_scale ≤ 1.0. - useEnqueueGenerate / useEnqueueCanvas dispatchers: route base === 'wan' to buildWanGraph. - graph/types.ts: add wan_l2i / wan_i2l / wan_denoise / wan_model_loader to the relevant node-type unions. - addTextToImage / addImageToImage: include wan_denoise / wan_l2i so width/height are wired correctly and the txt2img helper accepts the Wan l2i node. - isMainModelWithoutUnet: include wan_model_loader (Wan has no UNet, same as the other modern bases). - metadata.py: add wan_txt2img / wan_img2img / wan_inpaint to the generation_mode enum (img2img / inpaint pieces land next). - schema.ts: regenerated to pick up the metadata enum + new Wan invocations. Pieces left in Phase 9: params slice (standalone VAE / T5 / GGUF low-noise / LoRA / ref-image fields + selectors), img2img + I2V + inpaint branches in the graph builder, and Wan-specific UI components. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #2 - GGUF support and CFG-Low control in linear view Adds the three Wan-specific params + UI controls that gate GGUF workflows plus a separate low-noise CFG slider for A14B users. Params slice: - wanTransformerLowNoise (the second-expert GGUF for A14B) - wanComponentSource (Diffusers Wan model providing VAE + UMT5-XXL when the main is a GGUF) - wanGuidanceScaleLowNoise (optional separate CFG for the low-noise expert; null = fall back to the primary CFG) Plus a `selectIsWan` selector for accordion gating. UI components: - ParamWanModelSelects.tsx (Advanced accordion): two model pickers — Transformer (Low Noise) filtered to Wan GGUF mains, and VAE/Encoder Source filtered to Wan Diffusers mains. Mirrors the ParamQwenImageComponentSourceSelect structure. - ParamWanGuidanceScaleLowNoise.tsx (Generation accordion): slider + number input with an "auto" indicator when cleared. Default 3.5 matches the diffusers reference 4.0 / 3.0 split. Wiring: - Generation accordion: ParamWanGuidanceScaleLowNoise shown when base is wan, scheduler excluded for wan (same pattern as Anima/Qwen). - Advanced accordion: ParamWanModelSelects shown when base is wan, and Wan excluded from the SD-family VAE/CFG-rescale blocks. - buildWanGraph.ts: forwards the three new params to the model loader and denoise nodes (transformer_low_noise_model, component_source, guidance_scale_low_noise) and adds them to the graph metadata. Hooks/types: - useWanDiffusersModels + useWanGGUFModels in modelsByType.ts. - isWanDiffusersMainModelConfig + isWanGGUFMainModelConfig type guards. - Three new locale strings (wanComponentSource, wanTransformerLowNoise, wanGuidanceScaleLowNoise[Auto]). GGUF workflow now works end-to-end in the linear view: pick a Wan GGUF main, set Transformer (Low Noise) to the paired second-expert GGUF, set VAE/Encoder Source to any Diffusers Wan repo (TI2V-5B is convenient at ~12 GB) — generate produces an image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): UX polish on the Wan linear-view controls Bundles four small fixes applied during a usability review of the Wan linear-view section (piece #2): 1. **Filter Main vs Transformer (Low Noise) dropdowns by expert tag.** The Wan GGUF probe records each file's ``expert`` field (``"high"`` / ``"low"`` / ``"none"``) via filename heuristic. - ``MainModelPicker``: hides ``expert === 'low'`` Wan GGUFs so users can't accidentally wire a low-noise expert as the primary main. - Transformer (Low Noise) picker (``useWanGGUFLowNoiseModels``): shows ``expert === 'low'`` Wan GGUFs only. Diffusers Wan mains and TI2V-5B aren't affected — they don't carry the ``expert`` field on their config schema. The backend's auto-swap safety net stays in place. 2. **Match the primary CFG slider's range.** The Wan low-noise CFG slider was constrained to 1–10 while the primary CFG ranges 1–20. With the diffusers reference 4/3 split, the low-noise slider thumb sat noticeably further right than the primary — visually misleading. Both sliders now share the 1–20 range with marks at [1, 10, 20]. 3. **Label fits the form column.** "CFG (Low Noise)" → "CFG (Low)" so the slider fits cleanly next to its label instead of overlapping. 4. **Indicator state for the low-noise CFG slider.** Replaced the inline "(auto)" / "(same as cfg)" text — which kept overlapping the slider regardless of how short the label got — with an X-only reset button that's only visible when the user has set an explicit value. Absence of the X conveys auto/fallback state without any text overhang. 5. **Friendlier Transformer (Low Noise) placeholder.** "Second-expert GGUF for A14B (pair with the high-noise main)" → "Add for full detail" — concise nudge for users who haven't paired the second expert yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #3 - linear-view img2img branch Adds Wan 2.2 image-to-image to the linear view, mirroring the Qwen Image pattern. The mode switches on the canvas state — pure-prompt runs go through addTextToImage as before; canvas runs with an init image go through addImageToImage which wires a fresh wan_i2l (Image to Latents - Wan 2.2) node between the init image and the denoise's `latents` input, honoring the existing denoise_start slider. buildWanGraph: - Drops the txt2img-only guard, branches on generationMode. - img2img: spins up a wan_i2l node and hands it to addImageToImage alongside the existing denoise / l2i / modelLoader (as vaeSource). - inpaint / outpaint still fail loudly — pieces #4-#6. graphBuilderUtils.getDenoisingStartAndEnd: - Adds 'wan' to the simple-linear case (denoising_start = 1 - denoisingStrength). Note: Wan's flow-matching schedule is "sticky" on the init compared to SDXL — users will likely need denoisingStrength ≥ 0.7 to see substantial change, matching the user-found 0.15-0.3 denoising_start sweet spot from earlier img2img testing. We may revisit this with an exponent rescale (like FLUX uses) if the response curve feels off. addImageToImage: - Adds 'wan_i2l' to the i2l-node-type union so the Wan i2l can be threaded through the shared helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): add wan_denoise to addImageToImage/addInpaint/addOutpaint type checks Three sibling graph-helper utilities had the same modern-base list as addTextToImage did, and the buildWanGraph img2img branch tripped one of them at canvas-Generate time: error [generation]: Failed to build graph {name: 'Error', message: 'Wrong assertion encountered'} The else-branch in each helper assumes 'denoise_latents' (the SD1.5/SDXL legacy path) and asserts that — failing for any modern base not listed above the branch. addTextToImage was already updated in Phase 9 piece #1; this catches the parallel cases that the img2img/inpaint/outpaint flows go through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #4 - linear-view inpaint and outpaint branches Wires Wan 2.2 inpaint and outpaint through the existing addInpaint / addOutpaint helpers. The backend's RectifiedFlowInpaintExtension was plumbed into wan_denoise.py back in Phase 8 (commit ab54617173); this just connects the FE. buildWanGraph: - generationMode === 'inpaint' → spin up a wan_i2l, call addInpaint with denoise + l2i + modelLoader (used as both vaeSource and modelLoader since the Wan model loader carries the VAE). - generationMode === 'outpaint' → parallel branch with addOutpaint. addInpaint: - i2l-node-type union now includes 'wan_i2l' (the addImageToImage and addOutpaint type unions already do — different union shapes). metadata.py: - generation_mode literal adds "wan_outpaint" alongside the existing wan_txt2img / wan_img2img / wan_inpaint entries. isMainModelWithoutUnet already includes wan_model_loader (Phase 9 piece create_gradient_mask when Wan is the main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #5 - linear-view I2V branch (raster as reference image) Wan 2.2 I2V-A14B models condition on a reference image whose VAE-encoded latents are concatenated to the noise along the channel dim each step (in_channels=36 on the I2V transformer). In the linear view this maps cleanly onto the existing canvas raster layer: pick an I2V model, drag an image to raster, generate. buildWanGraph: - Fetch the modelConfig early so the variant gate (i2v_a14b vs the rest) can drive the branch shape instead of being a post-hoc check. - I2V + txt2img: fail loudly ("Switch to the canvas tab and drag an image to the raster layer"). I2V models won't produce useful output without a reference, and the backend would crash trying to concatenate a missing condition tensor. - I2V + img2img: pull the raster image via the canvas compositor, wire it through a wan_ref_image_encoder (which VAE-encodes it and builds the 4-mask + 16-latent condition tensor backend-side), then feed the result into denoise.ref_image. Denoise runs from fresh noise (denoising_start=0, no init_latents) — the ref image is cross-attention/concat conditioning, not a noise-trajectory anchor. - I2V + inpaint/outpaint: fail clearly. Combining ref-image conditioning with a denoise mask is conceptually possible but the backend interaction hasn't been validated end-to-end. metadata.py: - Adds "wan_i2v" to the generation_mode literal so the metadata field on I2V renders correctly. T2V flows (txt2img / img2img / inpaint / outpaint) are unchanged for non-I2V Wan variants (T2V-A14B and TI2V-5B). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): enforce multiple-of-16 dimensions to match transformer patch grid Wan 2.2's transformer has ``patch_size=(1, 2, 2)``: it patch-embeds with stride 2 then un-patches by 2. Combined with the VAE's 8x spatial scale, canvas H/W must be a multiple of ``8 * 2 = 16`` — not just 8 — for the patch round-trip to land exactly. Otherwise the latents and noise prediction disagree by one in the spatial dim and the scheduler step fails: RuntimeError: The size of tensor a (147) must match the size of tensor b (146) at non-singleton dimension 3 (here latent_w=147 → patch_w=73 → un-patched_w=146 ≠ 147) This was silent for T2V at 1024x1024 (already a multiple of 16) but fired for I2V at non-multiple-of-16 canvas sizes. Fixes: - ``optimalDimension.getGridSize``: Wan moves from the default 8 case to the multiple-of-16 case (alongside flux / sd-3 / qwen-image / z-image which have the same patch arithmetic). The canvas bbox UI now snaps Wan dimensions to multiples of 16. - ``wan_denoise.py`` and ``wan_ref_image_encoder.py``: bump width/height ``multiple_of`` from 8 to 16. Defense-in-depth — workflow-editor users won't be able to send a non-16-aligned dim either. Existing backend tests (23 passing) still hold — 1024 is divisible by 16 so the test fixtures didn't exercise the off-by-one path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): show negative prompt box in Wan linear-view Wan was missing from SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS, so the linear-view negative-prompt input was hidden even though the Wan denoise node already wires negative conditioning when CFG > 1 (buildWanGraph.ts:67-75). Adds 'wan' to the list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): Phase 9 piece #6 - Wan LoRA collection in linear view Adds Wan LoRA wiring to buildWanGraph, mirroring the Qwen Image pattern. The shared LoRASelect / LoRAList UI in the linear view already filters LoRAs by the selected main model's base, so Wan LoRAs surface automatically when a Wan main is picked — no UI changes needed. addWanLoRAs (new): - Filters state.loras.loras to enabled Wan LoRAs. - For each LoRA: spawns a ``lora_selector`` node and threads it through a single ``collect`` collector. - Routes the collector into a ``wan_lora_collection_loader`` which sits between modelLoader and denoise — modelLoader.transformer → loader, then loader.transformer → denoise (rerouting the original modelLoader → denoise edge). - Emits per-LoRA metadata so PNG metadata + workflow restore work. The dual-expert routing (high-noise vs low-noise vs untagged) is handled entirely on the backend by ``WanLoRACollectionLoader`` based on each LoRA's recorded ``expert`` tag (set by the probe from the filename heuristic in piece #5 of Phase 5). The FE just hands over the bag of LoRAs; no per-list FE plumbing needed. buildWanGraph: - Calls addWanLoRAs(state, g, denoise, modelLoader) after the base transformer edge is in place. The helper is a no-op when no Wan LoRAs are enabled, so it's safe to call unconditionally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(wan): detect LoRA variant and filter by main model Wan 2.2 A14B (inner_dim=5120) and TI2V-5B (inner_dim=3072) LoRAs are not interchangeable — applying one against the wrong main model crashes the layer patcher with a tensor-shape error (e.g. A14B Lightning on TI2V-5B mains produced ``shape '[3072, 3072]' is invalid for input of size 26214400``). Probe Wan LoRAs' inner-dim at install time and record the family on a new ``variant`` field (``a14b`` / ``5b`` / null). The LoRA picker in the linear view hides incompatible variants when the user selects a main, and the graph builder filters any still-enabled mismatches at submit time with a warning. Untagged LoRAs (probe couldn't identify) pass through so they aren't silently hidden. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(wan): ref-image panel, GGUF readiness, and auto-default sources Wan 2.2 I2V now uses the global Reference Images panel (same UX as Qwen Image Edit and FLUX.2 Klein) instead of pulling the conditioning image from a canvas raster layer. Adds: - WanReferenceImageConfig zod type + isWanReferenceImageConfig guard; integrated into the ref-image discriminated union, settings panel, layer hooks, and validators. - 'wan' added to SUPPORTS_REF_IMAGES_BASE_MODELS, but the panel only shows for the i2v_a14b variant (T2V and TI2V-5B don't consume ref images, so the panel is hidden for them). - buildWanGraph I2V branch reads the first enabled wan_reference_image from refImagesSlice; the canvas-raster-as-ref path is removed. I2V now only supports txt2img mode (canvas img2img/inpaint/outpaint assert with a clear message). GGUF Wan readiness check: GGUF mains carry only the transformer, so the loader needs a Diffusers Component Source (or standalone VAE + UMT5-XXL encoder) to resolve the VAE and text encoder. Without one, enqueue is now blocked with a clear reason. The low-noise A14B partner expert remains optional (loader falls back to the high-noise expert when it's missing). Adds standalone Wan VAE and Wan T5 Encoder selectors to the Advanced accordion (Qwen pattern). Wires them as vae_model / wan_t5_encoder_model on the wan_model_loader node — backend priority is standalone > diffusers main > component source. Auto-default on Wan selection (so GGUF users don't have to fiddle with Advanced): when the new main is a Wan GGUF, fill the Component Source, standalone VAE, and standalone T5 encoder with first available matches if not already set. Component Source is matched by variant family (A14B GGUF prefers an A14B Diffusers; TI2V-5B prefers a TI2V-5B Diffusers) since the two families use different VAE channel counts (16 vs 48); within A14B, T2V and I2V share VAE/encoder so they're interchangeable as a source. Runs on every Wan selection (including Diffusers -> GGUF switches), only fills empty slots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 starter models and bundle Wan 2.2 starter pack (selected when the user picks the Wan 2.2 bundle) brings up the minimal-cost path to running A14B T2V end-to-end: - Standalone UMT5-XXL encoder and A14B VAE (so GGUF mains don't need a full Diffusers download for their VAE/encoder sources). - T2V A14B Q4_K_M and Q8_0 GGUF expert pairs (high + low noise). - T2V Lightning V1.1 Seko rank-64 LoRA pair (4-step inference). Additional Wan 2.2 starter models browseable from the model manager: - Full Diffusers T2V A14B, I2V A14B, and TI2V-5B. - I2V A14B Q4_K_M and Q8_0 GGUF expert pairs + Lightning V1 LoRA pair. - TI2V-5B Q4_K_M and Q8_0 GGUFs + the 48-channel TI2V-5B VAE. Each "high noise" GGUF lists its low-noise partner plus the shared VAE and UMT5-XXL encoder as dependencies, so installing one of them pulls in everything the loader needs. QuantStack's HighNoise/LowNoise file naming and lightx2v's high_noise_model/low_noise_model.safetensors are both picked up by the existing filename heuristic in the GGUF probe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> docs(wan): add Wan 2.2 hardware requirements Adds Wan 2.2 A14B (T2V/I2V) and TI2V-5B rows to the hardware requirements table with rough VRAM/RAM guidance per quantization. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): recall low-noise transformer, component source, and standalone VAE/T5 Wan-specific metadata fields embedded by the graph builder (wan_transformer_low_noise, wan_component_source, wan_vae_model, wan_t5_encoder_model, wan_guidance_scale_low_noise) had no recall handlers in features/metadata/parsing.tsx, so recalling an image's parameters would leave these fields empty. Adds a handler for each that dispatches the matching paramsSlice action and renders a row in the metadata viewer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add default Wan 2.2 T2V and I2V workflows Ships two default workflows in the library, tagged so they appear in "Browse Workflows" under the wan2.2 / text to image / image to image tags: - Text to Image - Wan 2.2: full T2V/TI2V-5B graph (model loader, positive + negative encoders, denoise, l2i). Exposes the five model slots, prompts, steps, dual CFG, and dimensions. - Image to Image - Wan 2.2: I2V A14B graph that adds a wan_ref_image_encoder. Exposes the reference image input plus the standard fields. Both follow default-workflow rules: IDs prefixed with default_, meta.category = "default", and no references to user-installed resources. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 1 - backend video storage, records, REST API Adds a parallel video pipeline alongside the existing image pipeline so the gallery can host MP4 alongside PNGs. Implements: - New service modules (parallel to image equivalents): video_records/ record store + sqlite impl video_files/ disk file store (mp4 + first-frame webp thumb) videos/ orchestrating service board_video_records/ board <-> video association - migration_32 creates `videos` and `board_videos` tables - /api/v1/videos/ router: upload, list, get DTO, /full (with HTTP Range so HTML5 <video> seek/scrub works), /thumbnail, /metadata, star/unstar, delete, batch delete, board add/remove - LocalUrlService.get_video_url and SimpleNameService.create_video_name - imageio[ffmpeg] dep for video encode (used in later phases) - Wires all four new services into InvocationServices, dependencies.py, api_app.py, and three test fixtures Verified end-to-end against an in-memory db + tmp output dir: upload, probe, save (file + thumbnail + record), DTO build, list, delete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 2 - polymorphic gallery list endpoint Adds /api/v1/gallery/items/ and /api/v1/gallery/items/names returning a unified time-sorted stream of images + videos so the frontend can render them interleaved with a single virtualized query. - gallery_common: GalleryItem discriminated union (kind + name + shared fields + nullable video duration/fps), GalleryItemRef, names result - gallery_default: SqliteGalleryService implements UNION ALL across the images and videos tables, applying identical filters (origin/category/ is_intermediate/board_id/search) to each half; pagination via outer ORDER BY + LIMIT/OFFSET; counts are summed across the two halves - URLs are resolved at row -> DTO conversion time so each item routes to the correct /api/v1/images or /api/v1/videos endpoint - Wired into InvocationServices, dependencies.py, api_app.py, and the three test fixtures Existing /api/v1/images endpoints are unchanged so any non-gallery consumers (queue, recall, metadata workflows) continue to work as-is. Verified e2e: 2 images + 2 videos inserted in alternating order, both list_items and list_item_names return the correct interleaved order; category filter narrows to a single kind; starring an item bumps it to the top when starred_first=True. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 3 - frontend RTK endpoints + MP4 upload routing Adds the typed API surface and upload integration so videos can be uploaded through the same gallery upload button that handles images. Schema: re-ran pnpm typegen against the running backend to pick up VideoDTO, VideoRecordChanges, GalleryItem, GalleryItemKind, GalleryItemRef, GalleryItemNamesResult and the two new paginated result types. RTK Query (services/api/endpoints/videos.ts) - parallel to images.ts: listVideos, getVideoDTO, getVideoMetadata, getVideoNames, uploadVideo, deleteVideo / deleteVideos, changeVideoIsIntermediate, starVideos / unstarVideos, addVideoToBoard / removeVideoFromBoard. Imperative helpers (getVideoDTO, getVideoDTOSafe, uploadVideo, uploadVideos) and the useVideoDTO convenience hook ride alongside, mirroring the image side. Tag types and invalidation: added Video / VideoList / VideoMetadata / VideoNameList / BoardVideosTotal / GalleryItemList / GalleryItemNameList to the api root. Board-affecting mutations now invalidate the polymorphic gallery list/name caches so videos and images stay coherent once the gallery wiring lands in Phase 4. Added a sibling getTagsToInvalidateForVideoMutation helper. Upload UX: useImageUploadButton.tsx's dropzone now accepts video/mp4, video/webm, video/quicktime alongside the existing image MIMEs. The drop handler splits files into image/video sets and routes each through its own mutation; a new onUploadVideo callback parallels the existing onUpload. Existing image-only callers pass through unchanged. Polymorphic gallery query endpoints + the useGalleryItemDTO hook will land with Phase 4 where they have actual consumers; the schema types they'll need are already in place under @knipignore tags. Verified: pnpm lint (knip, dpdm, eslint, prettier, tsc) all green; pnpm test 1103/1103 pass; live curl against the running dev server uploads an MP4 and serves both the webp thumbnail and the MP4 with a working HTTP Range response (206 + Content-Range). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 4 - mixed gallery grid with video play badge Videos now appear in the same gallery grid as images, interleaved by created_at. Video thumbnails get a centered play-button badge so they read as videos at a glance; everything else (selection, virtualization, search, paged/virtual gallery views, keyboard nav) is unchanged. Approach: selection state stays `string[]` of names. The kind is recovered from the filename extension (.mp4 = video, anything else = image), which is reliable because the backend's SimpleNameService always emits `<uuid>.png` for images and `<uuid>.mp4` for videos. This sidesteps a 32-file cross-cut from changing the selection shape to a discriminated union, and selection is persist-denylisted so no migration is needed. Frontend: - new isVideoName helper in features/gallery/store/types - new endpoints/gallery.ts (deferred from Phase 3): useGetGalleryItemNamesQuery - new ImageGrid/GalleryItemPlayBadge: centered triangular badge over thumbnail - new ImageGrid/GalleryItemVideoStarIconButton: video-typed star toggle - new ImageGrid/GalleryVideoItem: counterpart to GalleryImage; reuses galleryItemContainerSX, GalleryItemSizeBadge (width/height-only stand-in), selection handling (single/shift/ctrl/cmd); alt-click falls through to a normal select since comparison is image-only - use-gallery-image-names now calls the polymorphic gallery names endpoint and exposes a mixed flat name list (existing callers - paged grid, search, navigation hotkeys - get the same shape) - useRangeBasedImageFetching partitions visible names by extension; images bulk-fetch via the existing getImageDTOsByNames mutation, videos dispatch individual getVideoDTO queries (no batch endpoint yet) - GalleryImageGrid's ImageAtPosition dispatches on isVideoName to render GalleryImage or GalleryVideoItem; star hotkey dispatches to the right star/unstar mutation based on kind - pruned the now-unused useGetImageNamesQuery / isImageName exports Verified: pnpm lint (knip, dpdm, eslint, prettier, tsc) all green; pnpm test 1103/1103 pass; live curl of /api/v1/gallery/items returns 57 polymorphic items with video duration populated and image duration null, /api/v1/gallery/items/names returns matching {kind, name} refs. The useGalleryItemDTO hook is intentionally deferred to Phase 5 where the polymorphic viewer is its first real consumer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): Phase 5 - inline video player in the image viewer Selecting a video now renders a polymorphic preview inside the existing viewer panel: thumbnail with a centered play button by default; clicking play swaps in an HTML5 <video controls autoplay>. Switching to a different item drops the video element back to idle (auto-pauses) and selecting an image again returns to the normal image preview. New components (features/gallery/components/ImageViewer/): - VideoPlayButtonOverlay: large centered play button with hover/shadow, used over the thumbnail in the idle state. - CurrentVideoPreview: idle/playing state machine. Resets on video_name change. The <video> src points at /api/v1/videos/i/.../full which supports HTTP Range, so seek/scrub work natively in the browser. New hook: - common/hooks/useGalleryItemDTO: polymorphic DTO resolver that dispatches between useImageDTO and useVideoDTO based on filename extension (isVideoName). Centralizes the kind-dispatch the viewer and toolbar both need. Wiring: - ImageViewer dispatches on galleryItem.kind to render CurrentImagePreview or CurrentVideoPreview. The compare-image DnD drop target is hidden when a video is selected (comparison is image-only). - ImageViewerToolbar hides the image-specific action row (CurrentImageButtons - load workflow, recall metadata, edit, etc.) and the metadata viewer toggle when a video is selected. The general-purpose ToggleProgressButton stays. Out of scope (per the plan): video deletion from the viewer (use gallery hover icons), video-specific metadata viewer, comparison-mode support for videos. Verified: pnpm lint (knip, dpdm, eslint, prettier, tsc) all green; pnpm test 1103/1103 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): accept MP4 (and other video) drops on the fullscreen dropzone The gallery-wide drag-and-drop target lives in FullscreenDropzone, not in useImageUploadButton (which only powers the upload button). It had its own hardcoded image-only zod allowlist that rejected MP4 files with "File type / extension is not supported". - Broaden the zod refines to accept video/mp4, video/webm, video/quicktime, video/x-matroska and the matching extensions - Add isVideoFile helper, split dropped files into image/video sets, and route each set through its own uploader (uploadImages / uploadVideos). Both update their respective RTK caches and invalidate the polymorphic gallery list/names. - Skip the canvas-paste fast-path for single-video drops — the canvas doesn't host videos as layers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): right-click context menu on video items Adds a three-item context menu (delete, change board, download) on right-click / long-press of any gallery video item. Mirrors the image context menu's singleton-portal architecture so re-renders stay cheap. New files: - features/gallery/contexts/VideoDTOContext: small React context that scopes the active video DTO to the menu items (parallels ImageDTOContext). - features/gallery/components/ContextMenu/MenuItems/ ContextMenuItemDeleteVideo: window.confirm + deleteVideo mutation. Videos can't be referenced from canvas/nodes/refs, so the image modal's usage analysis is unnecessary; a one-step confirm matches the "minimal" scope. ContextMenuItemDownloadVideo: reuses the existing useDownloadItem hook against videoDTO.video_url / video_name. ContextMenuItemChangeBoardVideo: dispatches videosToChangeSelected and opens the (now polymorphic) ChangeBoardModal. - features/gallery/components/ContextMenu/VideoContextMenu: singleton pattern lifted from ImageContextMenu — registers gallery video elements via a Map; right-click looks up the target node and opens the menu at the cursor. Extended files: - features/changeBoardModal/store/slice: added video_names alongside image_names plus a videosToChangeSelected action. The two arrays are mutually exclusive — setting one clears the other. - features/changeBoardModal/components/ChangeBoardModal: now dispatches the matching video board mutations (add/removeVideoToBoard, plural endpoints don't exist yet so videos move one at a time — the menu acts on a single selection so this is a one-iteration loop). - features/gallery/components/ImageGrid/GalleryVideoItem: registers itself with useVideoContextMenu. - app/components/GlobalModalIsolator: mounts the singleton. Verified: pnpm lint (knip, dpdm, eslint, prettier, tsc) all green; pnpm test 1103/1103 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): Phase 6 - Wan 2.2 T2V/I2V workflow nodes Adds two new invocation nodes that produce MP4 videos from a Wan 2.2 A14B transformer + VAE, plus the supporting plumbing. New invocations: - WanVideoDenoise (wan_video_denoise) — multi-frame counterpart to WanDenoise. Same per-step logic (CFG, MoE expert swap at the boundary timestep, LoRA patching, scheduler dispatch) — reuses _ExpertSwapper, _resolve_variant, and the scheduler/LoRA helpers from wan_denoise. Difference: the noise tensor has a real temporal dim built from num_frames, and the I2V condition is built across all latent frames (frame 0 conditioned, rest zero). Defaults match the Wan 2.2 reference: 832x480 / 81 frames / 40 steps / CFG 5.0 (high) + 4.0 (low). Inpaint / img2img are out of scope for this first cut. TI2V-5B is rejected; T2V/I2V A14B only. - WanLatentsToVideo (wan_l2v) — VAE-decodes 5D latents to RGB frames via AutoencoderKLWan (T_pixel = (T_lat - 1) * 4 + 1), then encodes an MP4 with imageio[ffmpeg] (libx264, yuv420p for browser compatibility). The temp file is moved into outputs/videos/ via context.videos.save(). Backend shared pieces: - make_noise gains num_latent_frames (default 1, backward compatible). - Added num_latent_frames_for(num_frames, scale=4) helper. - New encode_reference_image_to_video_condition mirrors diffusers' WanImageToVideoPipeline.prepare_latents with last_image=None and expand_timesteps=False: pads the reference image with zero pixel-frames, VAE-encodes the full pseudo-video, normalises, and builds the 4-channel temporal-rearranged first-frame mask. Verified numerically: 21 latent frames for num_frames=81, first latent frame's 4 mask channels = 1, rest = 0. - The existing single-frame encoder is left untouched. Schema / context: - New VideoField primitive (parallel to ImageField) and VideoOutput invocation output (width/height/num_frames/fps/duration/video). - New VideosInterface on InvocationContext with .save(source_path, width, height, duration, fps, ...) returning VideoDTO. Mirrors ImagesInterface — falls back to WithBoard / WithMetadata mixins and embeds the queue item's workflow/graph as a JSON sidecar. - WanRefImageConditioningField now carries num_frames so the denoise nodes can sanity-check the I2V condition. WanRefImageEncoder bumps to v1.1.0 and gains num_frames=1 input (use 81+ for video I2V; the encoder dispatches between the single- and multi-frame helpers). - Image WanDenoise now rejects multi-frame conditions with a clear message pointing at WanVideoDenoise. Verified: pnpm lint (5/5) green; pnpm tests (multiuser auth 122/122 + broader suite via prior runs); numerical shape checks for noise and ref-image condition; end-to-end smoke via VideoService.create. A restart of the InvokeAI server is required to pick up the new invocations in the workflow editor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 T2V and I2V starter video workflows Two new default workflows for the workflow editor 'Browse' modal: - 'Text to Video - Wan 2.2' — model loader -> two text encoders -> wan_video_denoise -> wan_l2v. Exposes prompt, model picks, CFG (high + low), dimensions, frames, fps, and steps. - 'Image to Video - Wan 2.2' — same shape plus a wan_ref_image_encoder feeding the denoise node's ref_image input. Exposes the reference image and the frames field on the ref-image node (must match the denoise node's frames — there is a clear validation error if they diverge, but the starter has them in sync at 81). Both default to the Wan 2.2 reference settings: 832x480, 81 frames @ 16 FPS (~5 s), 40 steps, CFG 5.0 (high expert) + 4.0 (low expert), seeded by a rand_int. Pass the existing _sync_default_workflows validator (id starts with default_, meta.category=default). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): startup crash from stringified VideoOutput annotation run_app.py validates every invocation's return-type annotation against the output-class registry. wan_latents_to_video.py had a stray 'from __future__ import annotations' which made the `invoke()` return annotation a string ('VideoOutput') at runtime. The registry mismatch triggered the unregistered-output warning path, which itself crashed on output_annotation.__name__ because the annotation was a str: AttributeError: 'str' object has no attribute '__name__' The other Wan invocations don't use future annotations — drop the import to match. Verified post-fix: api_app import populates 95 output classes, wan_l2v annotation resolves to the real VideoOutput class and is in the registry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 Lightning T2V starter workflow Same graph as 'Text to Video - Wan 2.2' but with two Apply LoRA - Wan 2.2 nodes chained between the model loader and the denoise node, and defaults retuned for the Lightning distillation: 4 steps and CFG 1.0 on both experts (CFG=1 skips the negative-conditioning forward pass entirely, ~20x faster than the 40-step / CFG-5.0 baseline at similar quality). Adapted from a user-saved workflow; cleaned for distribution by stripping the install-specific model/LoRA key bindings (defaults should not bake in local UUIDs), bumping to a fresh default_-prefixed id with meta.category=default, exposing the two LoRA fields (lora + weight) so users can swap LoRAs without diving into the canvas, and flagging the negative-prompt node as unused at CFG=1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 Lightning T2V and I2V starter workflows Two new default workflows that wire the Lightning LoRA pair into the T2V and I2V video pipelines for a ~20x speedup: - 'Text to Video - Wan 2.2 Lightning' — model loader -> apply LoRA (high) -> apply LoRA (low) -> text encoders -> wan_video_denoise -> wan_l2v. Defaults to 4 steps and CFG 1.0 (no negative branch). Cleaned-up version of Lincoln's saved Lightning workflow: stripped per-install model/LoRA keys, switched meta.category to 'default' with a default_ id, and exposed both LoRA loaders' lora/weight/ target fields so users can swap LoRAs without diving into the canvas. - 'Image to Video - Wan 2.2 Lightning' — same chain plus a wan_ref_image_encoder (v1.1.0 with num_frames) feeding the denoise ref_image input. Defaults match the non-Lightning I2V starter (832x480, 81 frames @ 16 FPS) but with 4 steps / CFG 1.0. LoRA target defaults to 'auto' so properly-tagged Lightning LoRAs route themselves; both workflow descriptions tell users to set explicit 'high'/'low' targets if their LoRAs are untagged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): use FFMPEG plugin (not pyav) for MP4 encode wan_latents_to_video was passing plugin='pyav' to iio.imwrite, but the runtime only has imageio-ffmpeg installed (no PyAV). The encode step at the very end of generation crashed with: ImportError: The `pyav` plugin is not installed. Use `pip install imageio[pyav]` to install it Switch to plugin='FFMPEG' — backed by the bundled imageio-ffmpeg binary that pyproject already requires via imageio[ffmpeg]. libx264 yuv420p is the FFMPEG plugin's default for .mp4, so the explicit pixel_format is dropped (specifying it just produced a "Multiple -pix_fmt options" warning). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): log VAE decode and MP4 encode milestones in wan_l2v The video VAE decode + MP4 encode tail can take 30-90s on top of the denoise loop, and the toast-style signal_progress() messages don't land in the server log. Add context.logger.info() at: - VAE decode start: latent frame count -> pixel frame count + resolution - MP4 encode start: frames, fps, duration, dimensions - MP4 encode complete: encoded file size - Video saved: final video_name Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): switch video thumbnail/probe to imageio[ffmpeg] backend After wan_l2v wrote a successful libx264 MP4 to disk, the invocation would hang in DiskVideoFileStorage.save() during the cv2.VideoCapture thumbnail-extraction step. cv2 wheels on this build can't reliably decode our libx264/yuv420p output (most often the wheel was compiled without an h264 decoder, but the failure mode is silent hang rather than a clear error). The net effect: the MP4 ends up in outputs/videos but the queue item never completes, so the frontend spinner spins forever and the gallery doesn't pick up the new entry. Fix: rewrite extract_video_frame and probe_video to try imageio's FFMPEG plugin first (same backend that did the encoding — so reading our own output is guaranteed to work), with cv2 retained only as a fallback for uploaded videos in formats imageio can't decode. Also add fine-grained log lines + exception guards inside DiskVideoFileStorage.save() so a future thumbnail failure can no longer hang the whole save — it now logs a warning and continues, leaving the video record in place even if the thumbnail step errored. With logging at each step (video written, thumbnail written, sidecar written) any future hang will be obvious from the last log line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): handle VideoField outputs in invocation_complete After wan_l2v wrote its MP4 successfully, the gallery and viewer were never updated: the new video didn't appear and the viewer stayed stuck on the previous "Saving video" progress spinner indefinitely. Root cause: onInvocationComplete.tsx only inspected results for isImageField / isImageFieldCollection. VideoField outputs were silently dropped, so the polymorphic gallery list never invalidated and no auto-switch happened. The viewer therefore kept rendering CurrentImagePreview, whose ImageViewerContext-local $progressEvent / $progressImage atoms intentionally aren't cleared on queue completion when autoSwitch is on — they rely on the new image's DndImage onLoad to clear them, which never fires for a video. Fix: add isVideoField (mirrors isImageField against {video_name}) and plumb video outputs through onInvocationComplete: - getResultVideoDTOs pulls VideoDTOs via getVideoDTOSafe - addVideosToGallery invalidates GalleryItemNameList / GalleryItemList so the polymorphic gallery refetches and the new video shows up - auto-switch dispatches the video name into selection (selection is a polymorphic string[]; useGalleryItemDTO already discriminates by filename extension) The selection change swaps CurrentImagePreview for CurrentVideoPreview, which unmounts the stale progress overlay along with it — so the stuck spinner clears as a side-effect of the auto-switch. Also drops the now-stale @knipignore on getVideoDTOSafe, which has a real consumer now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add 'Frame from Video' invocation Extracts a single frame from a VideoField input and saves it as a regular ImageDTO via context.images.save, so it appears in the gallery like any other generated image. Primary use case is I2V "shot extension": take the last frame of a Wan-generated clip (default frame_index=-1) and feed it back as the reference image for the next clip, then stitch the MP4s to get videos longer than the model's single-shot frame budget at a given VRAM. Negative frame_index is resolved against the actual decoded frame count via probe_video() rather than passed through to imageio — not all imageio plugins handle index=-1 uniformly, and being explicit lets us emit a precise out-of-range error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add 'Concatenate Videos' invocation Joins two or more videos into a single MP4 with one of three transition modes between consecutive clips: - cut: hard splice, no blending. Total length = sum of inputs. - crossfade: linear A→B dissolve over transition_frames. Each boundary consumes N frames from both surrounding clips, shrinking total length by N per boundary. - fade_through_black: A fades to black, then B fades in. Each boundary consumes N/2 from each side and emits N output frames — total length is preserved. Implementation decodes via imageio's FFMPEG plugin (matching wan_l2v on the encode side) and runs the blends in numpy. All decoded frames are kept in memory at once; fine for the few-hundred-frame I2V chains that motivated this, would want streaming if anyone ever feeds in hour-long uploads. Up-front validation enforces matching dimensions across inputs and checks that each clip has enough frames to spare from its head and tail for the requested transitions — saves a wasted decode pass when the transition window is too wide for one of the clips. Pairs with 'Frame from Video' for I2V shot extension: generate N clips chained via last-frame-as-ref-image, then glue them with a crossfade. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): show full-resolution first frame in viewer The viewer used a chakra <Image src={thumbnail_url}> in the idle (not- playing) state, so once a clip auto-selected after generation the preview snapped from the full-resolution denoise progress image to the small WebP gallery thumbnail upscaled to fit — visibly soft compared to what the user was watching seconds earlier. Switch to a single <video> element that spans both states: - idle: muted, no controls, preload="metadata". With no `poster` attr the browser decodes and shows the video's actual first frame at full resolution (this is the documented HTMLVideoElement default). - playing: same DOM node with controls+audio toggled on, kicked off via ref.play(). No reload between states — the decoded buffer carries over. `key={videoName}` swaps the element cleanly when the user moves to a different clip, dropping any in-progress playback state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(nodes): show 'Save in gallery' on video-output nodes The footer checkbox was gated on useNodeHasImageOutput, which only matched ImageField outputs. wan_l2v and video_concat produce VideoField and so had no toggle — users had no UI path to flip is_intermediate on them, even though VideoOutput goes through context.videos.save and lands in the gallery the same way ImageOutput does. Rename the hook to useNodeHasGalleryOutput and extend it to match VideoField as well. Update the three call sites (the hook itself, the checkbox, and the footer wrapper) so the toggle and the footer render whenever a node produces something destined for the gallery. The image primitive ('image' type) is still excluded since it doesn't save a new image; no equivalent video primitive exists yet, so no analogous exclusion is needed for VideoField. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: remove unwanted planning documents * chore: fix ruff I001 import-order violations Auto-fix from `ruff check --select I001 --fix`. Touches 10 files across the Wan and videos changes where added imports landed out of order. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): restrict uploads to MP4 only The upload allowlist previously included .mov/.webm/.mkv, but the names service (create_video_name) unconditionally emits {uuid}.mp4 and we don't transcode on upload. The result: non-MP4 containers were stored under a .mp4 name and served with the .mp4 MIME type, which silently broke playback in browsers when the container didn't match. Drop the non-MP4 extensions from ACCEPTED_VIDEO_EXTENSIONS and tighten the accepted MIME prefix to "video/mp4". Wan-generated output is MP4 anyway, so this matches current reality. If we want to support more containers later, the right move is to extend the names service to preserve the source extension, then re-add the formats here. Also drops the now-dead suffix-detection block in upload_video and the os import it required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(videos): clean up stale @knipignore on consumed hooks useDeleteVideoMutation, useAddVideoToBoardMutation, and useRemoveVideoFromBoardMutation are now consumed by Phase 4 components (context menu, change-board modal) but were still annotated with the multi-phase @knipignore tag — that generated false-positive knip warnings and misrepresented the implementation status. Move those three into the unconditional export block. The remaining five hooks (useListVideosQuery, useGetVideoMetadataQuery, useGetVideoNamesQuery, useDeleteVideosMutation, useChangeVideoIsIntermediateMutation) are still unused in the current codebase and stay under a narrower @knipignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(videos): document invalidate/select race in addVideosToGallery The video gallery path uses tag invalidation rather than an optimistic insert (the image path's `insertImageIntoNamesResult` doesn't have a polymorphic equivalent yet). Because invalidation kicks off an async refetch, the `imageSelected` dispatch below it fires before the new video name is in `imageNames`, so the gallery grid's `useKeepSelectedImageInView` no-ops on its first pass. The scroll self-corrects on the next pass when the refetch lands and the `imageNames` dep updates. The user-visible effect is just a small lag on gallery scroll-to- selection — the viewer selection applies immediately — so this is a documented limitation rather than a bug. Worth a follow-up if the lag becomes noticeable in practice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add Video Primitive invocation and VideoField input UI Mirrors the Image Primitive flow end-to-end for videos. Users can now drag a video from the gallery onto a "Video Primitive" node and feed its output into downstream nodes like Frame from Video or Concatenate Videos — exactly the way Image Primitive feeds the rest of the image pipeline. Backend (invokeai/app/invocations/primitives.py): - New VideoInvocation, declared *after* VideoOutput so the return annotation is a real class (not a forward-ref string). Stringified output annotations crashed startup before — see cac366229a — so the ordering matters. Frontend: - Register VideoField as a stateful field type in types/field.ts: zVideoFieldType, zVideoFieldValue, zVideoFieldInputInstance/Template, output template + type guards, plus entries in the four stateful unions (FieldType, FieldValue, InputInstance, InputTemplate). - buildFieldInputTemplate / buildFieldInputInstance gain VideoField branches so OpenAPI-derived templates resolve correctly. - nodesSlice: fieldVideoValueChanged reducer + export. - imageActions/actions.ts: setNodeVideoFieldVideo helper. - dnd.ts: singleVideoDndSource + setNodeVideoFieldVideoDndTarget, wired into the dndTargets array. - GalleryVideoItem: register itself as a drag source so videos in the gallery actually drag (previously they were click-only). - VideoFieldInputComponent: parallel to ImageFieldInputComponent — shows the WebP thumbnail with a dimensions badge, accepts video DnD, drops stale references on reconnect if the underlying video was deleted. - InputFieldRenderer: dispatch VideoField templates to the new component (placed right after the ImageField branch). - useNodeHasGalleryOutput: also exclude the new `video` primitive type so the "Save in gallery" toggle does not render on the pass-through node (same treatment the `image` primitive already gets). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> fix(video): allow video drops to reach DnD target handlers useDndMonitor is the global drop monitor that actually invokes each target's handler() — DndDropTarget only does enter/leave bookkeeping. Its canMonitor gate explicitly allowlists source types and only listed singleImageDndSource + multipleImageDndSource. So when a video was dragged from the gallery onto a VideoField input, the drop was visible to the DOM but the monitor silently filtered it out, the handler never ran, and fieldVideoValueChanged was never dispatched. Add singleVideoDndSource to the allowlist. Dropping a video onto a Video Primitive (or any other VideoField input) now wires the asset into the field as intended. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(backend): ruff * chore(frontend): typegen * feat(wan): split Wan 2.2 starter bundle into T2V and I2V Replaces the single ~63 GB Wan 2.2 bundle with two smaller bundles so users only pay for the capability they need. T2V (~36 GB) covers text-to-video plus a low-VRAM image-to-video option via TI2V-5B; I2V (~32 GB) adds the heavier I2V-A14B path. Drops the Q8 T2V pair from the default bundle — both Q8 variants and full Diffusers builds remain available as a-la-carte starters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): tighten multiuser isolation in list and board-move endpoints Three related fixes flagged in code review (PR #9163, JPPhoto): 1. Video and gallery list/name SQL paths only filtered by user_id when board_id was the literal "none" sentinel. When the URL parameter was omitted entirely, no user filter applied and non-admin callers could enumerate every user's videos / mixed gallery items. Added an explicit per-user isolation branch for the omitted case. 2. /v1/videos/ and /v1/videos/names accepted explicit board IDs with no read-access check; the route now mirrors the images and gallery routers and calls _assert_board_read_access for non-"none" values. 3. add_video_to_board and remove_video_from_board only validated video ownership, not destination/source board write access — a caller could move their video into someone else's private board. Added _assert_board_write_access and a strict _assert_video_direct_owner helper (no board-owner / public-board fallback) for board-move ops, mirroring _assert_image_direct_owner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(boards): cascade video deletion when deleting a board with media Previously delete_board only handled images. With include_images=true the backend would delete images on the board but the videos would silently cascade out of board_videos and survive as uncategorized records — almost certainly not what the caller intended. Without include_images, the same mismatch meant the response could not report affected videos. Now: - include_images=true also calls videos.delete_videos_on_board - include_images=false collects the soon-to-be-uncategorized video names - DeleteBoardResult gains deleted_board_videos and deleted_videos fields (default empty so existing clients are unaffected) Frontend deleteBoard / deleteBoardAndImages mutations gain the matching VideoList / VideoNameList / GalleryItem* tag invalidations so the polymorphic gallery and video list views refresh. Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): return affected_boards from board move/remove endpoints removeVideoFromBoard previously returned VideoDTO; the frontend then read result.board_id (null after removal) and only invalidated the 'none' board cache — the previous board's list stayed stale until refetch. addVideoToBoard had the same problem (the route never knew the source board, so the old-board cache was never invalidated). Mirror the image equivalents (board_images.py): the routes now return AddVideosToBoardResult / RemoveVideosFromBoardResult with the moved video name(s) and the full set of affected board IDs. Both old and new boards get invalidated atomically. Frontend mutations updated to consume the new shape via getTagsToInvalidateForBoardAffectingMutation on result.affected_boards. The auto-generated schema.ts will need a typegen pass after the dev server restart to pick up the new response types. Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): stream uploads, FileResponse for full video + thumbnail Three perf items from the code review (PR #9163, JPPhoto): - upload_video read the entire UploadFile into a Python bytes object before writing to the temp file. Multi-GB videos allocated multi-GB buffers. Now chunk-stream into the temp file with a 1 GB per-upload cap (HTTP 413 on overflow). Cap is intentionally generous — the goal is RAM-exhaustion protection, not content policy. - get_video_full read the whole MP4 into RAM when no Range header was present. Browsers usually send Range, but curl / direct downloads / CDN edge fetches do not, and a multi-GB load per such request is a trivial DoS vector. Replaced with FileResponse (sendfile). - get_video_thumbnail similarly buffered the WebP. Thumbnails are tiny so this was minor, but FileResponse is idiomatic and shaves the unnecessary copy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): localize video UI strings - Add gallery.deleteVideo_one / deleteVideo_other, deleteVideoConfirmation, and playVideo to en.json - ContextMenuItemDeleteVideo: drop the inline English defaultValue (the translation key now exists) and use gallery.deleteVideo for aria/tooltip (was reusing gallery.deleteImage so it rendered "Delete Image") - VideoPlayButtonOverlay: replace the hardcoded "Play video" aria with t('gallery.playVideo') Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(video-invocations): exact frame counts and odd-tf transitions video_frame_extract: resolving frame_index=-1 previously computed n_frames as round(duration * fps). For VFR uploads or containers with approximate metadata that can overshoot the actual decoded frame count, making the last-frame extraction fail. Use iio.improps(plugin='FFMPEG') for the exact decoder count when available; fall back to the duration * fps estimate only if the props query fails. video_concat fade_through_black: with an odd transition_frames the symmetric half = tf // 2 split emitted tf - 1 frames per boundary, violating the documented "emits transition_frames" contract. Split asymmetrically (tail_half = tf // 2, head_half = tf - tail_half) so the emitted count equals tf exactly for both even and odd values. Validation and docstring updated to match. Verified with manual cases: tf=1, tf=4, tf=5 all emit the documented total length for two 10-frame inputs. Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(gallery): describe video gallery items, upload, and deletion Update the Gallery Panel docs to reflect the polymorphic gallery added in the Wan 2.2 video feature branch: - Gallery intro now mentions images + videos coexist on boards. - Board deletion warning clarified to cover both kinds of media. - New "Videos in the Gallery" section covering: how video items appear (first-frame thumbnail + play badge), MP4-only upload constraint with the typical re-encode command, the video context menu, and that videos count toward board totals. Reported in code review (PR #9163, JPPhoto). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(videos): regression coverage for PR #9163 review fixes Adds tests that pin the behaviour fixed in the JPPhoto review and would catch a recurrence: - tests/app/services/video_records/test_video_records_sqlite.py get_many / get_video_names: non-admin callers only see their own videos when board_id is omitted; admins see all; the "none" branch still filters by user. - tests/app/services/gallery/test_gallery_default.py Same multiuser isolation guarantee through the polymorphic gallery union for both images and videos. - tests/app/routers/test_videos_multiuser.py /v1/videos/ and /v1/videos/names: 403 when a non-owner passes an explicit private board_id; 200 for owners, admins, "none", and omitted board_id (the auth-required smoke tests pin the 401 paths too). - tests/app/routers/test_boards_multiuser.py Adds two delete-board cases proving the video cascade: include_images invokes delete_videos_on_board and reports deleted_videos; the no-include path reports deleted_board_videos without calling the destructive service. Existing fixture extended to stub the video services that the new route logic now touches. - tests/app/invocations/test_video_concat.py Parametric coverage that fade_through_black emits exactly tf frames for both even and odd tf, plus three-clip chains, plus the crossfade and cut/zero-tf cases as guards. - tests/app/invocations/test_video_frame_extract.py _decoder_frame_count returns the exact count via the cv2 fallback for several clip lengths and gracefully returns None for missing / non-video inputs (caller falls back to duration * fps). Bug found during test authoring: _decoder_frame_count over-flowed int() on iio's "inf" nframes for libx264 streams, and improps never returns a real count for that codec anyway. Helper now ignores non-finite shapes and falls back to cv2's CAP_PROP_FRAME_COUNT, which gives the exact value for libx264. schema.ts regenerated to pick up the AddVideosToBoardResult / RemoveVideosFromBoardResult / extended DeleteBoardResult types added in earlier commits in this series. All 70 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): add 'Wan 2.2 I2V Ideal Dimensions' invocation Computes Wan I2V-compatible (width, height) for a source W×H at a target short-side resolution (e.g. 720 for "720p"), snapping each output to a multiple of 16 (Wan's transformer patch_size × VAE 8x pixel-grid constraint enforced by wan_ref_image_encoder). Replaces the 6-node math chain (Float Math × 4 + Float To Integer × 2) that was otherwise required to compute these dimensions from an arbitrary input image. Wire the Image Primitive's width/height outputs into this node, and feed its (width, height) outputs into both wan_ref_image_encoder and wan_denoise (they must match). Three rounding modes: - nearest (default): minimizes aspect-ratio drift - floor: guaranteed not to exceed unsnapped target (safer for VRAM) - ceiling: rounds up Output schema reuses IdealSizeOutput so it slots into existing pipes that already consume Ideal Size — SD1.5, SDXL. Includes regression tests covering the documented common-case table, all three rounding modes, postcondition invariants (multiple of 16, aspect ratio within 1.2%, never zero), and input validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): swap target_short_side int for 480p/720p/1080p preset dropdown Wan 2.2 was trained at 480p and 720p; a free integer encouraged users to pick noncanonical short sides that the model handles poorly. Replace the int field with a Literal dropdown of "480p" / "720p" / "1080p" (via ui_choice_labels) so the UI surfaces the canonical choices. 1080p is included with a label noting it's extrapolated from training (not a Wan native size) — useful for users with VRAM headroom but shouldn't be the default. Version bumped to 1.1.0 since the field schema changed (the node was only committed locally; no published workflow needs migrating). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): empty_cache around the I2V reference-image VAE encode Two-sided fix to avoid VRAM allocator fragmentation that was causing the subsequent denoise-transformer partial load to OOM: - Before vae.encode(): clears blocks left over from earlier nodes (the denoise expert swap especially leaves the cache fragmented). - After the condition tensor is on CPU: returns the VAE encode's intermediates so the next partial_load_to_vram sees a real free contiguous range. Mirrors the same pattern in wan_latents_to_image.py and wan_latents_to_video.py — those are the existing precedent. The cost is a handful of microseconds per encoder invocation and only the cache state is touched; model weights stay resident. Observed-by symptom from a workflow review: at encoder=480x720 and a source image of 880x1184, the encoder ran fine but the I2V high-noise expert failed to partial-load with a cryptic CUDA OOM at _load_state_dict_with_fast_device_conversion. Pre-resizing the source to 80% incidentally cleared the allocator state and let the run succeed; this fix removes the incidental dependency on source size. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): support TI2V-5B in the video denoise node (T2V mode) The video denoise node previously hard-errored on TI2V-5B with "not supported." Most of the surrounding machinery (variant-aware spatial scale, variant-aware scheduler, single-expert ExpertSwapper path) was already in place — the gate just needed lifting and the hard-coded A14B latent channel count needed to follow the variant. Changes: - Drop the upfront "TI2V-5B is not supported" raise. - Use get_default_latent_channels(variant) so latents are 48-channel for TI2V-5B and 16-channel for the A14B family (matches the image denoise node's existing logic). - For TI2V-5B with a Reference Image input, raise a sharper, accurate error that explains TI2V-5B's I2V uses diffusers' expand_timesteps path (first-frame-mask blend + per-position timestep gating) which this node does not implement yet — pointing the user at the working T2V path or the I2V-A14B model. - Update the transformer field description to reflect what's now supported. Image-to-video with TI2V-5B remains a follow-up; the conditioning math is genuinely different from A14B (no 36-channel concat) and warrants a separate code path rather than parameterising this one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): instantiate TI2V-5B VAE with the right architectural config The single-file Wan VAE loader was always calling ``AutoencoderKLWan(z_dim=config.latent_channels)`` and relying on diffusers' constructor defaults for every other parameter — but those defaults match the Wan 2.1 / A14B VAE (base_dim=96, in/out=3, 8x spatial, no patchify). For TI2V-5B's Wan 2.2-VAE the architecture is materially different: - base_dim=160, decoder_base_dim=256 - in_channels=12, out_channels=12 (3 RGB x 2x2 patch) - patch_size=2 - scale_factor_spatial=16 - is_residual=True - 48-vector latents_mean / latents_std (required for the model's encode/decode normalisation to produce non-garbage outputs) Loading the TI2V-5B VAE state_dict into the default-constructed model failed with shape mismatches throughout the encoder + decoder, surfaced in wan_l2v as "Error(s) in loading state_dict for AutoencoderKLWan." This commit routes z_dim=48 to a verbatim copy of the TI2V-5B VAE config (from vae/config.json in Wan-AI/Wan2.2-TI2V-5B-Diffusers); z_dim=16 keeps the previous A14B / Wan 2.1 default behaviour. Verified end-to-end: both kwargs construct cleanly and produce the expected layer shapes (decoder.conv_out emits 12 channels for TI2V-5B, 3 channels for A14B). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): variant-aware default scheduler for standalone installs When the main model has no on-disk ``scheduler/`` directory (every standalone GGUF / single-file install), ``_build_scheduler`` previously fell back to ``FlowMatchEulerDiscreteScheduler()`` for every variant. That's correct for the A14B family but wrong for TI2V-5B, which ships ``UniPCMultistepScheduler`` with ``flow_shift=5.0`` + ``prediction_type="flow_prediction"`` + ``use_flow_sigmas=True``. The mismatch produces drifty samples on TI2V-5B. Add a ``_default_scheduler_for_variant`` helper that reconstructs the right scheduler from the variant tag (values verbatim from each variant's ``scheduler/scheduler_config.json`` in the matching Wan-AI/Wan2.2-*-Diffusers repo). The on-disk-config-present path is unchanged — if the model ships a scheduler dir, that wins. Full scheduler-selection UI is deferred to a future PR per discussion; this special-case keeps the standalone TI2V-5B path producing the right sampler without surfacing a new field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wan): TI2V-5B image-to-video support TI2V-5B I2V uses a fundamentally different conditioning scheme from A14B I2V. Implement diffusers' ``expand_timesteps`` path so the same ``Reference Image - Wan 2.2`` node and ``Denoise Video - Wan 2.2`` node work for both variants, dispatched by VAE z_dim / transformer variant. Encoder side (wan_ref_image_extension.py / wan_ref_image_encoder.py) - Add ``encode_reference_image_to_ti2v_condition`` that VAE-encodes a single image frame to ``[1, 48, 1, H/16, W/16]`` with the Wan2.2-VAE normalisation, no mask channels. - ``WanRefImageEncoderInvocation`` dispatches on ``vae.config.z_dim``: z_dim=48 → TI2V-5B path, z_dim=16 → existing A14B path. - Enforce ``multiple_of=32`` for width/height in the TI2V-5B case (16x VAE * 2 transformer patch = pixel dims must divide by 32) with a clear error message pointing at the constraint. Denoise side (wan_video_denoise.py) - Replace the "TI2V-5B I2V not supported" raise with a variant-aware dispatch on ``ref_condition.shape`` and ``variant``. - For TI2V-5B I2V build a ``first_frame_mask`` once (0 at frame 0, 1 elsewhere). At each step: latent_model_input = (1 - mask) * condition + mask * latents temp_ts = (mask[0,0,:,::2,::2] * t).flatten() timestep = temp_ts.unsqueeze(0).expand(B, -1) Per-token timesteps gate the model: frame 0 sees t=0 (locked to condition), other frames see t (normal denoise). - After the denoise loop, re-clamp frame 0 to the clean condition so the locked first frame doesn't show scheduler drift in the final VAE decode. Mirrors WanImageToVideoPipeline:813-814. - Skip the encoder-num_frames-must-match check for TI2V-5B (its condition is always single-frame regardless of output length). Tests - Three new tests on encode_reference_image_to_ti2v_condition covering output shape at small and Wan-realistic dims plus the no-mask-channels invariant. Full video-denoise integration tests would need a new fixture stack (none exist for wan_video_denoise yet) — deferred. A14B I2V is unchanged. TI2V-5B T2V (added in the previous commit) is unchanged. Verified at the import + encoder-shape level; end-to-end verification requires a TI2V-5B I2V workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): show denoise progress overlay over the video viewer CurrentVideoPreview rendered only the <video> element, so when the last-selected gallery item was a video, a freshly-started render's denoise preview images had nowhere to display — the user saw the static first-frame still of the previously-loaded video until the new render's final video swapped in. Mirror CurrentImagePreview's progress-overlay pattern: subscribe to $progressImage / $progressEvent, gate on selectShouldShowProgressInViewer, and render a ProgressImage stack on top of the video when a render is in progress. Hide the play-button overlay while progress is showing so it doesn't sit on top of the preview. Reported by Lincoln during TI2V-5B testing: previews started working after restarting the server only because there was no video loaded at that point; once a video was selected, the previews silently dropped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(lint): apply ruff format + isort across recent Wan video work ruff check found one I001 (import order) in ``invokeai/backend/model_manager/load/model_loaders/vae.py`` and ruff format flagged five files. All cosmetic; no behaviour changes. - vae.py: import reorder - video_concat.py: minor reflow - test_wan_ideal_dimensions.py / test_boards_multiuser.py / test_videos_multiuser.py: prettier-style wrapping Verified: full ruff check + ruff format --check clean, 141 backend tests pass, and ``pnpm lint`` (knip + dpdm + eslint + prettier + tsc) all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(features): add user guide for Wan 2.2 video generation Comprehensive guide covering: - The three Wan 2.2 variants (T2V-A14B, I2V-A14B, TI2V-5B), their conditioning differences, and the dual-expert MoE explanation - Lightning LoRA distillation for 4-step A14B inference - Starter bundles (Text-to-Video and Image-to-Video splits) - Workflow setup for T2V and I2V with the constraint matrix: * frame count: (num_frames - 1) % 4 == 0 * pixel dims: multiple of 16 for A14B, 32 for TI2V-5B * encoder + denoise must agree on width/height - The chain-and-concat trick for making longer videos, with the bridge-frame degradation mitigations - Troubleshooting: OOM, late-frame artifacts, dim mismatches, VAE load errors, scheduler issues, preview-not-appearing, MP4 glitches Lands under Features → Video Generation (experimental). Astro auto-generates the sidebar from features/ so no nav config change needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(nodes): skip image-DTO fetch for videos in Current Image node CurrentImageNode unconditionally called useImageDTO(lastSelectedItem) even when the selected gallery item was a video, firing GET /api/v1/images/i/<uuid>.mp4 on every video thumbnail click. The endpoint 404s and the backend logged "Image record not found" each time — benign but noisy. Apply the same null-skip pattern useGalleryItemDTO uses: pass the name only when it's not a video, so RTK Query skips the request for video selections. Current Image is image-only by design, so videos rendering the empty fallback matches existing behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(videos): clear stale progress overlay + force first-frame paint Two viewer bugs after auto-switching to a freshly-rendered video: - The denoise progress overlay never cleared. CurrentImagePreview clears the ImageViewerContext $progressImage/$progressEvent atoms via DndImage's onLoad callback; the video viewer had no analog, so the last progress still sat on top of the new video forever — clicking other video thumbnails did nothing visible, and only selecting an image (which fires onLoadImage via DndImage) cleared it. - Even with the overlay gone, the <video> element rendered its black background instead of the first frame. preload="metadata" loads dimensions/duration but doesn't guarantee a decoded first frame on all browsers; an explicit seek is needed to force a paint. Wire onLoadedMetadata to (1) call onLoadImage() — mirroring DndImage's onLoad — and (2) nudge currentTime to 0.0001 so the decoder paints the first frame without measurably advancing playback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(hotkeys): skip image-DTO fetch for videos in GlobalImageHotkeys Companion to a3bdc3304e (CurrentImageNode). GlobalImageHotkeys is a mounted-everywhere singleton that wires recall hotkeys (seed, prompts, remix, etc.) to whatever item is currently selected. It was passing the raw selection name through to useImageDTO unconditionally, so every video thumbnail click fired GET /api/v1/images/i/<uuid>.mp4 → 404 and the "Image record not found" log line. Gate on isVideoName(), mirroring the polymorphic null-skip pattern in useGalleryItemDTO. Recall hotkeys don't apply to videos anyway, so this just suppresses the noise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): empty CUDA cache between A14B expert swaps The dual-expert swapper releases the active expert via its context manager exit, but PyTorch's caching allocator retains the freed blocks as reserved-not-yet-claimable space until empty_cache runs. The next partial_load_to_vram for the incoming expert then sees a fragmented free pool and offloads layers it could otherwise have kept on device. Users running A14B observed the low-noise expert ending up far more CPU-resident than the high-noise one on otherwise identical settings — that was the leftover reservation from the high-noise expert masking real free VRAM. Call TorchDevice.empty_cache() between the release and the next load. Same pattern as the VAE-encode fix earlier in this branch. Regression test in test_wan_expert_swapper.py mocks empty_cache and asserts it fires on every actual swap but not on a same-label re-get. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): allow drag-and-drop to change a video's board Dropping a video thumbnail onto a board in the boards list was a no-op (the dnd target only accepted image sources). Extend addImageToBoardDndTarget and removeImageFromBoardDndTarget to also accept SingleVideoDndSourceData and dispatch the corresponding video mutations. Permission UX mirrors the image path: - Same canMoveFromSourceBoard gate (owner / public source board) - Same "do nothing if dropping on the current board" early-out Backend enforcement on /api/v1/videos/board already mirrors the image endpoints — _assert_board_write_access on the destination plus _assert_video_direct_owner on the video. The frontend gate intentionally mirrors only the source-board part of that, leaving the direct-owner check to surface as a 403 on attempt (same compromise as images, where the client doesn't have per-item owner info to gate cleanly). Multi-video drag is not supported yet (the gallery only registers a single-video draggable per item, no multi-select bundle), so this only wires the SingleVideoDndSourceData path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(wan): force outgoing A14B expert off GPU on swap The previous empty_cache() fix (53b2f4d4c7) was insufficient. unlock() only decrements the cache record's lock counter — the weights stay on GPU until the cache's automatic offload decides to free them on the next lock(). That heuristic uses ``torch.cuda.memory_allocated() - working_mem`` to estimate free space, which under-frees when the previous denoise step's workspace activations are still allocated alongside the just-unlocked expert. The user-visible symptom was a log line like Loaded model '...:transformer' onto cuda device in 0.37s. Total model size: 9203.13MB, VRAM: 2381.18MB (25.9%) for the incoming low-noise expert, while the high-noise expert continued to hold ~9 GB of VRAM. The swapper now stashes the LoadedModel info handle and, on each swap, explicitly invokes ``cached_model.full_unload_from_vram()`` on the outgoing expert before locking the incoming one. This sidesteps the heuristic and guarantees the previous expert's weights leave GPU before partial_load_to_vram measures available room. The access path ``info._cache_record.cached_model`` reaches into a private attribute — there is no public LoadedModel API for "unload from VRAM but keep in RAM" today, and a broader backend refactor felt out of scope. The call is wrapped in getattr/try-except and pinned by a regression test so a future refactor breaks the test, not the swap. Tests: - Updated existing dual-expert lifecycle test to expect the new full-unload step in the swap log sequence. - New test_outgoing_expert_force_unloaded_from_vram covers the per-swap behavior (outgoing only, no initial unload). - New test_force_unload_failure_does_not_break_swap pins the defensive fallback so swap reliability survives a future LoadedModel refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gallery): restore shift/ctrl-click range selection in image grid GalleryImage's modifier-key click handler was reading the legacy imagesApi getImageNames cache to compute range-selection indices, but the gallery grid was switched to the polymorphic galleryApi getGalleryItemNames endpoint (the only source that includes videos). The legacy cache is no longer populated for the grid, so the ordered-name list came back empty and the handler fell into its "no names cached" early-return: if (imageNames.length === 0) { if (!shiftKey && !ctrlKey && !metaKey && !altKey) { dispatch(selectionChanged([imageName])); } return; } making shift- and ctrl-click no-ops. GalleryVideoItem already had the correct reader inlined as a private helper. Hoist it to a shared module (features/gallery/store/selectCachedGalleryItemNames) so both grids use the polymorphic cache, and update GalleryImage to call it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(typegen): regenerate schema.ts Refresh of the OpenAPI-derived TypeScript bindings against the current backend. No hand edits — this is the output of the typegen step re-run against the Wan video routes and recent backend changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(startup): silence HF tokenizers fork-after-parallelism warning Set TOKENIZERS_PARALLELISM=false at startup (via os.environ.setdefault so users can override) before any HF library is imported. The Rust ``tokenizers`` library warms a thread pool the first time a tokenizer runs — for us that's UMT5 / T5 text encoding during Wan / FLUX / SD3 conditioning. Every subsequent fork() then logs huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks... In video generation we fork on every MP4 encode (imageio's FFMPEG plugin uses subprocess.Popen → fork+exec), so this warning lands once per generation in the server log. The advisory is benign — the child correctly falls back to single-threaded tokenization before exec(), and the parent's thread pool is unaffected — but the noise obscures real warnings. Setting the env var before any HF import prevents the thread pool from warming up at all, so the fork detector stays quiet without sacrificing anything: tokenization happens once per generation and isn't a hot path for us. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(startup): hoist TOKENIZERS_PARALLELISM=false to module level Follow-up to 2106f10ec4 — the previous attempt set the env var inside ``run_app()``, which races against any transitive HF import triggered by the console-script's from invokeai.app.run_app import run_app If ``tokenizers`` is imported anywhere in that import chain (directly or via diffusers/transformers re-exports), the library's fork detector registers before our setdefault runs and the warning still fires. Move the setdefault to module level so it executes the instant ``run_app.py`` is loaded — i.e. before the function defs are even parsed, and well before any HF library has a chance to import. Note for testing: jurigged hot-reload only re-runs function bodies, so picking up this fix requires a full server restart, not just a file save under ``--dev-reload``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(videos): replace window.confirm with ConfirmationAlertDialog Use the in-app delete-confirmation dialog (the same Chakra ConfirmationAlertDialog the image flow uses) instead of the browser's window.confirm() prompt. Matches the visual + interaction language of the rest of the gallery and picks up the shared ``shouldConfirmOnDelete`` system preference — flipping the "Don't ask me again" toggle now silences the prompt for both images and videos. Implementation mirrors features/deleteImageModal/ but trimmed: the image dialog computes "usage" (canvas layers, node fields, reference images, upscale source) so the user knows what they'll break. Videos have no analogous attachment points, so the video state machine is a straight confirm-then-delete with no usage analysis. - features/deleteVideoModal/store/state.ts — nanostores atom + an awaitable ``deleteVideosWithDialog`` that opens the dialog and resolves/rejects on confirm/cancel. Skips the dialog entirely when shouldConfirmOnDelete is off. - features/deleteVideoModal/components/DeleteVideoModal.tsx — ConfirmationAlertDialog with the new deleteVideoPermanent message and the shared "Don't ask me again" switch. - GlobalModalIsolator.tsx — mount the new modal alongside DeleteImageModal. - ContextMenuItemDeleteVideo.tsx — call useDeleteVideoModalApi().delete instead of window.confirm + useDeleteVideoMutation. - en.json — added gallery.deleteVideoPermanent, dropped the now-unused gallery.deleteVideoConfirmation. - videos.ts — useDeleteVideoMutation moves into the @knipignore export group since the only call site now uses videosApi.endpoints.deleteVideo.initiate via the modal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gallery): refetch polymorphic gallery cache on image completion The gallery grid subscribes to the polymorphic ``getGalleryItemNames`` RTK Query endpoint (so images and videos interleave by created_at). But ``onInvocationComplete``'s image path only did an optimistic insert into the image-only ``getImageNames`` cache, leaving the polymorphic cache stale — a freshly-generated image landed correctly in board totals and the per-DTO cache, but never showed up in the grid until the user reloaded the page. Mirror the videos path (which has invalidated these tags since the polymorphic endpoint was introduced) and dispatch ``galleryApi.util.invalidateTags(['GalleryItemNameList', 'GalleryItemList'])`` after image outputs are processed. The cost is one extra HTTP round-trip per generation; a future optimization could optimistically splice the new entry into the polymorphic shape, but that requires a different ``insertImageIntoNamesResult`` for the ``GetGalleryItemNamesResult`` shape and is a bigger change. Regression test in onInvocationComplete.test.ts pins the behavior: verifies the invalidation fires on a fake image complete event, and verifies it does NOT fire for denylisted passthrough node types (load_image, image). Confirmed test correctly fails when the fix is reverted via git stash. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: address 2nd-pass code review findings Self-review pass before re-pinging external reviewers. Five fixes; the three medium ones have user-visible consequences, the two low ones are guard + docstring. 1. videos.py: delete_video no longer swallows service errors into a misleading HTTP 200. Missing DTO -> 404, delete failure -> 500. The prior shape returned 200 with an empty deleted_videos list, which the frontend treated as success, dropped from cache, and left the video on disk — silent data-consistency failure visible only on next page reload. 2. videos.ts: starVideos / unstarVideos invalidate the LIST_TAG-scoped { type: 'VideoList' } entry alongside the per-video and board-affecting tags. Without this, starred_first=true gallery queries kept the just-starred video in its old position until the next list-affecting mutation. Mirrors the delete + upload pattern. 3. wan_denoise.py: _ExpertSwapper.get() stashes _active_device_ctx right after device_ctx.__enter__() succeeds, before attempting the LoRA patcher's __enter__. If the LoRA enter raises, _release() can now actually find the device context and exit it — previously the ctx was unreachable and 8-9 GB of GGUF expert weights stayed pinned to GPU until the model cache LRU evicted them. 4. wan_ideal_dimensions.py: reject sources whose longer side is below the 16-px Wan grid. The downstream max(w, 16) clamp would otherwise silently disconnect the output from the requested aspect ratio (returning 16×16 regardless of the source's actual shape). 6. wan_video_denoise.py: docstring now explains the deliberate absence of denoising_start / denoising_end / initial-latents inputs (video i2v uses reference-frame conditioning, not noise injection; the image denoise node still handles still-image img2img). Tests: - test_device_context_released_when_lora_enter_raises pins #3. - test_input_smaller_than_pixel_grid_rejected pins #4. - test_output_dims_never_zero renamed to test_smallest_valid_input_still_snaps_to_16_grid (now exercises 16×16 rather than 8×8 since the latter is now correctly rejected). All 58 affected backend tests pass, frontend lint clean. Audit note for the PR description (NOT a fix): delete_video's _assert_video_owner permits write access on public boards (mirroring the image router's _assert_image_owner — intentional symmetry). The stricter _assert_video_direct_owner is reserved for board-move ops. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(frontend): typegen * fix(gallery): multi-select context menu actions for videos The video gallery context menu only operated on the single right-clicked item, so selecting multiple videos and hitting the trash icon deleted just the first one. Adds a video-side multi-selection menu mirroring the image one for star/unstar/download/change-board/delete, switched in on selectionCount > 1. Each menu now filters the polymorphic selection to its own kind and labels the action with an explicit count + kind (e.g. "Delete 3 Videos", "Move 2 Images to Board"). The destructive items disable when the kind-filtered subset is empty, so a video-only selection greys out the image menu and vice versa. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(multiuser): address Pfannkuchensack PR #9163 review findings Finding 1 (Medium): delete_board cascade ignored per-video / per-image ownership, letting a board owner destroy other users' contributions to a public/shared board just by deleting the board with include_images=true. Adds user_id filtering through get_all_board_*_names_for_board and delete_*_on_board (base + sqlite + image wrapper). Non-admin requests pass the requester's id so the SQL WHERE clause narrows the cascade to that user's rows; admins still pass None for the unrestricted path. Other users' content cascades to "uncategorized" via the existing FK on board_videos / board_images. Finding 2 (Low, i18n): GalleryItemStarIconButton and GalleryItemVideoStarIconButton shipped raw English "Star"/"Unstar" tooltips. Both now use the gallery.starImage / starVideo translation keys. Finding 3 (Low): delete_videos_from_list and delete_images_from_list re-raised HTTPException mid-loop, throwing away the response payload for items already deleted before the foreign name was hit. The frontend cache never learned about those partial successes, so deleted records reappeared in the UI until the next manual refresh. Both routes now skip auth-failed items in-loop and return 200 with the partial-success list. Residual: adds a test that an upload with an .mp4 extension but non-decodable bytes (a) reaches probe_video, (b) surfaces 415, (c) unlinks the streamed-to-disk temp file so the server doesn't leak storage on garbage uploads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(openapi): regenerate openapi.json The committed schema was stale relative to the current server (missing the utilities/expand-prompt and utilities/image-to-prompt endpoints, the ModelRecordOrderBy / SQLiteDirection list params, and the Wan / QwenImage / QwenVLEncoder config variants this branch adds). Regenerated via the same command the new openapi-checks workflow uses so the diff CI is empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(workflows): add "Image to Video - Add Frames" starter workflow Extends an existing video by extracting its penultimate frame, running it through Wan 2.2 I2V A14B + the Lightning LoRA pair to generate a new clip, and concatenating the result onto the source with a short crossfade. Cleaned per the default-workflows README: stripped value references on the four model loader fields and both Lightning LoRA fields so the workflow ships without keys/hashes for user-installed resources, gave the LoRA nodes "Apply LoRA (High)" / "(Low)" labels matching the existing Lightning default, remapped six stale exposedFields entries that pointed to template LoRA IDs no longer present in the graph, and synced the wan_video_denoise num_frames default to the value driven by the connected integer node. Tagged with both Text to Video and Image to Video so it surfaces under either filter in the Workflow Library. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gallery): video viewer polish and selection regressions - Restore auto-select-on-startup and on-board-switch: the polymorphic getGalleryItemNames endpoint replaced getImageNames as the grid's source of truth, so appStarted and boardIdSelected now wait on / read that cache instead of timing out forever. - Delete-then-select: video delete used to clear selection to null; image delete read a cache that's no longer warmed. Both now snapshot the gallery list before deletion and advance to the adjacent surviving item (prev > next > null) via a shared pickSelectionAfterDelete helper. - Video Viewer: right-aligned action bar with Open in new tab, Copy frame, Download, Delete, and a labelled Close video player button that only appears while playback is active. Copy uses canvas + ClipboardItem since video MIME types aren't supported cross-browser. - Next/prev arrows + galleryNav hotkeys now work when a video is in the Viewer (previously image-only). - Video context menu uses full-width text MenuItems instead of the cramped icon group, and gains an Open in new tab entry. * fix(gallery): bulk video drag-to-board and shift-click range selection - Bulk video drag: introduced multipleVideoDndSource so a multi-selection dragged from a video thumbnail moves every selected video, not just the first. The whitelist in useDndMonitor.ts also needed updating — without it the monitor's canMonitor gate silently dropped the new source type. - Mixed selections: both the multi-image and multi-video drag payloads now carry image_names + video_names side-by-side, so dragging from either kind in a mixed selection dispatches addImagesToBoard + addVideosToBoard together. Previously the image side leaked video names into image_names and the image router 404'd on each one. - Bulk video helpers: added addVideosToBoard / removeVideosFromBoard that fan out over the existing singular video router endpoint (no batch endpoint exists yet) — mirrors the change-board modal's existing loop. - Shift-click range selection: selectCachedGalleryItemNames now looks up the cache entry matching the gallery's current query args instead of taking the first entry from selectInvalidatedBy. RTK Query keeps unused entries warm for 60s after a board switch, and the old "first wins" behavior frequently landed on a stale board's name list, making shift-click silently no-op until a delete/move forced a refetch. * fix(scripts): force generate_openapi_schema.py to resolve invokeai from the repo root When the script was invoked as ``python scripts/generate_openapi_schema.py``, Python placed the script's directory at ``sys.path[0]`` rather than the repo root. ``import invokeai`` then resolved via the venv's site-packages, which on multi-worktree editable installs ends up importing ``invokeai`` as a PEP 420 namespace package that aggregates every worktree's ``invokeai/`` directory. Side-effect imports driven by submodule discovery silently miss whichever worktree isn't first on the namespace path, so the registry came up short by the invocations declared only in this worktree (the wan/video set, 15 classes). Running the same imports via ``python -c`` worked because ``sys.path[0]`` defaulted to the cwd and ``invokeai/__init__.py`` resolved cleanly to the worktree. Prepend the resolved repo root to ``sys.path`` before importing ``invokeai.*`` so the script always picks up the local sources regardless of how it was launched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add Frame Range from Video invocation with scrubbable preview New ``extract_video_range`` node trims a source video to a contiguous frame range and re-encodes it as MP4, slotting in naturally between a generated clip and Concatenate Videos for I2V chain shaping. Bounds are inclusive and support negative indices (``end_frame=-1`` keeps the final frame), matching Frame from Video. Output fps inherits from the input unless overridden. The node renders a per-type preview inside the workflow editor: two ``<video>`` tiles side by side, each driven by a CompositeSlider that scrubs the corresponding integer field. The tile uses ``currentTime = frame / fps`` so browsers display the seeked frame natively without a canvas roundtrip. Negative-index entries in the standard integer input are resolved against the source frame count for display only; the underlying field value is preserved verbatim. The custom UI is wired in via a ``CustomNodeBody`` dispatcher in ``InvocationNode.tsx`` rather than a registry — small enough to be explicit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): emit resolved frame indices and move preview to per-field renderer Three changes to the ``extract_video_range`` invocation: 1. New ``ExtractVideoRangeOutput`` mirrors ``VideoOutput`` and additionally emits the resolved (positive, 0-based) ``start_frame`` and ``end_frame`` indices. Chained workflows can feed those back into a downstream Frame from Video to extract the same boundary frame the trim landed on. 2. ``fps`` is now a plain ``int`` defaulting to 16 (was ``Optional[int]`` with an "inherit from input" fallback). Matches the default used by wan_l2v and the other Wan video producers, so chained workflows agree on framerate without each node guessing. 3. The frame preview is now a per-field widget driven by a new ``UIComponent.VideoFrameIndex`` hint. ``start_frame`` and ``end_frame`` are tagged with it; the new ``VideoFrameIndexFieldInput`` renders a number input plus a live <video> thumbnail and a scrubber slider, all writing to the same Redux field. Negative indices entered in the number input are still resolved against the source frame count for display only — the backend re-resolves at invoke time. The widget reads its companion ``VideoField`` (by convention, the sibling field named ``video`` on the same node) via direct Redux selectors, so it works wherever ``InputFieldRenderer`` is used — the workflow editor's node body AND the Form Builder's view/edit modes. The previous node-body ``ExtractVideoRangePreview`` and its ``CustomNodeBody`` dispatcher in ``InvocationNode.tsx`` are removed; the per-field widget supersedes both. In the workflow editor, side-by-side framing is lost in exchange for Form Builder support; users wanting the side-by-side layout in a form can group the two frame fields in a row container. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: ruff * fix(video): address PR #9163 review follow-ups - delete_board: include_images query description and OpenAPI schema now mention videos alongside images - get_video_thumbnail: check path existence before returning FileResponse so a missing thumbnail produces the documented 404 instead of an after-route error - delete_videos_on_board: stop deleting records for videos whose files failed to delete, so a transient FS error no longer orphans the file with no record pointing at it - DeleteBoardModal: destructive button and warning copy now mention videos * fix(video): address PR #9163 May-22 review and failing CI - remove_video_from_board now accepts either the direct video owner or a board write-access holder, so videos uploaded to a board that later flipped Public -> Shared/Private aren't stranded. - VideoService.create rolls back the DB record and board association if the underlying file save fails, preventing ghost records whose file endpoints 404. - delete_videos_on_board returns the actually-deleted names; delete_board uses that list so the response can't claim a video was destroyed when its record was preserved due to a file-delete failure. - Local test_videos_multiuser fixture now patches invokeai.app.api.routers._access so list/names route 403 checks work. - Regenerate schema.ts to pick up the CacheStats description. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(frontend): rebuild openapi * fix(video): register Viewer <video> as drag source Drag-and-drop from the Viewer pane now produces the same singleVideoDndSource (and multipleVideoDndSource for active multi-selection) as the gallery thumbnail, so a video can be dropped onto a Video Primitive's "Starting Video" field directly from the Viewer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): add frame preview to Frame from Video node Tag frame_index with ui_component=VideoFrameIndex so the node renders the same live frame thumbnail + scrubber as Frame Range from Video. The widget keys off the sibling 'video' field, which this node already has, so no frontend changes are needed. Bump node version 1.0.0 -> 1.1.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(video): derive Frame Range fps from source video by default Make the fps field optional (default None). When unset, the output frame rate is inherited from the probed source video so a trimmed clip plays back at the same speed as its source, falling back to 16 fps when the source rate can't be probed. An explicit fps still overrides. Bump node version 1.0.0 -> 1.1.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video): use fps=0 sentinel for source-derived Frame Range rate The previous Optional[int]/None design had no natural way to express 'unset' in the node's number input, and the ge=1 constraint rejected the intuitive fps=0 with a validation error. Make fps a plain int defaulting to 0, allow ge=0, and treat 0 as 'match the source video's frame rate'. Keeps in-progress workflows (already saved with fps=0) working without a version bump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(wan): add Wan 2.2 TI2V Ideal Dimensions node TI2V-5B uses the 16x Wan 2.2-VAE plus a 2x transformer patch, so pixel dims must be multiples of 32 (the existing I2V node snaps to 16, which the TI2V-5B patchify step rejects). Add a wan_ti2v_ideal_dimensions node that snaps to 32. Factor the shared scale-and-snap math into _scale_and_snap(multiple=...) so both nodes derive from one implementation; the I2V node is unchanged behaviorally (its existing tests still pass). Add a mirrored TI2V test suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(wan): add A14B/5B model hints to ideal-dimensions node titles Suffix the node titles with the target model family (A14B / 5B) so they're distinguishable in the add-node search and node header, and rewrite both docstrings to lead with which Wan 2.2 model they're for and cross-reference the other node. Purely UI metadata — no behavior or schema change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(workflows): replace bundled Wan 2.2 video workflows with curated set Remove the 6 previously-bundled Wan 2.2 *video* default workflows (Text to Video, Text to Video Lightning x2, Image to Video, Image to Video Lightning, Image to Video - Add Frames) and replace them with the 8 curated starter workflows: Text/Image to Video Lightning (+ Concept LoRA variants), Extend Video Lightning (+ Concept LoRA variant), and the TI2V-5B text/image-to-video low-quality variants. Each is assigned a stable default_ id and meta.category=default. Model fields are intentionally blanked (per-install keys don't resolve cross-instance) with the required models listed in each workflow's Notes. The two Wan 2.2 *image* workflows (Image to Image, Text to Image) are retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(video): add beginner Video Workflows guide for the 8 starter workflows New Features-section page (sibling to Video Generation) describing the eight bundled Wan 2.2 video workflows in plain language: how to choose between the Text/Image/Extend families and their Lightning / Concept-LoRA / TI2V-5B variants, how to select models from each workflow's Notes, how to run one, and a quick per-GPU guide. Cross-linked both ways with the Video Generation technical reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(video): fix Concept LoRA slot guidance (slots are required) The w/ Concept LoRAs workflows wire required lora fields (lora_selector / wan_lora_loader, no default) into the graph, so an empty slot blocks invocation. Correct the earlier claim that empty concept slots behave like the base workflow: every LoRA slot must be filled, and users without concept LoRAs should use the plain variant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(video): drop '(experimental)' from Video Generation title Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: refresh uv.lock Routine lock refresh (transitive dev deps: docutils, idna, platformdirs, python_discovery, tornado). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(wan): add first-last-frame interpolation (FLF2V) to I2V-A14B The Reference Image - Wan 2.2 node gains an optional End Image input: when set, encode_reference_image_to_video_condition places the end image in the final temporal slot and anchors the mask at both the first and last latent frames, so I2V-A14B interpolates a clip from the start image to the end image. Mirrors diffusers WanImageToVideoPipeline.prepare_latents with last_image set. The denoise loop is unchanged - for A14B it just concatenates the 20-channel condition, which is agnostic to one vs two anchors. FLF2V is A14B video only (num_frames > 1); the encoder raises a clear error for TI2V-5B or single-frame. Bump wan_ref_image_encoder to 1.2.0; add mask-anchoring unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(workflows): add 'Interpolate 2 Images to Video' starter + FLF2V docs Ship a default workflow that wires the new FLF2V End Image input end to end (I2V-A14B + Lightning, two image inputs interpolated). Model fields blanked with the required models listed in Notes, default_ id + category=default. Document FLF2V in the Video Generation reference and add the workflow to the Video Workflows guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(workflows): add Text/Image/Video to Video library filters + fix video tags Add 'Text to Video', 'Image to Video', and 'Video to Video' to the Common Tasks filter list in the Workflow Library browser. Fix the tags on the nine bundled Wan 2.2 video workflows, which were all copy-pasted as 'text to video': - Text to Video: the three T2V workflows - Image to Video: the I2V workflows + Interpolate (two-image) - Video to Video: the two Extend Video workflows The TI2V-5B variants also drop the spurious lightning/lora tags (they have no LoRAs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(workflows): add 'Extend Video to Image' FLF2V starter + docs Ship a default workflow that extends a video toward a user-provided target image: the new segment interpolates (FLF2V) from the source video's last frame to the destination image, then concatenates onto the original with a cross-fade. Model fields blanked, default_ id + category=default, tagged 'video to video'. Document it (card + usage instructions) in the Video Workflows guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(video): reorganize Video Workflows guide sections Group the Interpolate section and the Concept-LoRA / TI2V-5B asides with the image workflows, keep the Extend family (including Extend Video to Image) at the end, and retitle the section to 'Bundled video workflows'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(frontend): regenerate openapi and typegen * chore(backend): ruff * fix(future): make the WAN LoRA loader compatible with LoRA picker node PR #9259 * chore(frontend): remove unused selectT5EncoderModels import * chore(frontend): remove unused export * fix(gallery): count videos and pick video covers for board tiles Gallery boards previously joined only the `images` table for their headline count and cover thumbnail, so a board containing nothing but videos rendered as empty with no preview. BoardDTO now exposes `video_count` and an optional `cover_video_name`; the boards service picks the best cover across both tables using the same (starred DESC, created_at DESC) tie-break the image path already used, and the gallery list renders `image_count + video_count` everywhere it previously rendered just images (real boards, no-board pseudo-board, and the tooltip). Adds `getBoardVideosTotal` to round out the no-board counts (the BoardVideosTotal tag was already wired into invalidation). * test(boards): wire video record storage into multiuser test fixtures After the board cover/count fix started reading from `video_records` and `board_video_records`, the multiuser test fixtures that still set both to `None` started erroring out — the boards router's catch-all turned the AttributeError into a 404, cascading through every test that PATCHes or GETs a board (auth, workflows, data-isolation suites). Swap the `None` placeholders for real SqliteVideoRecordStorage / SqliteBoardVideoRecordStorage instances (paralleling the existing image storage setup), and pin sane defaults on the MagicMocks in `test_videos_multiuser.py` so the get_dto cover/count lookups don't trip Pydantic validation. * fix(ui): widen useVideoContextMenu ref type to allow null The ref param was typed RefObject<HTMLElement>, but useRef produces RefObject<HTMLElement | null>, breaking lint:tsc in GalleryVideoItem. Match the sibling useImageContextMenu signature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(backend): ruff * fix(tests): pass video/gallery services to InvocationServices in workflow-call router tests The workflow-call router tests from main construct InvocationServices directly and predate the video/gallery services added on this branch, so every test in the file errored with missing positional arguments. Mirror tests/conftest.py: real sqlite stores for video_records and board_video_records, None for the services the tests never touch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): refresh board caches when a generated video completes Video completion previously invalidated only the polymorphic gallery list tags, so the new video appeared in the grid while the board's video_count, cover thumbnail (Board tag / listAllBoards), and BoardVideosTotal stayed stale until an unrelated mutation refetched them. Use the shared getTagsToInvalidateForBoardAffectingMutation helper over the affected boards, matching the video mutation endpoints. Reported by @JPPhoto in PR #9163 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: include videos in date-based virtual boards Date virtual boards were image-only even though the gallery grid is now polymorphic: video-only dates never appeared, and mixed dates omitted videos from counts/contents/covers. - SqliteGalleryService owns virtual-board dates now: get_dates() unions images+videos per date (video_count added to VirtualSubBoardDTO, cover is the newest item of either kind via cover_image_name/cover_video_name), and list_item_names() gained a created_date filter. - New GET /api/v1/virtual_boards/by_date/{date}/item_names returns the same polymorphic (kind, name) refs as the gallery names endpoint; the legacy image_names route is kept for API compatibility. - Frontend virtual-board selection consumes the new endpoint, so videos show up in virtual date boards; VirtualBoardItem shows video counts (localized tooltip) and falls back to the video thumbnail for video covers. - tests/conftest.py wires a real SqliteGalleryService so router tests exercise the filter SQL; service + router tests cover video-only dates, mixed dates, cover selection, and per-user isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): don't advance gallery selection for videos whose delete failed handleDeletions treated every requested video as deleted when picking the post-delete selection, so a 403/500 on deleteVideo could jump the Viewer away from a video that still exists, and a surviving neighbour was skipped as a replacement candidate. Only successfully deleted names now count: a failed displayed video keeps its selection, and failed neighbours remain valid replacements. Covered by state.test.ts with rejected deleteVideo dispatches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): count uncategorized videos when deciding the gallery has content useHasImages only looked at boards and the uncategorized *image* total, so a gallery whose only content was an uncategorized video rendered the new-user/get-started view instead of the normal no-selection state. The hook now also reads the uncategorized video total (getBoardVideosTotal('none')); the decision logic is extracted as getHasGalleryContent and unit-tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): stop labeling board video counts as images in tooltips Board tooltips folded video_count into image_count and rendered boards.imagesWithCount, so a video-only board read e.g. '1 image, 0 assets'. Tooltips now show split image/video/asset counts using the new boards.videosWithCount translation; the compact unlabeled headline count in the boards list stays combined so video-only boards don't read as empty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): restrict client-side video upload acceptance to MP4 only The dropzone accept map advertised .webm/.mov and isVideoFile treated .webm/.mov/.mkv as videos, but the upload router accepts MP4 only, so those files were accepted client-side and then rejected with 415 after the bytes were uploaded. Consolidate the accepted-media lists into common/util/uploadMediaAccept.ts (single source of truth shared by useImageUploadButton and FullscreenDropzone) and pin them to the backend contract with a regression test. Also split the accept map: image-only upload fields (board covers, style presets, model images, workflow thumbnails) no longer advertise video/mp4, which they had inherited when video entries were added to the shared map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): bulk video star/unstar returns partial successes instead of 403 mid-batch star_videos_in_list and unstar_videos_in_list re-raised the ownership HTTPException mid-loop, so a batch containing one foreign (or stale) name mutated the earlier owned videos and then returned 403 with no payload — the client never invalidated caches for the videos that did change. Skip unauthorized names and return 200 with the actually starred/unstarred videos, mirroring delete_videos_from_list. Router tests cover the mixed-ownership batch for both routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): localize virtual board section header and toggle The 'By Date' header and the Collapse/Expand aria-label in VirtualBoardSection were hardcoded English. Add boards.byDate and common.collapse/common.expand translation keys and use them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): failed video saves no longer orphan files on disk DiskVideoFileStorage.save() moves the source MP4 into permanent storage before writing the thumbnail and sidecar, so a failure in either later step used to leave the moved MP4 (and partial artifacts) on disk with no DB record through which they could be managed. save() now removes its destination files before raising, and VideoService.create()'s rollback also deletes files to cover failures after a successful file save (e.g. building the DTO). Also documents why board attachment during create is best-effort (mirroring ImageService.create: a board deleted mid-generation must not destroy the render) and pins the explicit fallback — DTO reports the actual missing board association and a warning is logged — with a service test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(app): document videos.user_id lifecycle and pin user-deletion behavior videos.user_id deliberately has no FK to users, matching images/boards/ workflows (migration_27 adds those user_id columns index-only): deleting a user leaves their media in place for admin review/cleanup rather than cascading a row delete that would strand files on disk. A migration comment now states the parallel, and a migration-backed test creates a user and a video, deletes the user, and asserts the record survives, stays attributed to the deleted owner, and remains visible only to admins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(app): support VideoField values in session queue batch data Batch.data previously allowed ImageField but not VideoField, so submitting multiple VideoField values through the generic batching capability failed Pydantic validation before enqueueing. VideoField now joins the BatchScalarDataType union; a test asserts a VideoField batch validates and expands into separate sessions. schema.ts/openapi.json regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): video uploads are opt-in per consumer; upload validation fixes - useImageUploadButton gains an allowVideos opt-in (default off). Only the gallery uploader accepts videos; image-only consumers (ref images, board covers, launchpad buttons, image-to-prompt, etc.) no longer let a selected MP4 upload into the gallery while the requested image action goes nowhere. Videos are excluded from their accept map and rejected at runtime if the file dialog bypasses it, with tests via partitionUploadFiles. - The hook's loading state now covers both the image and video mutations, so an in-flight MP4 upload shows a loading button and blocks resubmission. - The fullscreen drag-drop/paste validator accepts a file when either its MIME type or its extension is recognized — a clip.mp4 with an empty File.type used to be rejected even though the backend accepts it. The validator moved to a pure module with tests. - Failed video uploads no longer toast "Image Upload Failed": video-only batches use a new toast.videoUploadFailed key, mixed batches the neutral toast.uploadFailed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): bound untrusted video decoding with a killable subprocess timeout probe_video / extract_video_frame / decoder_frame_count now run in a short-lived child process (video_decode_worker.py) killed after a hard timeout. Previously a crafted MP4 that failed the imageio probe and then hung inside cv2.VideoCapture()/read() would pin the FastAPI request worker that called it forever; repeated uploads could exhaust the pool. The worker is run by file path (not -m) and imports only imageio/PIL/cv2 so it starts without pulling in the invokeai package or torch. Tests substitute a never-returning worker command and assert the helpers fail within a bounded interval, plus happy-path tests against a real synthetic MP4 to validate the subprocess plumbing end to end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): stream frames through the video concat/trim nodes video_concat and extract_video_range fully decoded their inputs into lists of uncompressed frames before encoding; with the 1 GB upload cap a long 1080p source can expand to tens of gigabytes of RAM, so any user able to enqueue these nodes could exhaust server memory. Frames now stream from the decoder straight into an incremental FFMPEG writer. The concat node buffers only the transition windows (bounded by transition_frames), and the range node holds one frame at a time and stops decoding at the end of the requested range. Tests use lazy frame iterators to pin that encoding begins before the inputs are exhausted and that look-ahead stays bounded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): video upload feedback parity with images - Add uploadVideo matchFulfilled/matchRejected listeners mirroring the image upload listeners: success toasts name the destination board and navigate the gallery on the first upload of a batch; failure toasts name the failed file, which is what makes partially failed Promise.allSettled batches attributable (uploadVideos and the fullscreen dropzone aggregate without rethrowing, same as images). - GalleryUploadButton now uses the hook's combined isUploading, so an in-flight MP4 shows a spinner and blocks resubmission. - Media-neutral labels on the two video-enabled surfaces: gallery uploader aria/tooltip says Upload Media, the fullscreen overlay says uploaded items (not images) will be added, and its invalid-file toast mentions MP4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: include videos in the user-deletion data-loss note The admin guide's user-deletion warning enumerated boards, images, workflows, queue items, and style presets but not videos. State that video records survive with the deleted user_id, that files remain under outputs/videos, and that administrators keep gallery visibility of the orphaned media for review/cleanup — matching the behavior pinned by the video_records user-deletion lifecycle test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix video processing resource bounds * fix video lifecycle edge cases * stabilize decoder inactivity test * chore(deps): declare psutil as a direct dependency video_thumbnails.py now imports psutil for decode-worker process-tree termination, but it was only present transitively (via accelerate and friends). Declare it so the import can't silently break when an upstream package drops it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix video security and lifecycle regressions * chore: regenerate OpenAPI schema * fix: preserve exact frame dimensions in video encoders imageio's FFMPEG writer defaults to macro_block_size=16, which makes ffmpeg silently rescale frames to the next multiple of 16 — a 1920x1080 upload trimmed by Frame Range from Video came back as 1920x1088 while the DTO recorded 1080, so concatenating the trim with its own source failed the same-dimensions check. - New invokeai/app/util/video_encoding.make_mp4_writer single-sources the encoder settings (libx264, macro_block_size=1) for wan_latents_to_video, video_concat, and video_frame_extract_range. - yuv420p requires even dimensions, so concat and extract-range now reject odd-dimension sources up front with a clear error instead of an opaque ffmpeg failure mid-encode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: correct A14B fallback scheduler and stop LoRA leakage to low-noise expert Two silent-wrong-output bugs on the GGUF A14B path: - The no-scheduler-dir fallback returned FlowMatchEulerDiscreteScheduler for A14B, but both Wan-AI/Wan2.2-{T2V,I2V}-A14B-Diffusers repos ship UniPCMultistepScheduler with flow_shift=3.0 / flow_prediction / use_flow_sigmas (verified against the upstream scheduler_config.json). Every A14B GGUF render ran an unshifted first-order Euler schedule, degrading output and skewing how many steps land above the MoE boundary. An unreadable on-disk config now also falls back to the variant default instead of bare FlowMatchEuler. - low_loras fell back to the primary list when loras_low_noise was empty, but the Wan LoRA loader deliberately routes expert-tagged LoRAs to exactly one list — so a high-noise-only LoRA (e.g. a Lightning high-noise distill) was silently applied to the low-noise expert too, and high-only targeting was impossible. An empty low list now means no LoRAs on the low expert. Also (here and in the previous commit): the Wan VAE decode nodes now raise a clear latent-channel mismatch error (16-channel A14B latents vs 48-channel TI2V-5B VAE and vice versa) instead of an opaque tensor-size RuntimeError when the wrong VAE is selected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: keep viewer selection on surviving item after image deletion The image-side handleDeletions cleared the gallery selection (imageSelected(null)) whenever the deletion intersected the multi-selection but the displayed item was not among the deleted names — e.g. a video displayed while only images were deleted from a mixed selection, or a hover-delete of a non-displayed selected image. It also treated every requested name as deleted, ignoring the server's deleted_images response, so a partial failure could jump the selection away from an image that still exists. Port the deleteVideoModal logic: only server-confirmed deletions count, a surviving displayed item stays selected, and the usage-reset sweep (nodes/canvas/ref-image layers) runs only for actually-deleted images. Regression tests mirror deleteVideoModal/store/state.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf: ~48x faster Wan VAE decode on ROCm via conv2d decomposition MIOpen has no implicit-GEMM 3D-convolution kernels for the Wan VAE's shapes on RDNA3 and falls back to Im3d2Col (61% of decode GPU time in a torch profile). An 81-frame 832x480 decode took 730s on a W7900 vs 13s on an RTX 5060; dtype changes and cudnn.benchmark kernel search were all within +/-7%. A stride-1 kTxkHxkW conv3d is exactly the sum of kT conv2d taps over shifted temporal slices, and MIOpen's conv2d kernels are well optimized. This rebinds WanCausalConv3d.forward (class-level, idempotent, ROCm builds only) to that decomposition: - same 3-latent-frame decode: 81.6s -> 1.71s (~48x), extrapolating to ~12s for the 81-frame workload — matching NVIDIA wall-clock - numerically equivalent up to summation order: ~1e-6 max error vs F.conv3d in fp32; full bf16 decode differs by <=3/255 in pixel space (0.1% of pixels by more than 1/255) - strided encoder downsample convs keep the stock F.conv3d path (temporal taps couple under stride) - applied from every AutoencoderKLWan load site (Wan checkpoint/diffusers VAE loaders, Wan main-model VAE submodel, Anima VAE), so decode, encode, and ref-image conditioning all benefit; CUDA builds are untouched Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: self-heal the media cookie for sessions that predate or outlive it Video playback authenticates via an HttpOnly cookie (media elements can't send Authorization headers) that was only issued at login. A session restored from localStorage can hold a valid JWT without the cookie — the session may predate the cookie's introduction, or the cookie may have been cleared while the JWT survived. Every API call works, but each <video> request 401s and the player silently renders black with 0:00 duration (hit during PR #9163 functional testing). - New POST /api/v1/auth/media-cookie re-issues the cookie from a valid Bearer token: same live-user check as get_current_user, cookie lifetime clamped to the token's remaining validity, successful no-op in single-user mode. Cookie attributes are shared with login via _set_media_cookie so they can't drift. - Frontend calls it once per app load when an authenticated session exists (useMediaCookieRefresh in GlobalHookIsolator); failures are left to the existing global 401 handling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: use Apply LoRA Collection node in Wan concept-LoRA workflow templates Replace the per-slot concept-LoRA plumbing in the three 'w/ Concept LoRAs' video templates (Text to Video, Image to Video, Extend Video) with the single wan_lora_collection_loader node: users now add any number of concept LoRAs through one multi-LoRA form field instead of two fixed slots (T2V/Extend) or the lora_selector + collect chain (I2V). Chain in all three: model loader -> Lightning high-noise LoRA -> Lightning low-noise LoRA -> Apply LoRA Collection (concept LoRAs, ships empty) -> denoise. Also prunes exposedFields entries that referenced nodes deleted in an earlier revision of these templates (pre-existing; the frontend ignored them, but they were dead weight). Validated: backend WorkflowValidator + default-sync asserts, node versions current, every edge/form/exposedFields reference resolves, frontend parseAndMigrateWorkflow accepts all three, no machine-specific model identifiers ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: enforce multiuser image authorization * fix: address remaining Wan video review findings * test: give closed-stream decoder test headroom for slow Windows spawn The 0.2s decode timeout raced against Python subprocess startup on the Windows CI runner: the inactivity deadline fired before the worker could close its stdout, raising the generic decode timeout instead of the expected 'decoder worker' one. A generous timeout makes the EOF path deterministic; proc.wait still bounds the test at ~1s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address adversarial Wan video review findings * fix: resolve remaining Wan video review issues * feat(ui): rename gallery/board strings from Images to Images/Videos The gallery grid, selections, board operations, and related settings now operate on both images and videos, so the user-facing strings that describe them say so. Image-only surfaces (compare, reference images, progress previews, image storage maintenance, upload-format errors) are unchanged, as are unused legacy keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): harden video/media API per PR review - Scope the media cookie (set + delete) and the sliding-token middleware's auth-route exclusions to the reverse-proxy root_path, so media auth works behind sub-path proxies and a proxied logout can't mint a replacement token. - Video upload: run filesystem writes, MP4 validation, ffmpeg probing, and create() in the thread pool; add VideoUploadLimitASGIMiddleware to bound request size before multipart spooling and cap concurrent uploads. - Add GET /videos/i/{name}/workflow (mirrors the image route) so persisted video workflows/graphs are retrievable, with read-access checks. - Add DELETE /videos/uncategorized so the "Delete All Uncategorized Images/Videos" action can cover both media kinds. - Make polymorphic gallery ordering deterministic on created_at ties with kind+name tie-breakers, and pick virtual-board covers via ROW_NUMBER instead of a bare-column MAX() aggregate. - Add cpu_only to WanT5Encoder_WanT5Encoder_Config (parity with the other standalone text-encoder configs; the loader already honors the field). - Regenerate schema.ts/openapi.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): Wan VAE effective device + generalized working-memory estimation - Move VAE inputs to get_effective_device(vae) instead of the globally selected device — a cpu_only Wan VAE previously crashed every Wan VAE invocation on GPU hosts. - Add estimate_vae_working_memory_wan (per-frame conv working set + resident RGB clip, config-driven spatial scale for TI2V's 16x compression) and reserve working memory in all four Wan VAE paths, replacing the Flux estimator / missing reservations. - Fall back to spatial tiling for video decodes whose full-frame working set exceeds the execution device's VRAM, and move the decoded clip to the CPU before MP4 encoding so VRAM isn't held for the encode's duration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): video gallery/deletion/workflow fixes per PR review - Video deletion: use the batch endpoint (one request per invocation), clear workflow-node VideoField inputs only for server-confirmed deletions, and invalidate per-video DTO/metadata/workflow caches on delete (including board-cascade deletions in the board mutations). - VideoFieldInputComponent resets its value only on a confirmed 404, not on transient auth/server/network errors. - Global Delete hotkey partitions the polymorphic selection and routes videos through the video delete flow. - "Delete All Uncategorized Images/Videos" now deletes both media kinds; "Download Board" relabeled "Download Board Images" (image-only endpoint). - Translation splits: image-only multi-select actions revert to "Images"; polymorphic gallery search + star hotkey become media-neutral; the multi- drag preview counts the whole mixed selection. - Expose video metadata + workflow in the viewer: new video details overlay (metadata/workflow/graph tabs), a Load Workflow toolbar action for videos, and a 'video' source for the load-workflow dialog. - Model Manager: wan_t5_encoder gets the encoder settings panel (Run on CPU). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: make gallery docs video-aware; fix video workflow count Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address remaining video review findings * fix(backend): Wan inference fixes from full-PR self-review - Force bfloat16 in the standalone Wan VAE checkpoint loader: `precision: auto` resolves to fp16 on CUDA, and fp16 is unstable on the Wan VAE (the diffusers folder path already forced bf16). Both starter VAEs route through this loader. - Count the decoded RGB clip twice in the Wan working-memory estimator: diffusers' frame accumulation transiently holds ~2x the clip at peak, which the tiled-decode fallback previously undercounted by up to ~2 GB. - Ignore a wired 'Transformer (Low Noise)' for TI2V-5B (warn instead of raising a misleading A14B error), matching the field's documented behavior. - Release the expert swapper's device context even when LoRA weight-restore raises, so a failed unwind can't pin an 8-9 GB expert in VRAM. - Validate LoRA variant (A14B vs 5B) against the wired transformer in both Wan LoRA loaders — a mismatch previously crashed mid-denoise with an opaque layer-patcher shape error. - Fix the WanDiffusersModel exception ladder: the old-diffusers torch_dtype retry now also gets the missing-variant OSError fallback, with the matching dtype kwarg. - Mark both Wan ideal-dimensions nodes Prototype like every other Wan node; correct the text-encoder docstring (seq_len 512, not 226). - Add CPU tests for the multi-frame WanVideoDenoise loop (T_lat>1 shapes, zero-velocity invariant, A14B I2V 36-channel concat across frames, TI2V-5B expand-timesteps mask blend incl. per-token timesteps and frame-0 restore). Node version bumps: wan_model_loader, wan_lora_loader, wan_lora_collection_loader -> 1.0.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): video service fixes from full-PR self-review - Purge cached invocation outputs on video deletion: the memory invocation cache registered images/tensors/conditioning on_deleted hooks but not videos, so re-running an identical graph after deleting its output "succeeded" with a cached VideoOutput naming a 404 video. - Add the single-user early-return to VideosInterface's read-access and board-save checks, matching ImagesInterface — after a multiuser->single-user switch, video workflows no longer fail with PermissionError where identical image operations succeed. - Restructure staged-delete recovery to match the image side: video_records .get() raises rather than returning None, so the explicit commit branch was unreachable and recovery semantics lived in the exception handler by luck. - Return 416 (not 206 with "bytes 0--1/0") for any Range request against a zero-length video file; add tests for the whole Range-parser matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): video UX fixes from full-PR self-review - Add 'video' to the invocation-complete passthrough denylist: a Video Primitive completing mid-run invalidated gallery caches and auto-switched the user's selection/board to the node's *input* video. - Show the multi-selection context menu only when the clicked item is part of the selection (both image and video menus): right-clicking a video with 2+ images selected previously produced a menu with every action disabled. - Clear workflow VideoField references when videos are cascade-deleted via board deletion or delete-uncategorized, matching the direct-delete flow. - Toast on total video-delete-batch failure (the untracked mutation was otherwise silent) and on failed logout (the button previously did nothing when the server was unreachable). - Check resp.ok in useDownloadItem so an expired media cookie can't save error bodies as .mp4/.png files. - Provide the LIST_TAG-scoped VideoList tag from listVideos so the star/board invalidations that reference it actually match; fix the misleading comment; dedupe the doubled BoardVideosTotal tag type. - Validate VideoField access on workflow load (checkVideoAccess), resetting stale refs with a warning like image fields. - Wire middle-click-open-in-new-tab for gallery videos (the setting label already promised it). - Show the effective fallback (primary CFG) in the low-noise guidance slider when unset, instead of a constant the run never uses. - "Moving 1 image/video to board:" singular form for mixed-media moves. - Regenerate schema.ts/openapi.json (node version bumps, classification, docstring fixes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: workflows/docs/deps fixes from full-PR self-review - Update all 12 bundled Wan workflows to current node versions (wan_model_loader / wan_lora_loader / wan_lora_collection_loader 1.0.1, wan_ref_image_encoder 1.2.0, backfilling the optional end_image/num_frames inputs) so fresh installs don't open with "node needs update" badges; add a registry-consistency test over the bundled Wan/video workflows so stale embeds can't recur. - Docs: the A14B auto scheduler is UniPC (not FlowMatchEuler); note that the bundled TI2V-5B workflows ship 20 steps as a speed compromise vs the 40-50 quality recommendation. - Pin imageio[ffmpeg]>=2.37 and psutil>=6 (imageio encode behavior is version-sensitive enough that we carry a regression test for it); relock. - De-flake the thumbnail worker descendant-kill test (0.5s was the only tight ceiling in the file; a loaded runner could kill the worker before the child pid file existed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: raise python-tests job timeout to 30 minutes Main already runs 9-11 min per platform and this PR pushed py3.11 windows-cpu past the 15-minute cap (cancelled mid-pytest at 15m10s on the last run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: resolve CI failures from main's route-auth audit and knip The route-authorization audit merged from main (#9367) only recognized the two Bearer-token dependencies, so it flagged the video media routes (which authenticate via get_current_media_user_or_default) and the media-cookie endpoint (which validated its Bearer token inline). Teach the audit about the media dependency, drop the image media routes from PUBLIC_ROUTES (they now carry cookie auth on this branch), and give refresh_media_cookie a CurrentUserOrDefault dependency in place of its duplicated inline validation. The media-cookie tests now patch auth_dependencies' ApiDependencies like every other auth-dependent route test. knip: getDeletedVideosFromDeleteBoardAction was exported but only used in-module; cover it in the listener unit tests like its image twin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): Wan invocation fixes from JPPhoto's 2026-07-21 review - Both Wan LoRA loaders now validate the *resolved* config (type=LoRA, base=Wan) instead of trusting the client-supplied identifier fields; a mislabeled Flux/SDXL/main key is rejected up front instead of reaching the layer patcher. - The collection loader rejects LoRAs already applied upstream on either expert list (same invariant as the single loader) instead of silently doubling their effective weight. - A LoRA routed only to the low-noise list of a TI2V-5B main now logs a warning — the single-transformer path never consumes that list, so the routing was a silent no-op. - _ExpertSwapper._release clears its slots in a nested finally, so a device-context exit failure can no longer leave stale contexts that a later close() would double-exit. - WanLatentsToImage rejects multi-frame (T>1) video latents with a clear error pointing at wan_l2v, before the VAE is even loaded — previously it ran the full multi-frame decode and died in an opaque einops rank error. - wan_ref_image_encoder docstrings now describe both the 20-channel A14B and 48-channel TI2V-5B condition paths (they claimed A14B-only and told users to omit the node for TI2V-5B, contradicting the implementation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): per-user upload slots, probe validation, stream decode fallback - VideoUploadLimitASGIMiddleware now accounts upload slots per user (cap 2) on top of the global cap, so one tenant's slow chunked uploads can no longer hold all four slots and starve other users into 429s. Single-user mode keeps the whole global capacity (no per-user quota). - probe_video validates decoder-reported metadata: non-positive or over-limit dimensions (> 64 MP) and non-finite/negative durations are rejected before the upload path persists them; garbage fps degrades to None (unknown). The decode worker refuses to decode frames from files whose probed dimensions exceed the bound — a small crafted container claiming 100k x 100k would otherwise trigger a ~30 GB allocation. - The worker's stream command falls back to cv2 like probe/frame/count do, so an MP4 accepted at upload via the cv2 path now also works in the frame-range and concat nodes. The fallback only engages before the first emitted frame; a mid-stream decoder death still surfaces as an error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): media resilience fixes from JPPhoto's 2026-07-21 review - Partial board deletion: the boardAndImagesDeleted listener now invalidates the per-item Image*/Video* tags for the confirmed-deleted names it parses out of the 500 detail — the rejected mutation runs invalidatesTags with no result, so those caches previously stayed readable. - Media-cookie refresh retries transient failures on a bounded backoff (2s, 10s) instead of latching before the request and giving up forever; 401 still bails (session genuinely expired). - Thumbnail 404s degrade gracefully: BoardTooltip, GalleryBoard, VirtualBoardItem, and VideoFieldInputComponent show an icon fallback via fallbackStrategy="onError" (thumbnail generation is best-effort server- side), and GalleryVideoThumbnail's <video> fallback does the near-zero seek on loadedmetadata so browsers that don't auto-paint the first frame no longer show a black tile. - CurrentVideoPreview handles play() rejection (rolls isPlaying back) and media element errors (drops back to the play overlay) instead of hiding the overlay over a dead element with an unhandled promise rejection. - Hardening from the disputed items: changeVideoIsIntermediate also invalidates the VideoList LIST_TAG (covers a future omitted-board_id list); logout clears gallery.selection and the logout mutation documents that resetApiState in store.ts is what actually clears cross-user caches. - Typegen regenerated for the wan_lora_loader / wan_ref_image_encoder docstring updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address final video review findings * fix: harden video workflows and auth refresh * chore: regenerate OpenAPI schema * fix: close video workflow review gaps * fix: address self-review findings on video workflows and auth refresh Fixes the confirmed findings from the 2026-07-22 self-review round (github.com/invoke-ai/InvokeAI/pull/9163#issuecomment-5051225515): Frontend: - DeleteVideoModal: detach the dialog promise callbacks before the async deletion so the accept path's synchronous onClose (bound to cancel) no longer rejects a confirmed deletion as "User canceled"; dismissal still rejects. Adds behavioral tests for both paths. - Sliding-window refresh: bound the media-cookie sync fetch with a 10s AbortSignal timeout so a stalled request can't hold the exclusive cross-tab media-auth lock (shared with login/logout) forever; commit the refreshed token even when the cookie sync fails with a 5xx/network error (only a 401/403 rejection of the token blocks the commit); throttle acceptance to once per minute so bulk mutations don't pay a serialized cookie round trip per request. - Fallback media-auth lock: waiters renew their ticket lease while queueing so a >30s wait no longer lets a later ticket enter concurrently. - useMediaCookieRefresh: a pause() abort now resumes the same attempt instead of consuming a retry slot (and no longer permanently disables self-heal when the final attempt was paused); effect cleanup aborts in-flight refreshes so every logout path (sessionExpiredLogout, direct logout) stops a pending refresh from re-minting the cookie post-logout. - CurrentVideoPreview: a benign AbortError from play() rolls back silently, and load errors during the pending media-cookie self-heal window no longer raise a spurious "Unable to Play Video" toast. Backend: - Decode-worker memory bounds resized for legal near-cap frames: worker RLIMIT_AS headroom 1->4 GiB, parent RSS kill threshold 1->3 GiB (with keep-in-sync cross-references), monitor poll 50->250 ms. - Upload probe: a decode-worker timeout is now inconclusive (upload proceeds) instead of a 415; the probe's decoded first frame is reused as the thumbnail source, dropping one worker subprocess per upload. - delete_images_on_board / delete_videos_on_board return (deleted, failed) and delete_board reports the services' ground truth instead of a racy router-side listing diff (which also doubled the DB work). - Video list/uncategorized delete endpoints skip HTTPException (ownership skips, 404 races) silently instead of reporting them as failures, matching the image endpoints; delete_images_from_list now populates failed_images for genuine failures, matching the video path. - video_concat: an unknown probed fps mixed with agreeing known rates uses the known rate again instead of hard-erroring; disagreeing known rates still require an explicit Output FPS. - SlidingWindowTokenMiddleware runs its synchronous SQLite user lookup via run_in_threadpool so a contended DB lock can't stall the event loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): deflake drip-feed upload timeout test on Windows The test gave the middleware a 20 ms absolute upload deadline and delivered a chunk every 5 ms — but Windows event-loop timers have ~15.6 ms granularity, so the deadline could expire before the first chunk was ever delivered. The request then ended at receive_calls == 1 and the `receive_calls > 1` assertion failed (py3.12 windows-cpu CI). Widen the margins so the scenario the test describes actually occurs on coarse timers: 250 ms absolute deadline (many chunks flow first on every platform) with a 1 s idle timeout that never fires between 5 ms chunks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): sort imports in test_video_upload_limits (ruff I001) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address in-PR items from JPPhoto's non-merge-blocker list Fixes the subset of JPPhoto's 2026-07-22 "Still Open, Non-Merge Blockers" that are small, self-contained, and scoped to surfaces this PR introduced; the rest are deferred to a follow-on PR (triage rationale posted on the PR). - video_thumbnails._run_worker: an unexpected exception from communicate() (e.g. OSError) now terminates the worker process tree unconditionally — previously the finally stopped the RSS-monitor backstop while the except path left the worker and its ffmpeg child running forever. Adds the injected-OSError test JPPhoto asked for. - reduxRemember driver: client-state persistence POSTs now commit X-Refreshed-Token via the same acceptance flow as dynamicBaseQuery (extracted as acceptRefreshedToken, sharing the cross-tab lock, cookie sync, throttle, and generation guards), so persistence-only sessions no longer hard-expire mid-activity. - delete_videos_from_list / delete_images_from_list: dedup request names — a repeated name was processed twice and landed in both deleted_* and failed_* under the admin ownership bypass, toasting a spurious partial failure. Regression test added. - gallery + videos list endpoints: bound offset (ge=0) and limit (ge=0, le=MAX_PAGE_SIZE=1000) — these flowed verbatim into SQL, where a negative LIMIT means unlimited in SQLite, so one request could materialize the entire gallery. openapi.json regenerated (schema.ts is unchanged — constraints don't alter the generated types). - get_video_full: open the file once and serve HEAD/range/full from the fd (full downloads now stream chunked from the handle instead of FileResponse's lazy path-based open), eliminating the delete-race 500; deletion's atomic rename can no longer invalidate a path between check and open. - upload_video: close the multipart spool immediately after the body copy, shrinking the double-temp-disk window (2 x 1 GiB x 4 concurrent worst case) to the copy loop itself. - docs/gallery.mdx: document shared-board deletion semantics (only your own media is permanently deleted; admins delete everything) and the kept-on-failure -> Uncategorized behavior with its UI warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix video gallery review findings * fix Windows video thumbnail handling * test: cover remaining video review findings * test: cover adversarial video and Wan findings * fix: address remaining video and Wan review findings * test: call now-sync star/unstar routes directly 11b38696bf converted the video batch routes from async def to sync def (so FastAPI offloads them to its threadpool), but the star/unstar dedupe test still drove them through asyncio.run(), which requires a coroutine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover Wan conditioning and video link regressions * fix: validate Wan conditions and video links --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com> Co-authored-by: JPPhoto <jpollack@jpollackphoto.com> | 1 个月前 | |
Add database migration discovery and graph-based precedence (#9319) * feat: add graph-based sqlite migration discovery * docs: clarify sqlite migration dependencies * fix: address sqlite migrator review feedback --------- Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev> | 2 个月前 | |
fixes to env parsing, textual inversion & help text - Make environment variable settings case InSenSiTive: INVOKEAI_MAX_LOADED_MODELS and InvokeAI_Max_Loaded_Models environment variables will both set `max_loaded_models` - Updated realesrgan to use new config system. - Updated textual_inversion_training to use new config system. - Discovered a race condition when InvokeAIAppConfig is created at module load time, which makes it impossible to customize or replace the help message produced with --help on the command line. To fix this, moved all instances of get_invokeai_config() from module load time to object initialization time. Makes code cleaner, too. - Added `--from_file` argument to `invokeai-node-cli` and changed github action to match. CI tests will hopefully work now. | 3 年前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 天前 | ||
| 1 天前 | ||
| 2 年前 | ||
| 3 年前 | ||
| 4 个月前 | ||
| 3 年前 | ||
| 3 年前 | ||
| 1 个月前 | ||
| 1 年前 | ||
| 3 年前 | ||
| 3 年前 | ||
| 6 个月前 | ||
| 22 天前 | ||
| 30 天前 | ||
| 22 天前 | ||
| 1 年前 | ||
| 4 个月前 | ||
| 24 天前 | ||
| 30 天前 | ||
| 1 个月前 | ||
| 2 年前 | ||
| 2 年前 | ||
| 2 年前 | ||
| 24 天前 | ||
| 5 个月前 | ||
| 20 天前 | ||
| 2 年前 | ||
| 2 年前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 3 年前 |