Skip to content

fix: deleting an image no longer breaks search or drops out of search mode#350

Merged
lstein merged 2 commits into
masterfrom
lstein/fix/delete-preserves-encoder-and-search
Jul 16, 2026
Merged

fix: deleting an image no longer breaks search or drops out of search mode#350
lstein merged 2 commits into
masterfrom
lstein/fix/delete-preserves-encoder-and-search

Conversation

@lstein

@lstein lstein commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Deleting an image from a set of search results had three failure modes, reported together:

  1. The search reset and the slideshow jumped back to slide 1.
  2. Every subsequent search failed with 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.
  3. A misleading "Preparing autotagging vocabulary" toast appeared afterwards when autotagging was active.

Backend: npz rewrites stripped the encoder stamp

remove_image_from_embeddings() (delete) and update_image_path() (move) rewrote embeddings.npz with only the four per-image arrays, dropping model_id and embedding_dim. The next reader fell back to LEGACY_ENCODER_SPEC, so _check_cache_compatibility raised 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() dispatched albumChanged (changeType: "deletion") before its own stay-in-search-mode logic, and slide-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.js now owns repositioning: it drops deleted entries from the search results in place (state.searchResults shares 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_labels or /image_label request still pending after 3s. After a deletion the slow part is the UMAP refit and labels-cache rebuild (the .npz is newer than umap.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 preserve model_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

lstein and others added 2 commits July 16, 2026 15:04
… 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>
@lstein
lstein merged commit c0a0471 into master Jul 16, 2026
10 checks passed
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