feat(faces): indexing pipeline + DI wiring

Phase 2 increment 5:
- FaceIndexingService: a FileLifecycleHook that, on image upload, detects +
  embeds faces in a background task and stores them. Dedup-aware (clones an
  identical blob's faces instead of re-running inference), reindexes on
  overwrite, and relies on the DB cascade for deletes. Completely inert when
  no model is ready.
- DI: registers the hook in the FileLifecycleService chain and exposes
  PeopleService in AppState — both gated on OXICLOUD_ENABLE_FACES, both
  using the default no-op analyzer until the operator wires a real ONNX
  model.

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:46:57 +00:00
parent de95de013d
commit 1dca9d7d05
3 changed files with 233 additions and 0 deletions
+41
View File
@@ -17,6 +17,7 @@ use crate::application::services::folder_service::FolderService;
use crate::application::services::i18n_application_service::I18nApplicationService;
use crate::application::services::nextcloud_file_id_service::NextcloudFileIdService;
use crate::application::services::nextcloud_login_flow_service::NextcloudLoginFlowService;
use crate::application::services::people_service::PeopleService;
use crate::application::services::places_service::PlacesService;
use crate::application::services::recent_service::RecentService;
use crate::application::services::search_service::SearchService;
@@ -360,6 +361,9 @@ impl AppServiceFactory {
fls = fls.with_hook(audio.clone());
}
fls = fls.with_hook(media_metadata_service.clone());
if self.config.features.enable_faces {
fls = fls.with_hook(self.create_face_indexing_service(db_pool));
}
let file_lifecycle = Arc::new(fls);
Ok(CoreServices {
@@ -810,6 +814,34 @@ impl AppServiceFactory {
service
}
/// Creates the face-indexing lifecycle hook (People feature). Uses the
/// default no-op analyzer until the operator wires a real ONNX model.
pub fn create_face_indexing_service(
&self,
db_pool: &Arc<PgPool>,
) -> Arc<crate::infrastructure::services::face_indexing_service::FaceIndexingService> {
let blob_root = self.storage_path.join(".blobs");
let analyzer: Arc<dyn crate::application::ports::face_ports::FaceAnalyzerPort> =
Arc::new(crate::infrastructure::services::noop_face_analyzer::NoopFaceAnalyzer);
Arc::new(
crate::infrastructure::services::face_indexing_service::FaceIndexingService::new(
db_pool.clone(),
blob_root,
analyzer,
),
)
}
/// Creates the People (faces) read/clustering service.
pub fn create_people_service(&self, db_pool: &Arc<PgPool>) -> Arc<PeopleService> {
let repo = Arc::new(
crate::infrastructure::repositories::pg::FacePgRepository::new(db_pool.clone()),
);
let service = Arc::new(PeopleService::new(repo));
tracing::info!("People service initialized");
service
}
/// Preloads translations for every locale in the registry. Build
/// the registry at startup via `LocaleRegistry::discover` and pass
/// the resulting list here.
@@ -1018,6 +1050,7 @@ impl AppServiceFactory {
let favorites_service: Option<Arc<FavoritesService>>;
let recent_service: Option<Arc<RecentService>>;
let places_service: Option<Arc<PlacesService>>;
let people_service: Option<Arc<PeopleService>>;
let storage_usage_service: Option<Arc<StorageUsageService>>;
let mut auth_services: Option<crate::common::di::AuthServices> = None;
let mut nextcloud_services: Option<NextcloudServices> = None;
@@ -1046,6 +1079,12 @@ impl AppServiceFactory {
None
};
people_service = if core.config.features.enable_faces {
Some(self.create_people_service(&pool))
} else {
None
};
storage_usage_service = Some(storage_usage.clone());
self.start_tree_etag_flush_job(&maintenance_pool);
@@ -1273,6 +1312,7 @@ impl AppServiceFactory {
favorites_service,
recent_service,
places_service,
people_service,
storage_usage_service,
calendar_service: None,
contact_service: None,
@@ -1720,6 +1760,7 @@ pub struct AppState {
pub favorites_service: Option<Arc<FavoritesService>>,
pub recent_service: Option<Arc<RecentService>>,
pub places_service: Option<Arc<PlacesService>>,
pub people_service: Option<Arc<PeopleService>>,
pub storage_usage_service: Option<Arc<StorageUsageService>>,
pub calendar_service: Option<Arc<CalendarService>>,
pub contact_service: Option<Arc<ContactStorageAdapter>>,