Skip to content

[Prefactor] Return keyed BatchedEmbeddingResult from image embedding generators#1563

Open
JonasWurst wants to merge 2 commits into
mainfrom
jonas-lig-10034-prefactor-keyed-embedding-result
Open

[Prefactor] Return keyed BatchedEmbeddingResult from image embedding generators#1563
JonasWurst wants to merge 2 commits into
mainfrom
jonas-lig-10034-prefactor-keyed-embedding-result

Conversation

@JonasWurst

@JonasWurst JonasWurst commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

Prefactor for LIG-10034 (per-item broken-file tolerance). The image and image-crop embedding paths now take (sample_id, filepath) / (sample_id, crop) pairs and return BatchedEmbeddingResult(embeddings, keys: list[UUID]): each embedding is tagged with the sample id that produced it.

Why

Today the embedding helpers return a bare array aligned by position, so the manager keeps parallel sample_ids / filepaths lists in lock-step and re-associates by integer index. That's brittle — any reordering silently mismaps embeddings to samples. Carrying the sample id with each input lets callers map results back by identity, which is what the upcoming broken-file skipping (LIG-10034) needs: a skipped file just omits its id from keys.

Behavior

No behavior change. Every input is embedded and keys covers them all; the manager maps result.keys straight to storage.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Embedding APIs now return both embeddings and matching item IDs to keep outputs traceable.
    • Image and crop embedding workflows accept keyed batches for more reliable mapping.
  • Bug Fixes

    • Embeddings are stored and returned based on returned IDs, no longer on the original input order.
    • Empty embedding requests now produce a consistent empty result structure.
  • Tests

    • Updated/expanded unit tests to verify keyed output, correct ordering behavior, and empty-input handling.

Prefactor for per-item broken-file tolerance (LIG-10034). The image and
image-crop embedding paths now take (sample_id, filepath) / (sample_id, crop)
pairs and return BatchedEmbeddingResult(embeddings, keys): each embedding is
tagged with the sample id that produced it.

This lets callers map results back to their samples by identity instead of by
position, so the manager no longer keeps parallel sample_id/filepath lists in
lock-step. No behavior change: every input is embedded and keys covers them all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 99bbe727-96ed-47e6-879f-0bdacf68e47d

📥 Commits

Reviewing files that changed from the base of the PR and between 27cc280 and 198ac73.

📒 Files selected for processing (2)
  • lightly_studio/src/lightly_studio/dataset/image_crop_embedding.py
  • lightly_studio/tests/dataset/test_image_crop_embedding.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lightly_studio/tests/dataset/test_image_crop_embedding.py

📝 Walkthrough

Walkthrough

This PR changes image embedding APIs to accept UUID-keyed inputs and return BatchedEmbeddingResult objects carrying embeddings plus keys. The batching helpers, model generators, manager flows, and tests are updated to preserve identity through embedding generation and storage.

Changes

Keyed embedding API refactor

Layer / File(s) Summary
BatchedEmbeddingResult contract and protocol
lightly_studio/src/lightly_studio/dataset/embedding_generator.py, lightly_studio/tests/dataset/test_embedding_generator.py
Adds BatchedEmbeddingResult, updates the image embedding protocol to keyed sequences and structured returns, and updates RandomEmbeddingGenerator plus its tests.
Full-image batching helper
lightly_studio/src/lightly_studio/dataset/image_embedding.py, lightly_studio/tests/dataset/test_image_embedding.py
embed_image_files_batched now accepts keyed filepaths and returns BatchedEmbeddingResult; tests verify empty output and key ordering.
Crop batching helper
lightly_studio/src/lightly_studio/dataset/image_crop_embedding.py, lightly_studio/tests/dataset/test_image_crop_embedding.py
embed_image_crops_batched now processes keyed crops, tracks output keys, and returns BatchedEmbeddingResult; tests verify reordered keys and embeddings.
Model-specific generator wiring
lightly_studio/src/lightly_studio/dataset/mobileclip_embedding_generator.py, lightly_studio/src/lightly_studio/dataset/perception_encoder_embedding_generator.py, lightly_studio/tests/dataset/test_mobileclip_embedding_generator.py, lightly_studio/tests/dataset/test_perception_encoder_embedding_generator.py
MobileCLIP and PerceptionEncoder generators now delegate keyed inputs to the updated batching helpers and return structured results; tests pass UUID-tagged inputs.
Embedding manager integration
lightly_studio/src/lightly_studio/dataset/embedding_manager.py, lightly_studio/tests/dataset/test_embedding_manager.py
Embedding manager flows now build keyed inputs and store results using returned keys; the fake generator stub is updated to the new signature.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

Suggested reviewers: MalteEbner, horatiualmasan

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR explains the change and motivation, but it omits the required testing section and changelog status from the template. Add a How has it been tested? section with repro steps and results, and include the Did you update CHANGELOG.md? checkbox answer.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: returning keyed BatchedEmbeddingResult from image embedding generators.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jonas-lig-10034-prefactor-keyed-embedding-result

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27cc280107

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

import logging
from dataclasses import dataclass
from uuid import UUID
from uuid import UUID, uuid4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use module-qualified uuid4 imports

The Python guideline in ai_guidelines/python.md asks function imports to use the containing module with dot notation, except for typing helpers. This new direct uuid4 import should be import uuid with uuid.uuid4() at the call site to keep production imports consistent.

Useful? React with 👍 / 👎.

@@ -1,5 +1,7 @@
from __future__ import annotations

from uuid import uuid4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use module-qualified uuid4 imports

The Python guideline in ai_guidelines/python.md asks function imports to use the containing module with dot notation, except for typing helpers. This added test import should use import uuid and call uuid.uuid4() instead of importing uuid4 directly.

Useful? React with 👍 / 👎.

@@ -1,4 +1,5 @@
from pathlib import Path
from uuid import uuid4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use module-qualified uuid4 imports

The Python guideline in ai_guidelines/python.md asks function imports to use the containing module with dot notation, except for typing helpers. This added test import should use import uuid and call uuid.uuid4() instead of importing uuid4 directly.

Useful? React with 👍 / 👎.

@@ -1,4 +1,5 @@
from pathlib import Path
from uuid import uuid4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use module-qualified uuid4 imports

The Python guideline in ai_guidelines/python.md asks function imports to use the containing module with dot notation, except for typing helpers. This added test import should use import uuid and call uuid.uuid4() instead of importing uuid4 directly.

Useful? React with 👍 / 👎.

if kept.batches
else np.empty((0, context.embedding_dimension), dtype=np.float32)
)
return BatchedEmbeddingResult(embeddings=embeddings, keys=kept.keys)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the crop docstring to match row order

This now returns kept.keys in grouped encoding order rather than the original input order, but the function docstring still says it preserves input order. Please update that wording so callers do not assume result.embeddings is input-aligned when the row order must be read from result.keys.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
lightly_studio/src/lightly_studio/dataset/image_embedding.py (1)

86-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate empty-input/validation logic with image_crop_embedding.py.

The "if not total_images: return empty BatchedEmbeddingResult" + "if context.max_batch_size <= 0: raise ValueError" block is duplicated verbatim in embed_image_crops_batched (image_crop_embedding.py, lines 47-56). Since both helpers are slated for coordinated changes to support per-item broken-file skipping (per PR objectives), extracting a shared helper now would reduce the risk of the two implementations drifting apart.

♻️ Proposed shared helper
+def _validate_batch_config(
+    total_items: int, embedding_dimension: int, max_batch_size: int
+) -> BatchedEmbeddingResult | None:
+    """Return an empty result if there's nothing to embed, else validate config."""
+    if not total_items:
+        return BatchedEmbeddingResult(
+            embeddings=np.empty((0, embedding_dimension), dtype=np.float32),
+            keys=[],
+        )
+    if max_batch_size <= 0:
+        raise ValueError("max_batch_size must be positive.")
+    return None

Then in both embed_image_files_batched and embed_image_crops_batched:

-    total_images = len(keyed_filepaths)
-    if not total_images:
-        return BatchedEmbeddingResult(
-            embeddings=np.empty((0, context.embedding_dimension), dtype=np.float32),
-            keys=[],
-        )
-
-    if context.max_batch_size <= 0:
-        raise ValueError("max_batch_size must be positive.")
+    total_images = len(keyed_filepaths)
+    if empty_result := _validate_batch_config(
+        total_images, context.embedding_dimension, context.max_batch_size
+    ):
+        return empty_result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lightly_studio/src/lightly_studio/dataset/image_embedding.py` around lines 86
- 95, The empty-input and max-batch-size validation in embed_image_files_batched
is duplicated in embed_image_crops_batched, so factor this shared setup into a
common helper used by both image_embedding and image_crop_embedding. Move the
“return empty BatchedEmbeddingResult when there are no items” and “raise
ValueError when context.max_batch_size <= 0” logic into the shared helper, then
call it from both embed_image_files_batched and embed_image_crops_batched to
keep the coordinated per-item skipping changes consistent.
lightly_studio/src/lightly_studio/dataset/embedding_manager.py (1)

292-298: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Note: result.embeddings[0] will need a length/result.keys check once broken-file skipping lands.

Currently safe since this PR guarantees exactly one embedding is returned for one input. Once the planned per-item skip logic (LIG-10034) is wired into embed_image_files_batched, a broken file here would yield an empty result.embeddings, causing an IndexError instead of a clear error message.

Not a blocker now, but worth tracking for the follow-up PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lightly_studio/src/lightly_studio/dataset/embedding_manager.py` around lines
292 - 298, The single-item embedding path in `embed_image` currently assumes
`result.embeddings[0]` always exists; when broken-file skipping is added to
`embed_image_files_batched`, a failed file could produce an empty result and
raise `IndexError`. Update `embed_image` to validate
`result.keys`/`result.embeddings` after `model.embed_images(...)`, and raise a
clear error (or otherwise handle the missing embedding) before indexing into
`result.embeddings[0]`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lightly_studio/tests/dataset/test_embedding_generator.py`:
- Around line 11-32: Add coverage in TestRandomEmbeddingGeneratorCrops for
RandomEmbeddingGenerator.embed_images, since only embed_image_crops currently
checks the keyed contract. Create a test that passes keyed filepaths through
embed_images and assert the returned result.keys preserves the same input keys,
using the existing RandomEmbeddingGenerator and result structure as the
reference point.

---

Nitpick comments:
In `@lightly_studio/src/lightly_studio/dataset/embedding_manager.py`:
- Around line 292-298: The single-item embedding path in `embed_image` currently
assumes `result.embeddings[0]` always exists; when broken-file skipping is added
to `embed_image_files_batched`, a failed file could produce an empty result and
raise `IndexError`. Update `embed_image` to validate
`result.keys`/`result.embeddings` after `model.embed_images(...)`, and raise a
clear error (or otherwise handle the missing embedding) before indexing into
`result.embeddings[0]`.

In `@lightly_studio/src/lightly_studio/dataset/image_embedding.py`:
- Around line 86-95: The empty-input and max-batch-size validation in
embed_image_files_batched is duplicated in embed_image_crops_batched, so factor
this shared setup into a common helper used by both image_embedding and
image_crop_embedding. Move the “return empty BatchedEmbeddingResult when there
are no items” and “raise ValueError when context.max_batch_size <= 0” logic into
the shared helper, then call it from both embed_image_files_batched and
embed_image_crops_batched to keep the coordinated per-item skipping changes
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 91a9b065-fa11-4b4c-a94b-166e6d17bcad

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa1ad6 and 27cc280.

📒 Files selected for processing (12)
  • lightly_studio/src/lightly_studio/dataset/embedding_generator.py
  • lightly_studio/src/lightly_studio/dataset/embedding_manager.py
  • lightly_studio/src/lightly_studio/dataset/image_crop_embedding.py
  • lightly_studio/src/lightly_studio/dataset/image_embedding.py
  • lightly_studio/src/lightly_studio/dataset/mobileclip_embedding_generator.py
  • lightly_studio/src/lightly_studio/dataset/perception_encoder_embedding_generator.py
  • lightly_studio/tests/dataset/test_embedding_generator.py
  • lightly_studio/tests/dataset/test_embedding_manager.py
  • lightly_studio/tests/dataset/test_image_crop_embedding.py
  • lightly_studio/tests/dataset/test_image_embedding.py
  • lightly_studio/tests/dataset/test_mobileclip_embedding_generator.py
  • lightly_studio/tests/dataset/test_perception_encoder_embedding_generator.py

Comment on lines 11 to +32
class TestRandomEmbeddingGeneratorCrops:
def test_embed_image_crops__returns_one_embedding_per_crop(self) -> None:
generator = RandomEmbeddingGenerator(dimension=4)
image_crops = [
ImageCrop(filepath="a.jpg", x=0, y=0, width=10, height=10),
ImageCrop(filepath="a.jpg", x=5, y=5, width=20, height=20),
ImageCrop(filepath="b.jpg", x=0, y=0, width=30, height=30),
keys = [uuid4(), uuid4(), uuid4()]
keyed_crops = [
(keys[0], ImageCrop(filepath="a.jpg", x=0, y=0, width=10, height=10)),
(keys[1], ImageCrop(filepath="a.jpg", x=5, y=5, width=20, height=20)),
(keys[2], ImageCrop(filepath="b.jpg", x=0, y=0, width=30, height=30)),
]

embeddings = generator.embed_image_crops(image_crops)
result = generator.embed_image_crops(keyed_crops)

assert embeddings.shape == (3, 4)
assert result.embeddings.shape == (3, 4)
assert result.keys == keys

def test_embed_image_crops__empty_input_returns_empty_array(self) -> None:
generator = RandomEmbeddingGenerator(dimension=4)

embeddings = generator.embed_image_crops([])
result = generator.embed_image_crops([])

assert embeddings.shape == (0, 4)
assert result.embeddings.shape == (0, 4)
assert result.keys == []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for tests exercising RandomEmbeddingGenerator.embed_images with the new keyed API.
rg -nP -C5 '\bembed_images\s*\(' lightly_studio/tests/dataset/test_embedding_generator.py
rg -nP 'class Test' lightly_studio/tests/dataset/test_embedding_generator.py

Repository: lightly-ai/lightly-studio

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File contents =="
cat -n lightly_studio/tests/dataset/test_embedding_generator.py

echo
echo "== Search for embed_images coverage =="
rg -n -C 3 'embed_images\s*\(' lightly_studio/tests

echo
echo "== Search for RandomEmbeddingGenerator references =="
rg -n -C 3 'RandomEmbeddingGenerator' lightly_studio/tests

Repository: lightly-ai/lightly-studio

Length of output: 27487


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Direct RandomEmbeddingGenerator embed_images test references =="
rg -n -C 3 'RandomEmbeddingGenerator.*embed_images|embed_images.*RandomEmbeddingGenerator' lightly_studio/tests lightly_studio | sed -n '1,200p'

echo
echo "== Any test names or assertions mentioning result.keys for embed_images =="
rg -n -C 2 'result\.keys|keys ==|keys\)' lightly_studio/tests/dataset/test_*embedding_generator.py lightly_studio/tests/dataset/test_embedding_manager.py | sed -n '1,240p'

Repository: lightly-ai/lightly-studio

Length of output: 230


Add embed_images coverage for RandomEmbeddingGenerator The keyed contract is only exercised for embed_image_crops here; add a test for embed_images that passes keyed filepaths and asserts result.keys matches the input keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lightly_studio/tests/dataset/test_embedding_generator.py` around lines 11 -
32, Add coverage in TestRandomEmbeddingGeneratorCrops for
RandomEmbeddingGenerator.embed_images, since only embed_image_crops currently
checks the keyed contract. Create a test that passes keyed filepaths through
embed_images and assert the returned result.keys preserves the same input keys,
using the existing RandomEmbeddingGenerator and result structure as the
reference point.

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