face-intel / storage /reference_store.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
3.68 kB
"""
Reference store — known-faces gallery.
Stores per-person face embeddings (numpy arrays) on disk plus a JSON
manifest mapping person_name -> list of embedding filenames. Used by
recognition providers as the "known faces" database.
This is the ONLY place gallery mutations happen. Recognition providers
read from here; they never write.
"""
from __future__ import annotations
import json
import threading
from pathlib import Path
from typing import Dict, List
import numpy as np
from loguru import logger
class ReferenceStore:
"""Disk-backed known-faces gallery."""
def __init__(self, root: Path) -> None:
self._root = Path(root)
self._root.mkdir(parents=True, exist_ok=True)
self._manifest_path = self._root / "manifest.json"
self._lock = threading.RLock()
self._manifest: Dict[str, List[str]] = {}
self._load()
# ------------------------------------------------------------------ #
# Manifest persistence
# ------------------------------------------------------------------ #
def _load(self) -> None:
if self._manifest_path.exists():
try:
self._manifest = json.loads(self._manifest_path.read_text())
except json.JSONDecodeError:
logger.warning("Gallery manifest corrupted, starting fresh")
self._manifest = {}
else:
self._manifest = {}
def _save(self) -> None:
self._manifest_path.write_text(json.dumps(self._manifest, indent=2))
# ------------------------------------------------------------------ #
# CRUD
# ------------------------------------------------------------------ #
def add(self, name: str, embedding: np.ndarray) -> None:
"""Add an embedding for a person."""
name = name.strip().lower().replace(" ", "_")
with self._lock:
if name not in self._manifest:
self._manifest[name] = []
idx = len(self._manifest[name])
emb_path = self._root / f"{name}_{idx}.npy"
np.save(emb_path, embedding.astype(np.float32))
self._manifest[name].append(emb_path.name)
self._save()
def remove(self, name: str) -> bool:
name = name.strip().lower().replace(" ", "_")
with self._lock:
if name not in self._manifest:
return False
for fname in self._manifest[name]:
(self._root / fname).unlink(missing_ok=True)
del self._manifest[name]
self._save()
return True
def clear(self) -> int:
with self._lock:
n = len(self._manifest)
for name, files in self._manifest.items():
for f in files:
(self._root / f).unlink(missing_ok=True)
self._manifest = {}
self._save()
return n
def list_persons(self) -> List[dict]:
with self._lock:
return [
{"name": name, "num_embeddings": len(files)}
for name, files in self._manifest.items()
]
def get_embeddings(self, name: str) -> List[np.ndarray]:
name = name.strip().lower().replace(" ", "_")
with self._lock:
files = self._manifest.get(name, [])
return [np.load(self._root / f) for f in files]
def get_all(self) -> Dict[str, List[np.ndarray]]:
with self._lock:
return {
name: [np.load(self._root / f) for f in files]
for name, files in self._manifest.items()
}
@property
def root(self) -> Path:
return self._root