Spaces:
Sleeping
Sleeping
Commit ·
e335b6a
1
Parent(s): f6df0e9
fix(backend): embed_reel never raises and never returns an unstorable vector
Browse files- main.py +11 -5
- test_pipeline.py +20 -4
main.py
CHANGED
|
@@ -210,6 +210,7 @@ EMBEDDING_MODEL_NAME = os.getenv(
|
|
| 210 |
"EMBEDDING_MODEL_NAME",
|
| 211 |
"sentence-transformers/all-MiniLM-L6-v2",
|
| 212 |
)
|
|
|
|
| 213 |
CLUSTER_SAMPLE_SIZE = 4
|
| 214 |
MIN_SEMANTIC_CLUSTERS = 8
|
| 215 |
MAX_SEMANTIC_CLUSTERS = 12
|
|
@@ -261,15 +262,20 @@ def embed_reel(extracted_json: Optional[dict]) -> Optional[List[float]]:
|
|
| 261 |
Never raises: a reel must still save when embedding is unavailable
|
| 262 |
(backfill_embeddings.py repairs it later). Returns None when there is
|
| 263 |
nothing to embed."""
|
| 264 |
-
text = _reel_embedding_text(extracted_json)
|
| 265 |
-
if not text:
|
| 266 |
-
return None
|
| 267 |
try:
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
| 270 |
except Exception as e:
|
| 271 |
logger.warning(f"Could not embed reel for search: {getattr(e, 'detail', e)}")
|
| 272 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
|
| 275 |
def _target_cluster_count(item_count: int) -> int:
|
|
|
|
| 210 |
"EMBEDDING_MODEL_NAME",
|
| 211 |
"sentence-transformers/all-MiniLM-L6-v2",
|
| 212 |
)
|
| 213 |
+
EMBEDDING_DIMENSIONS = 384 # size of the saved_reels.embedding vector(384) column
|
| 214 |
CLUSTER_SAMPLE_SIZE = 4
|
| 215 |
MIN_SEMANTIC_CLUSTERS = 8
|
| 216 |
MAX_SEMANTIC_CLUSTERS = 12
|
|
|
|
| 262 |
Never raises: a reel must still save when embedding is unavailable
|
| 263 |
(backfill_embeddings.py repairs it later). Returns None when there is
|
| 264 |
nothing to embed."""
|
|
|
|
|
|
|
|
|
|
| 265 |
try:
|
| 266 |
+
text = _reel_embedding_text(extracted_json)
|
| 267 |
+
if not text:
|
| 268 |
+
return None
|
| 269 |
+
vector = [float(x) for x in next(iter(_get_embedding_model().embed([text])))]
|
| 270 |
except Exception as e:
|
| 271 |
logger.warning(f"Could not embed reel for search: {getattr(e, 'detail', e)}")
|
| 272 |
return None
|
| 273 |
+
if len(vector) != EMBEDDING_DIMENSIONS:
|
| 274 |
+
# pgvector would reject the whole row, losing the reel along with the vector.
|
| 275 |
+
logger.warning("Embedding has %s dimensions, expected %s; not storing it. Check EMBEDDING_MODEL_NAME.",
|
| 276 |
+
len(vector), EMBEDDING_DIMENSIONS)
|
| 277 |
+
return None
|
| 278 |
+
return vector
|
| 279 |
|
| 280 |
|
| 281 |
def _target_cluster_count(item_count: int) -> int:
|
test_pipeline.py
CHANGED
|
@@ -24,6 +24,7 @@ client = TestClient(main.app)
|
|
| 24 |
|
| 25 |
REEL_URL = "https://www.instagram.com/reel/Z/"
|
| 26 |
LONG_TRANSCRIPT = " ".join(["word"] * 30) # clears the 15-word silent-hook guard
|
|
|
|
| 27 |
EXTRACTION = ReelExtraction(
|
| 28 |
core_topic="Topic", key_takeaway="Takeaway", action_items=["a"], tools_or_resources=["b"])
|
| 29 |
|
|
@@ -111,7 +112,7 @@ class FakeEmbeddingModel:
|
|
| 111 |
|
| 112 |
def embed(self, texts):
|
| 113 |
self.texts.extend(texts)
|
| 114 |
-
return (
|
| 115 |
|
| 116 |
|
| 117 |
@pytest.fixture(autouse=True)
|
|
@@ -309,7 +310,7 @@ def test_embed_reel_uses_topic_takeaway_and_tools(embedding_model):
|
|
| 309 |
vector = main.embed_reel({
|
| 310 |
"core_topic": "AI email tools", "key_takeaway": "Automate replies",
|
| 311 |
"tools_or_resources": ["Zapier", "ChatGPT"], "action_items": ["not embedded"]})
|
| 312 |
-
assert vector ==
|
| 313 |
assert embedding_model.texts == ["AI email tools Automate replies Zapier, ChatGPT"]
|
| 314 |
|
| 315 |
|
|
@@ -323,6 +324,21 @@ def test_embed_reel_returns_none_when_model_unavailable():
|
|
| 323 |
assert main.embed_reel({"core_topic": "AI"}) is None
|
| 324 |
|
| 325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
def test_every_save_path_stores_an_embedding(monkeypatch, pipeline, embedding_model):
|
| 327 |
saved = []
|
| 328 |
|
|
@@ -341,7 +357,7 @@ def test_every_save_path_stores_an_embedding(monkeypatch, pipeline, embedding_mo
|
|
| 341 |
files = {"file": ("clip.mp3", io.BytesIO(b"audio"), "audio/mpeg")}
|
| 342 |
assert client.post("/extract/file", files=files).status_code == 200
|
| 343 |
assert main.worker_tick() is True
|
| 344 |
-
assert saved == [
|
| 345 |
|
| 346 |
|
| 347 |
def test_reel_still_saves_when_embedding_fails(monkeypatch):
|
|
@@ -368,7 +384,7 @@ def test_backfill_embeds_only_rows_with_content(monkeypatch, embedding_model):
|
|
| 368 |
saved = {}
|
| 369 |
monkeypatch.setattr(db, "set_embedding", lambda reel_id, embedding: saved.update({reel_id: embedding}))
|
| 370 |
assert backfill_embeddings.backfill() == (2, 1)
|
| 371 |
-
assert saved == {"a":
|
| 372 |
|
| 373 |
|
| 374 |
def test_backfill_fails_loudly_without_a_model(monkeypatch):
|
|
|
|
| 24 |
|
| 25 |
REEL_URL = "https://www.instagram.com/reel/Z/"
|
| 26 |
LONG_TRANSCRIPT = " ".join(["word"] * 30) # clears the 15-word silent-hook guard
|
| 27 |
+
FAKE_VECTOR = [0.25] * 384 # same size as the saved_reels.embedding vector(384) column
|
| 28 |
EXTRACTION = ReelExtraction(
|
| 29 |
core_topic="Topic", key_takeaway="Takeaway", action_items=["a"], tools_or_resources=["b"])
|
| 30 |
|
|
|
|
| 112 |
|
| 113 |
def embed(self, texts):
|
| 114 |
self.texts.extend(texts)
|
| 115 |
+
return (FAKE_VECTOR for _ in texts)
|
| 116 |
|
| 117 |
|
| 118 |
@pytest.fixture(autouse=True)
|
|
|
|
| 310 |
vector = main.embed_reel({
|
| 311 |
"core_topic": "AI email tools", "key_takeaway": "Automate replies",
|
| 312 |
"tools_or_resources": ["Zapier", "ChatGPT"], "action_items": ["not embedded"]})
|
| 313 |
+
assert vector == FAKE_VECTOR
|
| 314 |
assert embedding_model.texts == ["AI email tools Automate replies Zapier, ChatGPT"]
|
| 315 |
|
| 316 |
|
|
|
|
| 324 |
assert main.embed_reel({"core_topic": "AI"}) is None
|
| 325 |
|
| 326 |
|
| 327 |
+
def test_embed_reel_never_raises_on_malformed_extraction(embedding_model):
|
| 328 |
+
assert main.embed_reel("a double-encoded string") is None
|
| 329 |
+
assert main.embed_reel(["a", "list"]) is None
|
| 330 |
+
assert main.embed_reel({"core_topic": "AI", "tools_or_resources": 5}) is None
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def test_embed_reel_drops_a_vector_the_database_cannot_store(monkeypatch):
|
| 334 |
+
class WrongSizeModel:
|
| 335 |
+
def embed(self, texts):
|
| 336 |
+
return ([0.1, 0.2, 0.3] for _ in texts)
|
| 337 |
+
|
| 338 |
+
monkeypatch.setattr(main, "_get_embedding_model", lambda: WrongSizeModel())
|
| 339 |
+
assert main.embed_reel({"core_topic": "AI"}) is None
|
| 340 |
+
|
| 341 |
+
|
| 342 |
def test_every_save_path_stores_an_embedding(monkeypatch, pipeline, embedding_model):
|
| 343 |
saved = []
|
| 344 |
|
|
|
|
| 357 |
files = {"file": ("clip.mp3", io.BytesIO(b"audio"), "audio/mpeg")}
|
| 358 |
assert client.post("/extract/file", files=files).status_code == 200
|
| 359 |
assert main.worker_tick() is True
|
| 360 |
+
assert saved == [FAKE_VECTOR] * 4
|
| 361 |
|
| 362 |
|
| 363 |
def test_reel_still_saves_when_embedding_fails(monkeypatch):
|
|
|
|
| 384 |
saved = {}
|
| 385 |
monkeypatch.setattr(db, "set_embedding", lambda reel_id, embedding: saved.update({reel_id: embedding}))
|
| 386 |
assert backfill_embeddings.backfill() == (2, 1)
|
| 387 |
+
assert saved == {"a": FAKE_VECTOR, "c": FAKE_VECTOR}
|
| 388 |
|
| 389 |
|
| 390 |
def test_backfill_fails_loudly_without_a_model(monkeypatch):
|