Video-Scout / src /memory /vector_index.py
ashleshp's picture
first commit
fca155a
import numpy as np
from pathlib import Path
from typing import List, Tuple, Dict, Any, Optional
from sklearn.metrics.pairwise import cosine_similarity
import pickle
class VectorIndex:
"""
In-memory Vector Database.
This acts as the 'Long Term Memory' for visual concepts.
It maps a Timestamp (when something happened) to a Vector (what it looked like).
"""
def __init__(self, index_file_path: Path):
self.file_path = index_file_path
self.timestamps: List[float] = []
self.embedding_matrix: Optional[np.ndarray] = None
self.metadata_store: List[Dict[str, Any]] = []
# Load existing index if available
if self.file_path.exists():
self.load()
def add(self, timestamp_seconds: float, vector: np.ndarray, extra_data: Dict[str, Any] = None):
"""Adds a new memory entry (timestamp + vector)."""
self.timestamps.append(timestamp_seconds)
self.metadata_store.append(extra_data or {})
# Normalize the vector to length 1.
# This is crucial so that 'Cosine Similarity' is just a Dot Product (faster).
vector_norm = np.linalg.norm(vector)
if vector_norm > 0:
vector = vector / vector_norm
if self.embedding_matrix is None:
self.embedding_matrix = vector.reshape(1, -1)
else:
self.embedding_matrix = np.vstack([self.embedding_matrix, vector])
def search(self, query_vector: np.ndarray, top_k: int = 5) -> List[Tuple[float, float]]:
"""
Finds the moments in the video that are most similar to the query.
Returns:
A list of tuples: (timestamp_seconds, similarity_score)
"""
if self.embedding_matrix is None:
return []
# Normalize the query too
query_norm = np.linalg.norm(query_vector)
if query_norm > 0:
query_vector = query_vector / query_norm
# Calculate similarity against ALL stored memories at once
similarity_scores = cosine_similarity(query_vector.reshape(1, -1), self.embedding_matrix)[0]
# Sort by highest score first
best_indices = np.argsort(similarity_scores)[::-1][:top_k]
results = []
for index in best_indices:
score = float(similarity_scores[index])
time_point = self.timestamps[index]
results.append((time_point, score))
return results
def save(self):
"""Persists the index to the disk using Pickle."""
data_packet = {
"timestamps": self.timestamps,
"vectors": self.embedding_matrix,
"metadata": self.metadata_store
}
with open(self.file_path, "wb") as f:
pickle.dump(data_packet, f)
def load(self):
"""Loads the index from disk."""
with open(self.file_path, "rb") as f:
data_packet = pickle.load(f)
self.timestamps = data_packet["timestamps"]
self.embedding_matrix = data_packet["vectors"]
self.metadata_store = data_packet.get("metadata", [])