fix: deleting an image no longer breaks search or drops out of search mode#350
Merged
Merged
Conversation
… mode Deleting (or moving) an image rewrote embeddings.npz with only the four per-image arrays, silently stripping the model_id encoder stamp. The next search then misread the index as the legacy openai-clip:ViT-B/32 encoder and failed with EmbeddingCacheMismatch on any album using a different encoder, until a full re-index. Both rewrite paths now carry over every non-per-image key (model_id, embedding_dim, and anything added later). Separately, deleting a search result kicked the user out of search mode and back to slide 1: slide-state's albumChanged handler ran before control-panel's search-preserving code, exited search mode, and reset the position, so the preserving code was dead. The deletion branch in slide-state.js now owns repositioning — it drops deleted entries from the search results in place (state.searchResults shares the array object), renumbers the survivors' global indices, and keeps the search position so the next result fills the slot. This also fixes bookmark multi-delete during an active search, which went through the same path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ecomputes The "Preparing autotagging vocabulary — this is usually a one-time operation" toast fires on ANY /cluster_labels or /image_label request still pending after 3s. After a deletion the slow part is the UMAP refit and labels-cache rebuild, not the vocab build (the vocab cache is keyed on the album's encoder spec, which didn't change), so the old wording both blamed the wrong work and broke its "one-time" promise. Use a generic message that covers both causes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Deleting an image from a set of search results had three failure modes, reported together:
EmbeddingCacheMismatch: Embedding cache ... was built with encoder 'openai-clip:ViT-B/32' but the active encoder is 'open-clip:ViT-L-14/dfn2b_s39b'until the album was fully re-indexed.Backend: npz rewrites stripped the encoder stamp
remove_image_from_embeddings()(delete) andupdate_image_path()(move) rewroteembeddings.npzwith only the four per-image arrays, droppingmodel_idandembedding_dim. The next reader fell back toLEGACY_ENCODER_SPEC, so_check_cache_compatibilityraised on any album configured with a non-legacy encoder — a single deletion effectively poisoned the index.Both rewrite paths now copy every non-per-image key through the rewrite via a shared
_copy_non_per_image_keys()helper, so keys added in the future can't be silently dropped either.Frontend: slide-state stomped the search before control-panel could preserve it
handleSuccessfulDelete()dispatchedalbumChanged(changeType: "deletion") before its own stay-in-search-mode logic, andslide-state.js's synchronous handler only preserved position when not in search mode — it exited search mode and reset the position, making control-panel's preserving code dead on arrival.The deletion branch in
slide-state.jsnow owns repositioning: it drops deleted entries from the search results in place (state.searchResultsshares the same array object), renumbers the survivors' global indices, and keeps the search position so the next result fills the slot. Control-panel's duplicate logic is removed. This also fixes bookmark multi-delete during an active search, which dispatches the same event shape.Frontend: the slow-autotag toast blamed the wrong work
The toast fires on any
/cluster_labelsor/image_labelrequest still pending after 3s. After a deletion the slow part is the UMAP refit and labels-cache rebuild (the.npzis newer thanumap.npz), not the vocabulary build — the vocab cache is keyed on the album's encoder spec, which didn't change. The old wording ("Preparing autotagging vocabulary — this is usually a one-time operation") both misattributed the delay and broke its "one-time" promise on every delete. It now reads "Computing autotag labels — this can take a while on large albums.", which is accurate for both causes.Tests
tests/backend/test_index.py::test_npz_rewrites_preserve_non_per_image_keys— both rewrite paths preservemodel_id,embedding_dim, and an unknown future key.tests/backend/test_index.py::test_delete_image— extended to assert the encoder stamp survives the delete endpoint.tests/frontend/slide-state-deletion.test.js— 8 new tests: deleting the current/last/earlier result, bookmark multi-delete, search emptying out, album-mode position math, and the shared-array in-place mutation contract.Full suites pass: 456 backend (pytest), 430 frontend (jest); ruff/eslint/prettier clean.
🤖 Generated with Claude Code