feat: add recognition engine with CLIP matching and Ollama vision LLM
- OllamaClient: analyze_image, compare_images, is_available - Recognition pipeline: CLIP embedding -> cosine similarity -> threshold routing (known >0.85, unknown <0.60, ambiguous -> Ollama fallback) - Vision dispatcher: get_description() supports haiku/ollama/both backends - 9 embedding serialization tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""Tests for the recognition module — embedding serialization helpers."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from recognition import bytes_to_embedding, embedding_to_bytes
|
||||
|
||||
|
||||
class TestEmbeddingToBytes:
|
||||
"""Tests for embedding_to_bytes()."""
|
||||
|
||||
def test_roundtrip_preserves_values(self):
|
||||
"""Packing and unpacking should return the same values."""
|
||||
original = [0.1, 0.2, 0.3, -0.5, 1.0, 0.0]
|
||||
packed = embedding_to_bytes(original)
|
||||
recovered = bytes_to_embedding(packed)
|
||||
np.testing.assert_allclose(recovered, original, rtol=1e-6)
|
||||
|
||||
def test_correct_byte_length_768(self):
|
||||
"""A 768-dim embedding should produce 768 * 4 = 3072 bytes."""
|
||||
embedding = [0.0] * 768
|
||||
packed = embedding_to_bytes(embedding)
|
||||
assert len(packed) == 768 * 4
|
||||
|
||||
def test_correct_byte_length_arbitrary(self):
|
||||
"""Byte length should always be len(embedding) * 4."""
|
||||
for dim in [1, 10, 512, 1024]:
|
||||
embedding = [1.0] * dim
|
||||
packed = embedding_to_bytes(embedding)
|
||||
assert len(packed) == dim * 4
|
||||
|
||||
def test_empty_embedding(self):
|
||||
"""An empty embedding should produce 0 bytes."""
|
||||
packed = embedding_to_bytes([])
|
||||
assert len(packed) == 0
|
||||
recovered = bytes_to_embedding(packed)
|
||||
assert len(recovered) == 0
|
||||
|
||||
|
||||
class TestBytesToEmbedding:
|
||||
"""Tests for bytes_to_embedding()."""
|
||||
|
||||
def test_correct_shape(self):
|
||||
"""Should return a 1-D array with the correct number of elements."""
|
||||
embedding = [0.5] * 768
|
||||
packed = embedding_to_bytes(embedding)
|
||||
recovered = bytes_to_embedding(packed)
|
||||
assert recovered.shape == (768,)
|
||||
|
||||
def test_correct_dtype(self):
|
||||
"""Should return float32 numpy array."""
|
||||
embedding = [0.1, 0.2, 0.3]
|
||||
packed = embedding_to_bytes(embedding)
|
||||
recovered = bytes_to_embedding(packed)
|
||||
assert recovered.dtype == np.float32
|
||||
|
||||
def test_negative_values(self):
|
||||
"""Negative values should survive roundtrip."""
|
||||
original = [-1.0, -0.5, 0.0, 0.5, 1.0]
|
||||
packed = embedding_to_bytes(original)
|
||||
recovered = bytes_to_embedding(packed)
|
||||
np.testing.assert_allclose(recovered, original, rtol=1e-6)
|
||||
|
||||
def test_large_values(self):
|
||||
"""Large float values should survive roundtrip within float32 precision."""
|
||||
original = [1e10, -1e10, 3.14159, 2.71828]
|
||||
packed = embedding_to_bytes(original)
|
||||
recovered = bytes_to_embedding(packed)
|
||||
np.testing.assert_allclose(recovered, original, rtol=1e-5)
|
||||
|
||||
def test_realistic_clip_embedding(self):
|
||||
"""Simulate a realistic CLIP embedding (768-dim, normalized)."""
|
||||
rng = np.random.default_rng(42)
|
||||
vec = rng.standard_normal(768).astype(np.float32)
|
||||
vec = vec / np.linalg.norm(vec) # normalize
|
||||
|
||||
packed = embedding_to_bytes(vec.tolist())
|
||||
recovered = bytes_to_embedding(packed)
|
||||
|
||||
assert recovered.shape == (768,)
|
||||
assert recovered.dtype == np.float32
|
||||
np.testing.assert_allclose(recovered, vec, rtol=1e-6)
|
||||
Reference in New Issue
Block a user