Repository navigation
Catalog embeddings + semantic search (Phase 2) - #31
Merged
Merged
Conversation
Embed the episodes in a catalog and search them by natural language.
pip install "forge-robotics[embed]"
forge embed --catalog ./forge-catalog
forge search "picks up the red cup" -c ./forge-catalog --top 10
forge search --like <episode_id> -c ./forge-catalog
New embedding engine (forge/embed/):
- base.py — EmbeddingModel interface + device selection (cuda -> mps -> cpu)
- siglip.py — SigLIP (siglip-so400m, D=1152; shared image-text space)
- registry.py — model registry; model_id = "<name>@<ckpt-hash>" (reproducible)
- sampling.py — ~1 Hz frame sampling + pooling (mean / first-mid-last)
Catalog integration:
- schema.py — additive `embeddings` table; SCHEMA_VERSION 1 -> 2 (v1 catalogs
open unchanged and gain the table on first embed)
- catalog.py — add_embeddings(), search() (text + --like), single-model_id
enforcement, DuckDB array_cosine_similarity
- embed.py — backfill pipeline: read catalog -> localize -> readers -> sample
-> embed -> match by content_hash -> commit
- cli.py — forge embed, forge search, forge ingest --embed
Design notes:
- Vectors stored as list<float32> (+ dim); one table holds multiple model_ids.
- Search enforces a single model_id — vector spaces never mix.
- Reuses existing internals: readers for frames, forge.io for cloud, [video] for
decode, and the catalog writer/commit machinery. No existing behavior changed.
- Device auto: Apple MPS is first-class (PYTORCH_ENABLE_MPS_FALLBACK, fp32).
- New deps behind the [embed] extra (torch, transformers, pillow, sentencepiece);
the base CLI never imports them.
Verified end-to-end on real droid_100 in MinIO: 100 episodes embedded on the Mac
GPU (300 vision + 46 text vectors), text search ranks the correct episodes
("close the drawer" -> the "Close the drawer" episode). The in-repo example
catalog (catalog_example_droid_100) now ships those vectors (copied + remapped by
content_hash, not regenerated), so `forge search` works from a fresh checkout;
the example stays excluded from the built wheel.
Tests (tests/test_embed.py): a torch-free deterministic fake model exercises the
full engine/schema/search/enforcement/idempotent/cloud/CLI paths; a guarded
real-SigLIP smoke test asserts D=1152. No existing tests change.
Co-Authored-By: Claude Opus 4.8 (1M context) <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.
What
Embed the episodes in a catalog and search them by natural language. Uses SigLIP (a shared image–text model), so text queries match episode video, not just metadata.
This is Phase 2 of the Forge Data Engine, building on the catalog (#30) and cloud I/O (#29).
New engine
forge/embed/base.pyEmbeddingModelinterface + device selection (cuda → mps → cpu)siglip.pysiglip-so400m, D=1152, shared image–text space)registry.pymodel_id = "<name>@<ckpt-hash>"(reproducible across machines)sampling.pymean/first-mid-last)Catalog integration
embeddingstable — additive; bumpsSCHEMA_VERSION1 → 2 (v1 catalogs open unchanged, gain the table on first embed). One row per(episode_id, model_id, level, camera): episode-level vision per camera + instruction text.Catalog.add_embeddings()/search()(text +--like, single-model_idenforcement, DuckDBarray_cosine_similarity);forge/catalog/embed.pybackfill; CLIforge embed/forge search/forge ingest --embed.Design highlights
list<float32>(+dim) so one table holds multiple models; search filters to onemodel_id, casts toFLOAT[dim]. Vector spaces never mix (enforced).forge.iofor cloud sources,[video]for decode, the catalog writer/commit machinery. No existing behavior changed.PYTORCH_ENABLE_MPS_FALLBACK, fp32). Same command on a CUDA box.[embed]extra;forge --helpnever imports them.Verified on real data
Ran end-to-end on droid_100 in MinIO: 100 episodes embedded on the Mac's GPU (300 vision + 46 text vectors), and text search ranks the right episodes —
"close the drawer"→ the episode captioned "Close the drawer", matched by its video. (SigLIP cosines are small in magnitude by design; the ranking is what's semantic.)The in-repo
catalog_example_droid_100/now ships those vectors (copied and remapped bycontent_hash, not regenerated), soforge searchworks from a fresh checkout —--likewith no deps, text search with[embed]. The example stays excluded from the built wheel.Testing
tests/test_embed.py— a torch-free deterministic fake model drives the full engine / schema / search-ranking / single-model-enforcement / idempotent-re-embed /--like/memory://cloud / CLI paths. A real-SigLIP smoke test (guarded byimportorskip+FORGE_RUN_SIGLIP=1) asserts D=1152 — it caught a transformers-5.x output-shape change during the demo.Docs
README "Semantic search" section,
forge/embed/README.md(models, device selection, reproducibility), the updated example README, and a CHANGELOG entry.🤖 Generated with Claude Code