feat: Add thumbnail management for albums and people, implement face embedding functionality
This commit is contained in:
@@ -3,9 +3,9 @@ use std::{path::PathBuf, sync::Arc};
|
||||
use futures_util::StreamExt;
|
||||
use libertas_core::plugins::PluginContext;
|
||||
use libertas_infra::factory::{
|
||||
build_album_repository, build_database_pool, build_face_region_repository,
|
||||
build_media_metadata_repository, build_media_repository, build_person_repository,
|
||||
build_tag_repository, build_user_repository,
|
||||
build_album_repository, build_database_pool, build_face_embedding_repository,
|
||||
build_face_region_repository, build_media_metadata_repository, build_media_repository,
|
||||
build_person_repository, build_tag_repository, build_user_repository,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tokio::fs;
|
||||
@@ -45,6 +45,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
let tag_repo = build_tag_repository(&config.database, db_pool.clone()).await?;
|
||||
let person_repo = build_person_repository(&config.database, db_pool.clone()).await?;
|
||||
let face_region_repo = build_face_region_repository(&config.database, db_pool.clone()).await?;
|
||||
let face_embedding_repo =
|
||||
build_face_embedding_repository(&config.database, db_pool.clone()).await?;
|
||||
|
||||
let context = Arc::new(PluginContext {
|
||||
media_repo,
|
||||
@@ -53,6 +55,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
tag_repo,
|
||||
person_repo,
|
||||
face_region_repo,
|
||||
face_embedding_repo,
|
||||
metadata_repo,
|
||||
media_library_path: config.media_library_path.clone(),
|
||||
config: Arc::new(config.clone()),
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use libertas_core::{
|
||||
ai::FaceDetector,
|
||||
config::{AiConfig, AppConfig, FaceDetectorRuntime},
|
||||
ai::{FaceDetector, FaceEmbedder},
|
||||
config::{AiConfig, AppConfig, FaceDetectorRuntime, FaceEmbedderRuntime},
|
||||
error::{CoreError, CoreResult},
|
||||
models::Media,
|
||||
plugins::{MediaProcessorPlugin, PluginContext},
|
||||
};
|
||||
use libertas_infra::ai::{
|
||||
remote_detector::RemoteNatsFaceDetector, tract_detector::TractFaceDetector,
|
||||
tract_embedder::TractFaceEmbedder,
|
||||
};
|
||||
|
||||
use crate::plugins::{
|
||||
exif_reader::ExifReaderPlugin, face_detector::FaceDetectionPlugin, thumbnail::ThumbnailPlugin,
|
||||
xmp_writer::XmpWriterPlugin,
|
||||
embedding_generator::EmbeddingGeneratorPlugin, exif_reader::ExifReaderPlugin,
|
||||
face_detector::FaceDetectionPlugin, thumbnail::ThumbnailPlugin, xmp_writer::XmpWriterPlugin,
|
||||
};
|
||||
|
||||
pub struct PluginManager {
|
||||
@@ -25,7 +26,7 @@ impl PluginManager {
|
||||
let mut plugins: Vec<Arc<dyn MediaProcessorPlugin>> = Vec::new();
|
||||
|
||||
if let Some(ai_config) = &config.ai_config {
|
||||
match build_face_detector(ai_config, nats_client) {
|
||||
match build_face_detector(ai_config, nats_client.clone()) {
|
||||
Ok(detector) => {
|
||||
plugins.push(Arc::new(FaceDetectionPlugin::new(detector)));
|
||||
println!("FaceDetectionPlugin loaded.");
|
||||
@@ -34,6 +35,16 @@ impl PluginManager {
|
||||
eprintln!("Failed to load FaceDetectionPlugin: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match build_face_embedder(ai_config, nats_client.clone()) {
|
||||
Ok(embedder) => {
|
||||
plugins.push(Arc::new(EmbeddingGeneratorPlugin::new(embedder)));
|
||||
println!("EmbeddingGeneratorPlugin loaded.");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to load EmbeddingGeneratorPlugin: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins.push(Arc::new(ExifReaderPlugin));
|
||||
@@ -86,3 +97,27 @@ fn build_face_detector(
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_face_embedder(
|
||||
config: &AiConfig,
|
||||
_nats_client: async_nats::Client,
|
||||
) -> CoreResult<Box<dyn FaceEmbedder>> {
|
||||
match &config.face_embedder_runtime {
|
||||
FaceEmbedderRuntime::Tract => {
|
||||
let model_path =
|
||||
config
|
||||
.face_embedder_model_path
|
||||
.as_deref()
|
||||
.ok_or(CoreError::Config(
|
||||
"Tract runtime needs 'face_embedder_model_path'".to_string(),
|
||||
))?;
|
||||
Ok(Box::new(TractFaceEmbedder::new(model_path)?))
|
||||
}
|
||||
FaceEmbedderRuntime::Onnx => {
|
||||
unimplemented!("ONNX face embedder not implemented yet");
|
||||
}
|
||||
FaceEmbedderRuntime::RemoteNats { subject: _ } => {
|
||||
unimplemented!("RemoteNats face embedder not implemented yet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
110
libertas_worker/src/plugins/embedding_generator.rs
Normal file
110
libertas_worker/src/plugins/embedding_generator.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use std::{io::Cursor, path::PathBuf};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use image::{ImageFormat, ImageReader};
|
||||
use libertas_core::{
|
||||
ai::FaceEmbedder,
|
||||
error::{CoreError, CoreResult},
|
||||
models::{FaceEmbedding, Media},
|
||||
plugins::{MediaProcessorPlugin, PluginContext, PluginData},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
pub struct EmbeddingGeneratorPlugin {
|
||||
embedder: Box<dyn FaceEmbedder>,
|
||||
model_id: i16,
|
||||
}
|
||||
|
||||
impl EmbeddingGeneratorPlugin {
|
||||
pub fn new(embedder: Box<dyn FaceEmbedder>) -> Self {
|
||||
Self {
|
||||
embedder,
|
||||
model_id: 1, // todo: come from config or something
|
||||
}
|
||||
}
|
||||
|
||||
fn f32_vec_to_bytes(vec: &[f32]) -> Vec<u8> {
|
||||
vec.iter().flat_map(|&f| f.to_le_bytes()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MediaProcessorPlugin for EmbeddingGeneratorPlugin {
|
||||
fn name(&self) -> &'static str {
|
||||
"embedding_generator"
|
||||
}
|
||||
|
||||
async fn process(&self, media: &Media, context: &PluginContext) -> CoreResult<PluginData> {
|
||||
if !media.mime_type.starts_with("image/") {
|
||||
return Ok(PluginData {
|
||||
message: "Not an image, skipping.".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Get all face regions for this media
|
||||
let faces = context.face_region_repo.find_by_media_id(media.id).await?;
|
||||
if faces.is_empty() {
|
||||
return Ok(PluginData {
|
||||
message: "No faces found to embed.".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Load the full original image
|
||||
let file_path = PathBuf::from(&context.media_library_path).join(&media.storage_path);
|
||||
let image_bytes = fs::read(file_path).await?;
|
||||
let img = ImageReader::new(Cursor::new(&image_bytes))
|
||||
.with_guessed_format()?
|
||||
.decode()
|
||||
.map_err(|e| CoreError::Unknown(format!("Failed to decode image: {}", e)))?;
|
||||
|
||||
let mut new_embeddings = 0;
|
||||
|
||||
for face in faces {
|
||||
// 3. Check if embedding already exists
|
||||
if context
|
||||
.face_embedding_repo
|
||||
.find_by_face_region_id(face.id)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Crop the face from the main image
|
||||
let cropped_face = img.crop_imm(
|
||||
face.x_min as u32,
|
||||
face.y_min as u32,
|
||||
(face.x_max - face.x_min) as u32,
|
||||
(face.y_max - face.y_min) as u32,
|
||||
);
|
||||
|
||||
// 5. Convert cropped image back to bytes (as JPEG)
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
cropped_face
|
||||
.write_to(&mut buf, ImageFormat::Jpeg)
|
||||
.map_err(|e| {
|
||||
CoreError::Unknown(format!("Failed to encode cropped image: {}", e))
|
||||
})?;
|
||||
let cropped_bytes = buf.into_inner();
|
||||
|
||||
// 6. Generate the embedding
|
||||
let embedding_f32 = self.embedder.generate_embedding(&cropped_bytes).await?;
|
||||
let embedding_bytes = Self::f32_vec_to_bytes(&embedding_f32);
|
||||
|
||||
// 7. Save to database
|
||||
let embedding_model = FaceEmbedding {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
face_region_id: face.id,
|
||||
model_id: self.model_id,
|
||||
embedding: embedding_bytes,
|
||||
};
|
||||
|
||||
context.face_embedding_repo.create(&embedding_model).await?;
|
||||
new_embeddings += 1;
|
||||
}
|
||||
|
||||
Ok(PluginData {
|
||||
message: format!("Generated {} new embeddings.", new_embeddings),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod embedding_generator;
|
||||
pub mod exif_reader;
|
||||
pub mod face_detector;
|
||||
pub mod thumbnail;
|
||||
|
||||
Reference in New Issue
Block a user