Index InvokeAI board videos alongside board images - #369
Conversation
An InvokeAI board holds videos next to its images and its gallery shows both, so a board-backed album now indexes both. * ``fetch_board_video_names`` lists a board's videos. Videos are a separate resource over there: their own router, the board passed as a query parameter rather than a path segment, and a ``VideoNamesResult`` object instead of a bare array. A backend predating the video API 404s the whole router, which reads as "no videos here" so those albums keep indexing their images. * ``delete_video`` routes video deletions to the video endpoint. It also checks ``failed_videos``, because that endpoint can report a failure inside a 200 — accepting it would drop the local index row while the video stayed on the board. * Board albums derive both output directories now. ``image_paths`` is what gates file access and relative-path resolution, so a board video would otherwise index but be refused at playback. The paths are recomputed on every construction, which is how albums written before this pick up the videos directory with no migration. * The indexing pipeline itself learned videos: a video is loaded as the still frame ffmpeg extracts near its start and then flows through the encoder exactly like a photo, with the probe facts riding along in the per-image metadata. The scan gate skips the pixel probe for videos, which has no header to read and would otherwise reject every clip small enough to reach it. Collecting videos is opt-in per album (``Embeddings.index_videos``) and only board albums set it: turning it on for a directory album re-scans it for a whole new media type and makes indexing depend on ffmpeg, which is a separate call to make. Verified end-to-end against a live InvokeAI: 48 videos + 2 images resolved off a real board, with frame extraction and probe facts intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Master grew its own video support while this branch was open (#358–#362: badge, modal player, guards, indexing, semantic-map filter). Conflict resolution: * ``embeddings.py`` — took master's wholesale. Its video indexing is a superset of this branch's: same still-frame-as-the-image approach, plus priming the per-album frame cache at index time, invalidating scan-reject entries written while videos still went through the pixel gate, and pruning stale stills. ``Embeddings.index_videos`` and ``scan_extensions`` are gone with it — master collects videos for every album, so there is nothing left to opt into. * ``_resolve_board_album_files`` — kept this branch's. Master had added a filter that skipped board videos explicitly, because board deletion routed through ``delete_image`` and had not been verified against them. That is precisely what this branch fixes: videos are deleted through ``delete_video``, verified against a live InvokeAI. * Both ``Embeddings(...)`` call sites take master's ``album_key=``. Also dropped the docs note claiming videos were board-album-only, which master's directory-album indexing makes false.
An adversarial pass over #369 turned up four defects in the board-video work and three smaller ones. All are fixed here. * ``delete_video`` read ``failed_videos`` off the response body guarded only against a parse failure, so a 200 carrying a JSON array or string raised ``AttributeError`` — surfacing as a 500 *after* InvokeAI had already deleted the video, leaving the row in the local index pointing at a file that is gone. Guarded on the shape now, as its sibling ``fetch_board_video_names`` already was. * Deleting a single board video never discarded its extracted still. The non-board branch and the batch endpoint both do; the board branch returned first. Inert before this feature, since a board album could not hold a video. * ``fetch_board_video_names`` reported "no video API" and a board with no videos as the same empty list, and the index update prunes rows for every file the resolver does not return — so an unreachable video router silently dropped every indexed board video and reported a clean success. It now returns ``BoardVideoNames(names, api_available)``, and a run that loses rows to an unanswered listing says so. The docstring claimed a 404 could only mean a missing router; it cannot. InvokeAI's ``get_video_names`` calls ``assert_board_read_access``, which answers 404 "Board not found" to a non-admin caller, and a proxy can route ``/api/v1/images`` while 404ing ``/api/v1/videos``. Names already collected from earlier boards are kept when a later board 404s. * ``_delete_board_media`` justified its suffix dispatch with a 404 that does not happen: InvokeAI's image-delete route wraps its lookup in a bare ``except`` and answers 200 with an empty ``deleted_images``, so a mis-dispatch would read as success rather than as "already gone". The dispatch is right; the reasoning was not. * Deriving ``<root>/outputs/videos`` made the "Image path does not exist" warning fire on every config load for any InvokeAI that has never produced a video — which is most of them, on a correct configuration. The warning moved to a model validator that can see ``source_type`` and exempts a board album's derived directories; a wrong root still surfaces at index time, with a better diagnosis than this could give. * A board holding only videos, none of them on disk, blamed ``outputs`` as a whole. The error names the directory that actually came up empty, and an empty album after an unanswered video listing no longer asserts "contains no videos" — that is precisely what could not be checked. Tests: the missing-video test was gated on ffmpeg while never touching a video file, which left the two-directory accounting with no coverage anywhere ffmpeg is absent. ``min_image_bytes: 0`` went out of the board fixture (the bundled images clear the default gate, and videos bypass it outright). New: a board video served end-to-end through ``/videos/`` and ``/video_frame/``, the frame-cache discard on delete, both outage shapes including the partial listing that must *not* claim videos were dropped, the videos-only "none found" error, the legacy single-path album gaining the videos directory, and a 200 delete body that is not an object. Backend 691 passed, ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial reviewThree fresh-context agents attacked this branch on separate lanes (config path derivation, the InvokeAI client + delete dispatch, indexing pipeline + test honesty). Every claim below was verified against both this code and the InvokeAI checkout it talks to. Baseline reproduced clean at Everything found is fixed in 87061d6. Confirmed defects1.
2. Single-delete of a board video orphaned its extracted still — The board branch returned before 3. A falsely-empty video listing silently pruned already-indexed videos —
4. "A 404 cannot mean anything else here" is false — InvokeAI's 5. It claimed a video name sent to the images endpoint "just returns 404". InvokeAI's Lower severity
Attacks that failedSuffix mis-dispatch (InvokeAI names are What changed in 87061d6
The fixes went through two further adversarial rounds. The first found three real defects in them — the drop count included videos being kept, the empty-album error still asserted "no videos" during an outage, and the count primed a path-keyed Backend 691 passed, ruff clean. Out of scopeFiled #371 for a pre-existing bug this widens: Two other pre-existing items left alone: 🤖 Generated with Claude Code |
Rebase fallout, not a behaviour change. #369 gave board albums an ``outputs/videos`` directory alongside ``outputs/images``, so "the paths this root derives" is now both. The test echoed back only the images directory, which is genuinely not what the album derives, and the guard was right to refuse it with a 400. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: treat an album update as a patch, not a replacement (#371) ``POST /update_album/`` rebuilt the album from the request payload alone, so every field the payload left out was reset to its model default. Real callers send partial payloads — the bookmark menu sends five keys, the search dialog sends the album plus three overrides — and the worst of what that reset was ``source_type``: an InvokeAI-board album came back as a directory album with every ``invokeai_*`` field cleared. After that the album's own indexing walked InvokeAI's output directories as if they were ordinary folders, and deletions stopped routing through InvokeAI's API, leaving dangling rows in its database. Reproducing it took one bookmark and the "Move to folder → Yes, Add Folder" prompt. Now a key the payload does not carry keeps its stored value, and falsy values that real edits send (``min_image_bytes: 0``, ``use_query_optimization: false``, an emptied description) still win. Null is treated as absent, because ``create_album`` turns a None into the model *default* rather than into a cleared field — honoring it would swap an album's encoder for the host default and silently invalidate its index. The one field that genuinely needs clearing, ``invokeai_username`` (InvokeAI leaving multi-user mode), goes through a helper that keys on presence instead. Two things the patch rule would otherwise have broken, and one it exposed: * Board albums pass ``image_paths=None`` so the model re-derives them. Carrying the stored list over would suppress that derivation and pin the album to its old root the moment the user edits the root — the re-index would write under the new root while the access gate still pointed at the old one, 404ing every image. * ``min_search_score`` is left to the model to re-resolve when the encoder *family* changes. The floors differ by an order of magnitude (0.005 for SigLIP, 0.2 for CLIP), so carrying one across makes the new encoder look like it returns nothing — but swapping one CLIP model for another must not discard a score the user tuned by hand. Both the model and the router now read the rule from one ``default_min_search_score``. * An update whose ``source_type`` disagrees with the stored album is refused: the edit form branches on the stored kind and never offers to switch, so a disagreement is a partial payload being read as a replacement. A board album's directories cannot be changed or added to, and saying so beats accepting the request and quietly deriving something else — the request is refused with a 400 unless it merely echoes the album's paths (either the stored list or the one the same request derives, both of which callers legitimately send while a root edit is in flight). An ``HTTPException`` raised inside the handler is re-raised rather than wrapped, so that 400 (and the 404 for an unknown key) survives as itself. Frontend: the bookmark menu no longer offers to add a destination folder to a board album, since the backend has to refuse it, and its update payload carries only the keys it is changing. Tests: eleven new cases covering the demotion, the refused folder and source-type changes, root changes moving the derived paths, an explicit null neither clearing a tuning field nor surviving as one, the username clear, the encoder-family re-resolve and the same-family preservation, a blank index, and an unresolvable path being refused rather than crashing. Board-album tests also stopped deleting from the developer's real user data directory: ``default_board_index_path`` resolves against ``platformdirs.user_data_dir``, which nothing isolated, so a test album keyed like a real one removed the real index on cleanup. Backend 680 passed, frontend 581 passed, ruff and prettier clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: let the InvokeAI password be cleared, not only replaced A blank password means "I did not touch this" — the edit form never sees the stored one — so there was no way to say "forget it". A backend that leaves multi-user mode left a credential in config.yaml that nothing but a text editor could remove, and ``has_invokeai_password`` kept reporting it to the form. ``invokeai_password: null`` now clears it, while ``""`` and an omitted key still keep what is stored. No existing caller can send that null by accident: ``_album_public_dict`` carries ``has_invokeai_password`` and never the password itself, so the search-settings persister's round trip cannot reach it, and the bookmark menu sends three keys. The only source is a new *Forget saved password* checkbox in the edit form, offered only when there is one to forget and reset every time the form opens; a password typed in the same edit wins over it. The checkbox needs its own ``[hidden]`` rule: it lives in a ``<label>`` inside a ``.form-group``, and that selector sets ``display: block``, which as an author rule beats the UA stylesheet's ``[hidden]`` — the element would have stayed on screen for albums with no stored password. Same shape as the existing ``.video-player-*[hidden]`` rules. Clearing (or changing) a credential also invalidates the cached JWT. The token is keyed on ``(base_url, username)``, neither of which has to change when a password does, so a forgotten password would otherwise keep working from cache until the token expired — up to a day later. Tests: the three-way backend distinction (absent / blank / null), and on the frontend the payload matrix including "typed wins over the box", the row's visibility under the real CSS rule (asserting computed style, since the attribute alone reported hidden while the box was visible), and the reset on re-open. Backend 681 passed, frontend 587 passed, ruff and prettier clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: a board album's derived paths are a pair, not just images Rebase fallout, not a behaviour change. #369 gave board albums an ``outputs/videos`` directory alongside ``outputs/images``, so "the paths this root derives" is now both. The test echoed back only the images directory, which is genuinely not what the album derives, and the guard was right to refuse it with a 400. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: close four gaps in the update-as-patch rules Adversarial review of this branch. The first is a regression the patch semantics introduced; the rest are older, but this endpoint's new contract is what makes them contradictions. **A null min_search_score kept a stale floor across an encoder change.** The re-resolve was gated on `"min_search_score" not in album_data` while the value came from `kept`, which treats a null as absent — so the two halves disagreed. An explicit null both suppressed the re-resolve and fell through to the stored number, leaving a CLIP floor of 0.35 on a SigLIP album whose similarities sit around 0.05-0.15, i.e. a search that silently returns nothing. Gating on `is None` makes both halves agree. Before this branch the same payload re-resolved correctly, so this one is new. **`name` was not patchable.** It was read straight out of the payload while the docstring promised that any omitted key keeps its stored value, so `{"key": "a", "min_search_score": 0.3}` raised a KeyError that surfaced as a 500 whose detail was the word "name". **A missing album answered 404 or 500 depending on the payload.** A complete one reached the failed write and got its 404; a partial one died building a half-formed Album first. With patch semantics there is nothing to patch either way, so the key is checked up front. **The token cache was invalidated before the write, not after.** The cache key is (url, username) and does not include the password, so any request that read the album in between — an index scan, a board delete — logged in with the old password and re-cached a token that outlived the change by up to a day. That is precisely what the invalidation was added to prevent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: reload search settings after an edit; accept an echoed board path list Two remaining findings from the adversarial review of this branch. **A stale score could be persisted back over a re-resolved one.** Changing an album's encoder family re-resolves min_search_score server-side, but the album manager never told state about it, so `state.minSearchScore` kept the old family's floor. The next nudge of any search setting then POSTed that floor back — 0.2 on a SigLIP album, whose similarities sit around 0.05-0.15, so search silently returns nothing. The stomp predates this branch; what is new is that it sticks. Every album edit used to re-resolve the score from the encoder, so a bad value was corrected on the next save; now that an update keeps the fields its payload carries, it survives. `saveAlbumChanges` reloads the active album's settings before anything can write them back. **A board album refused a path list it had just handed out.** The guard compared normalized lists, so a caller echoing the album's own directories in a different order got a 400, and so did one echoing a snapshot taken before board albums gained their `outputs/videos` directory — one path where the album now has two. Neither is asking for a change. It compares sets and accepts a subset now, which is the same tolerance the guard already extends to a caller whose snapshot straddles a root edit. A directory the album does not derive is still refused out loud. Three sibling suites mock state.js and had to learn the new export. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
An InvokeAI board holds videos next to its images and its gallery shows both, so a board-backed album now indexes both. This removes the board-video restriction added in #361, which skipped them explicitly pending a verified delete path.
What changed
InvokeAI client
fetch_board_video_names()lists a board's videos. Videos are a separate resource over there: their own router, the board passed as a query parameter rather than a path segment, and aVideoNamesResultobject instead of the bare array the images endpoint returns. The sameis_intermediate=false+categories=general,userfiltering as images applies, and it matters — a Wan pipeline writes its intermediate clips to the board too (a real board listed 52 unfiltered vs 48 filtered).delete_video()routes deletions to the video endpoint. It also checksfailed_videos, because that endpoint can report a failure inside a 200 — accepting it would drop the local index row while the video stayed on the board, and the video would reappear on the next re-index.Board albums
outputs/imagesandoutputs/videos).image_pathsis what gates file access and relative-path resolution, so a board video would otherwise index but be refused at playback. The paths are recomputed on every construction, which is how albums written before this pick up the videos directory with no migration step.Verification
Backend 679 passed, frontend 578 passed, ruff clean.
Verified end-to-end against a live InvokeAI: 48 videos + 2 images resolved off a real board, through the auth fallback, with frame extraction and probe facts intact.
Note on the merge commit
Master's video work (#358–#362) landed while this was open.
embeddings.pytook master's side wholesale — its video indexing is a superset of what this branch had grown independently — and_resolve_board_album_fileskept this branch's. Details are in the merge commit message. Since PRs here are squash-merged, the merge commit disappears on landing.🤖 Generated with Claude Code