""" Face embedding clustering using DBSCAN with cosine distance. Called by the periodic `recluster_faces` Celery task (PR7). """ import logging import numpy as np from sklearn.cluster import DBSCAN logger = logging.getLogger(__name__) def cluster_faces( embeddings: np.ndarray, eps: float = 0.35, min_samples: int = 2, ) -> np.ndarray: """Cluster face embeddings using DBSCAN with cosine metric. Args: embeddings: (N, D) float32 array of L2-normalized face embeddings. eps: Maximum cosine distance between two samples to be in the same neighborhood. Lower = tighter clusters. min_samples: Minimum cluster size. Returns: (N,) int array of cluster labels. -1 = noise / unclustered. """ if len(embeddings) < min_samples: return np.full(len(embeddings), -1, dtype=int) db = DBSCAN(eps=eps, min_samples=min_samples, metric="cosine") labels = db.fit_predict(embeddings) n_clusters = len(set(labels) - {-1}) n_noise = (labels == -1).sum() logger.info( "Face clustering: %d embeddings → %d clusters, %d noise", len(embeddings), n_clusters, n_noise, ) return labels