feat(faces): domain entities, ports & no-op analyzer

Phase 2 increment 2:
- domain: Face, Person, BoundingBox, DetectedFace (512-d embeddings).
- ports: FaceAnalyzerPort (detect + embed from raw bytes; decodes internally
  so the application layer stays image/ML-crate agnostic) and FaceRepository
  (user-scoped face/person persistence).
- DTOs: PersonDto, FaceBoxDto.
- NoopFaceAnalyzer — reports is_ready()==false and returns no faces, so the
  whole People pipeline compiles and runs inert until the operator wires a
  real ONNX-backed analyzer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
This commit is contained in:
Claude
2026-06-19 11:34:46 +00:00
parent 7cb79d41b9
commit 524e573756
8 changed files with 210 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
//! Domain entities for the People (faces) feature.
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Length of a face embedding vector (ArcFace-style).
pub const EMBEDDING_DIM: usize = 512;
/// A face bounding box in normalized image coordinates (each component 0..1).
#[derive(Debug, Clone, Copy)]
pub struct BoundingBox {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl BoundingBox {
/// `[x, y, w, h]` — the storage representation (Postgres `REAL[]`).
pub fn to_array(self) -> Vec<f32> {
vec![self.x, self.y, self.w, self.h]
}
/// Build from a stored `[x, y, w, h]` array; missing components default to 0.
pub fn from_slice(a: &[f32]) -> Self {
Self {
x: a.first().copied().unwrap_or(0.0),
y: a.get(1).copied().unwrap_or(0.0),
w: a.get(2).copied().unwrap_or(0.0),
h: a.get(3).copied().unwrap_or(0.0),
}
}
}
/// A face produced by the analyzer but not yet persisted: where it is, how
/// confident the detector was, an optional quality score, and a 512-d,
/// L2-normalized embedding.
#[derive(Debug, Clone)]
pub struct DetectedFace {
pub bbox: BoundingBox,
pub det_score: f32,
pub quality: Option<f32>,
pub embedding: Vec<f32>,
}
/// A persisted face detection.
#[derive(Debug, Clone)]
pub struct Face {
pub id: Uuid,
pub file_id: Uuid,
pub user_id: Uuid,
/// Identity cluster this face belongs to, if any.
pub person_id: Option<Uuid>,
pub bbox: BoundingBox,
pub det_score: f32,
pub quality: Option<f32>,
pub embedding: Vec<f32>,
pub blob_hash: Option<String>,
pub created_at: DateTime<Utc>,
}
/// An identity cluster ("person"). `display_name` is `None` until the user
/// names it.
#[derive(Debug, Clone)]
pub struct Person {
pub id: Uuid,
pub user_id: Uuid,
pub display_name: Option<String>,
pub cover_face_id: Option<Uuid>,
pub is_hidden: bool,
pub created_at: DateTime<Utc>,
}
+1
View File
@@ -4,6 +4,7 @@ pub mod calendar_event;
pub mod contact;
pub mod device_code;
pub mod entity_errors;
pub mod face;
pub mod file;
pub mod folder;
pub mod magic_link_token;