Skip to content

feat: guard the paths that assume an album entry is a still image - #360

Merged
lstein merged 2 commits into
masterfrom
lstein/feature/video-backend-guards
Aug 17, 2026
Merged

feat: guard the paths that assume an album entry is a still image#360
lstein merged 2 commits into
masterfrom
lstein/feature/video-backend-guards

Conversation

@lstein

@lstein lstein commented Aug 15, 2026

Copy link
Copy Markdown
Owner

PR 6 of 8 in the video-support stack. Stacked on #359.

Six places would misbehave once the directory walk starts collecting videos. Each is fixed and pinned here, ahead of the flip in PR 7.

find_duplicate_clusters excludes videos

A video's embedding describes one extracted frame, and opening frames are so often a black slate or title card that two unrelated clips sit at ~1.0 cosine and get reported as duplicates — which users act on by deleting. Videos were already excluded from dedupe as a product decision; nothing enforced it.

_load_image_path raises 400 for videos

It's the choke point for /invokeai/use_ref_image, which POSTs the file to InvokeAI's /images/upload. An .mp4 would come back as an opaque 502 carrying raw upstream text. The drawer already withholds the button for videos (PR 3); this closes the API path behind it.

Worth noting for review: /recall goes through _load_raw_metadata, not the file path, so it already refused videos for a different reason. There's a test pinning that too, so a future refactor routing recall through the file path stays correct.

search_with_text_and_image returns 400, not 500

A video dropped on the search panel used to surface as an opaque error from deep inside PIL.

download_images_zip

Video members are ZIP_STORED rather than deflated (their streams are already compressed), and selections over 2 GB get a 413. The archive is assembled entirely in BytesIO, so twenty bookmarked 200 MB clips would have been several gigabytes resident — an OOM only reachable once videos exist.

index_metadata splits the count

Returns image_count and video_count alongside filename_count, so the album card can explain why the number jumped. Legacy indexes report video_count == 0.

progress_tracker.add_completion_warning

The completion warning was a single slot, already used for the board-album "N of M missing" notice — a video-skip warning would have silently discarded it. A run can legitimately produce both. set_completion_warning keeps its replace semantics.

bookmarks.js video download

Points an <a download> straight at the URL instead of buffering the whole file into a blob (a 200 MB clip would sit entirely in browser memory before the save dialog appeared), and derives the fallback extension from the real path rather than hardcoding .jpg.

Tests: 14 new in test_video_guards.py. Backend 590 passed, frontend 501 passed, ruff + eslint clean.

🤖 Generated with Claude Code

@lstein
lstein force-pushed the lstein/feature/video-player-modal branch from e089533 to 7efd8c2 Compare August 16, 2026 19:59
@lstein
lstein force-pushed the lstein/feature/video-backend-guards branch 2 times, most recently from a32b9aa to 2bf3652 Compare August 16, 2026 21:43
@lstein
lstein force-pushed the lstein/feature/video-player-modal branch from 786c215 to 19d77e8 Compare August 17, 2026 00:33
@lstein
lstein force-pushed the lstein/feature/video-backend-guards branch from 2bf3652 to ca1a9a4 Compare August 17, 2026 00:33
@lstein
lstein force-pushed the lstein/feature/video-player-modal branch from 19d77e8 to 2ae86d8 Compare August 17, 2026 01:56
@lstein
lstein force-pushed the lstein/feature/video-backend-guards branch from ca1a9a4 to 9b20f94 Compare August 17, 2026 01:56
@lstein
lstein force-pushed the lstein/feature/video-player-modal branch from 2ae86d8 to c6b1c34 Compare August 17, 2026 02:53
@lstein
lstein force-pushed the lstein/feature/video-backend-guards branch from 9b20f94 to 935cccc Compare August 17, 2026 02:53
@lstein
lstein force-pushed the lstein/feature/video-player-modal branch from c6b1c34 to 0685d6f Compare August 17, 2026 02:58
@lstein
lstein force-pushed the lstein/feature/video-backend-guards branch from 935cccc to dd91950 Compare August 17, 2026 02:58
@lstein
lstein force-pushed the lstein/feature/video-player-modal branch from 0685d6f to 6aded63 Compare August 17, 2026 03:38
@lstein
lstein force-pushed the lstein/feature/video-backend-guards branch from dd91950 to bd5276a Compare August 17, 2026 03:38
@lstein
lstein force-pushed the lstein/feature/video-player-modal branch from 6aded63 to 3ae210e Compare August 17, 2026 03:52
@lstein
lstein force-pushed the lstein/feature/video-backend-guards branch 2 times, most recently from 2f85ccf to 60f945c Compare August 17, 2026 04:06
Base automatically changed from lstein/feature/video-player-modal to master August 17, 2026 10:40
Six places would misbehave once the directory walk starts collecting videos.
Each is fixed and pinned here, ahead of the flip.

find_duplicate_clusters now excludes videos. A video's embedding describes one
extracted frame, and opening frames are so often a black slate or a title card
that two unrelated clips would sit at ~1.0 cosine and be reported as
duplicates — which users act on by deleting. The owner had already excluded
videos from dedupe as a product decision; nothing enforced it.

_load_image_path raises 400 for videos. It is the choke point for
/invokeai/use_ref_image, which POSTs the file to InvokeAI's /images/upload;
an .mp4 would come back as an opaque 502 carrying raw upstream text. The
drawer already withholds the button for videos, so this closes the API path
behind it. (Recall reaches videos via the metadata path and already refused
them; a test pins that too.)

search_with_text_and_image returns 400 rather than 500 when the query blob is
not a decodable still — a video dropped on the search panel used to surface
as an opaque error from deep inside PIL.

download_images_zip stores video members instead of deflating them (their
streams are already compressed) and refuses selections over 2 GB. The archive
is assembled entirely in memory, so twenty bookmarked 200 MB clips would have
been several gigabytes resident — an OOM only reachable once videos exist.

index_metadata reports image_count and video_count alongside filename_count,
so the album card can explain why the number jumped rather than leaving the
user to guess. Legacy indexes report video_count == 0.

progress_tracker gains add_completion_warning. The completion warning was a
single slot already used for the board-album "N of M missing" notice, so a
video-skip warning would have silently discarded it — a run can legitimately
produce both.

bookmarks.js downloads a video by pointing an <a download> straight at the
URL instead of buffering it into a blob, and derives its fallback extension
from the real path rather than hardcoding .jpg.

Tests: 14 new in test_video_guards.py. Backend 590 passed, frontend 501
passed, ruff and eslint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lstein
lstein force-pushed the lstein/feature/video-backend-guards branch from 60f945c to 878a453 Compare August 17, 2026 10:42
The positive search test could not fail. test_search_still_accepts_an_
image_query never built an index, so the endpoint 500s on the missing
.npz — and the assertion was `!= 400`, which a 500 satisfies. The test
would have held even if the new guard rejected every valid image, which
is the one thing it was there to rule out. It now builds the index and
asserts 200 plus a non-empty result set.

add_completion_warning silently dropped distinct notices. Dedupe tested
`message not in existing` against the accumulated text, so "2 videos
could not be read." is a substring of "12 videos could not be read." and
was discarded — exactly the silent discard the method was added to
prevent. Notices are now held as a list per album and compared whole,
joined with a space only when handed to the poller, so the text the user
sees is unchanged.

ZIP_STORED for videos was claimed but untested. compress_type is a
per-member argument that can be dropped with no other visible effect;
now asserted against the returned archive (and confirmed to fail when
the argument is removed).

The bookmarks.js change shipped with no test at all. New
bookmarks-download.test.js covers the video link path (no fetch of the
media, no object URL), the still-image blob path it must not disturb, and
the extension fallback — including a video whose index carries no path,
where the old hardcoded .jpg would have named a clip nothing can open.

The ZIP size pre-pass now applies the same validate_image_access check as
the loop that writes the archive, so the ceiling is measured over files
that would actually be included; counting a rejected path could refuse a
selection that zips to nothing.

Also: the 413 message renders sub-gigabyte ceilings as MB instead of
"over the 0 GB download limit", and the dead `except HTTPException` in
the query-image guard is gone — nothing inside that block raises one.
@lstein
lstein merged commit 6a01f89 into master Aug 17, 2026
10 checks passed
@lstein
lstein deleted the lstein/feature/video-backend-guards branch August 17, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant