feat: Add thumbnail management for albums and people, implement face embedding functionality
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
pub mod remote_detector;
|
||||
pub mod tract_detector;
|
||||
pub mod tract_embedder;
|
||||
|
||||
89
libertas_infra/src/ai/tract_embedder.rs
Normal file
89
libertas_infra/src/ai/tract_embedder.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use async_trait::async_trait;
|
||||
use image::imageops;
|
||||
use libertas_core::{
|
||||
ai::FaceEmbedder,
|
||||
error::{CoreError, CoreResult},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::task;
|
||||
|
||||
use tract_onnx::{prelude::*, tract_core::ndarray::Array4};
|
||||
|
||||
type TractModel = SimplePlan<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>;
|
||||
|
||||
pub struct TractFaceEmbedder {
|
||||
model: Arc<TractModel>,
|
||||
}
|
||||
|
||||
impl TractFaceEmbedder {
|
||||
pub fn new(model_path: &str) -> CoreResult<Self> {
|
||||
let model = tract_onnx::onnx()
|
||||
.model_for_path(model_path)
|
||||
.map_err(|e| CoreError::Config(format!("Failed to load embedding model: {}", e)))?
|
||||
.with_input_fact(0, f32::fact([1, 112, 112, 3]).into())
|
||||
.map_err(|e| CoreError::Config(format!("Failed to set input fact: {}", e)))?
|
||||
.into_optimized()
|
||||
.map_err(|e| CoreError::Config(format!("Failed to optimize model: {}", e)))?
|
||||
.into_runnable()
|
||||
.map_err(|e| CoreError::Config(format!("Failed to make model runnable: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
model: Arc::new(model),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FaceEmbedder for TractFaceEmbedder {
|
||||
async fn generate_embedding(&self, image_bytes: &[u8]) -> CoreResult<Vec<f32>> {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let image_bytes = image_bytes.to_vec();
|
||||
let model = self.model.clone();
|
||||
|
||||
let embedding = task::spawn_blocking(move || {
|
||||
println!("Running face embedding locally on the CPU...");
|
||||
|
||||
let img = image::load_from_memory(&image_bytes)
|
||||
.map_err(|e| CoreError::Unknown(format!("Failed to load cropped face: {}", e)))?;
|
||||
|
||||
let resized = imageops::resize(&img, 112, 112, imageops::FilterType::Triangle);
|
||||
|
||||
let tensor: Tensor = Array4::from_shape_fn((1, 112, 112, 3), |(_, y, x, c)| {
|
||||
(resized.get_pixel(x as u32, y as u32)[c] as f32 - 127.5) / 128.0
|
||||
})
|
||||
.into();
|
||||
|
||||
let result = model
|
||||
.run(tvec!(tensor.into()))
|
||||
.map_err(|e| CoreError::Unknown(format!("Failed to run embedding model: {}", e)))?;
|
||||
|
||||
let output_tensor = result[0].to_array_view::<f32>().map_err(|e| {
|
||||
CoreError::Unknown(format!("Failed to convert output tensor: {}", e))
|
||||
})?;
|
||||
|
||||
let output_vec: Vec<f32> = output_tensor.as_slice().unwrap_or(&[]).to_vec();
|
||||
|
||||
if output_vec.is_empty() {
|
||||
return Err(CoreError::Unknown(
|
||||
"Embedding model returned empty output".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let norm = (output_vec.iter().map(|&x| x * x).sum::<f32>()).sqrt();
|
||||
if norm > 1e-5 {
|
||||
let normalized_vec: Vec<f32> = output_vec.iter().map(|&x| x / norm).collect();
|
||||
Ok(normalized_vec)
|
||||
} else {
|
||||
Ok(output_vec)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| CoreError::Unknown(format!("Embedding task failed: {}", e)))?;
|
||||
|
||||
let duration = start_time.elapsed();
|
||||
println!("Face embedding generated in {} ms", duration.as_millis());
|
||||
|
||||
embedding
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ pub enum PostgresMediaMetadataSource {
|
||||
TrackInfo,
|
||||
}
|
||||
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct PostgresUser {
|
||||
pub id: Uuid,
|
||||
@@ -116,4 +115,12 @@ pub struct PostgresPersonShared {
|
||||
pub owner_id: Uuid,
|
||||
pub name: String,
|
||||
pub permission: PostgresPersonPermission,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct PostgresFaceEmbedding {
|
||||
pub id: Uuid,
|
||||
pub face_region_id: Uuid,
|
||||
pub model_id: i16,
|
||||
pub embedding: Vec<u8>,
|
||||
}
|
||||
|
||||
@@ -177,3 +177,19 @@ pub async fn build_media_import_repository(
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_face_embedding_repository(
|
||||
_db_config: &DatabaseConfig,
|
||||
pool: DatabasePool,
|
||||
) -> CoreResult<Arc<dyn libertas_core::repositories::FaceEmbeddingRepository>> {
|
||||
match pool {
|
||||
DatabasePool::Postgres(pg_pool) => Ok(Arc::new(
|
||||
crate::repositories::face_embedding_repository::PostgresFaceEmbeddingRepository::new(
|
||||
pg_pool,
|
||||
),
|
||||
)),
|
||||
DatabasePool::Sqlite(_sqlite_pool) => Err(CoreError::Database(
|
||||
"Sqlite face embedding repository not implemented".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use libertas_core::models::{Album, AlbumPermission, AlbumShare, FaceRegion, Media, MediaMetadata, MediaMetadataSource, Person, PersonPermission, Role, Tag, User};
|
||||
use libertas_core::models::{
|
||||
Album, AlbumPermission, AlbumShare, FaceEmbedding, FaceRegion, Media, MediaMetadata,
|
||||
MediaMetadataSource, Person, PersonPermission, Role, Tag, User,
|
||||
};
|
||||
|
||||
use crate::db_models::{PostgresAlbum, PostgresAlbumPermission, PostgresAlbumShare, PostgresFaceRegion, PostgresMedia, PostgresMediaMetadata, PostgresMediaMetadataSource, PostgresPerson, PostgresPersonPermission, PostgresPersonShared, PostgresRole, PostgresTag, PostgresUser};
|
||||
use crate::db_models::{
|
||||
PostgresAlbum, PostgresAlbumPermission, PostgresAlbumShare, PostgresFaceEmbedding,
|
||||
PostgresFaceRegion, PostgresMedia, PostgresMediaMetadata, PostgresMediaMetadataSource,
|
||||
PostgresPerson, PostgresPersonPermission, PostgresPersonShared, PostgresRole, PostgresTag,
|
||||
PostgresUser,
|
||||
};
|
||||
|
||||
impl From<PostgresRole> for Role {
|
||||
fn from(pg_role: PostgresRole) -> Self {
|
||||
@@ -186,4 +194,15 @@ impl From<PostgresPersonShared> for (Person, PersonPermission) {
|
||||
let permission = PersonPermission::from(pg_shared.permission);
|
||||
(person, permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PostgresFaceEmbedding> for FaceEmbedding {
|
||||
fn from(pg_embedding: PostgresFaceEmbedding) -> Self {
|
||||
Self {
|
||||
id: pg_embedding.id,
|
||||
face_region_id: pg_embedding.face_region_id,
|
||||
model_id: pg_embedding.model_id,
|
||||
embedding: pg_embedding.embedding,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,4 +166,21 @@ impl AlbumRepository for PostgresAlbumRepository {
|
||||
|
||||
Ok(result.exists)
|
||||
}
|
||||
|
||||
async fn set_thumbnail_media_id(&self, album_id: Uuid, media_id: Uuid) -> CoreResult<()> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE albums
|
||||
SET thumbnail_media_id = $1
|
||||
WHERE id = $2
|
||||
"#,
|
||||
media_id,
|
||||
album_id
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
61
libertas_infra/src/repositories/face_embedding_repository.rs
Normal file
61
libertas_infra/src/repositories/face_embedding_repository.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use async_trait::async_trait;
|
||||
use libertas_core::{
|
||||
error::{CoreError, CoreResult},
|
||||
models::FaceEmbedding,
|
||||
repositories::FaceEmbeddingRepository,
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db_models::PostgresFaceEmbedding;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PostgresFaceEmbeddingRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PostgresFaceEmbeddingRepository {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FaceEmbeddingRepository for PostgresFaceEmbeddingRepository {
|
||||
async fn create(&self, embedding: &FaceEmbedding) -> CoreResult<()> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO face_embeddings (id, face_region_id, model_id, embedding)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
"#,
|
||||
embedding.id,
|
||||
embedding.face_region_id,
|
||||
embedding.model_id,
|
||||
embedding.embedding
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_by_face_region_id(
|
||||
&self,
|
||||
face_region_id: Uuid,
|
||||
) -> CoreResult<Option<FaceEmbedding>> {
|
||||
let pg_embedding = sqlx::query_as!(
|
||||
PostgresFaceEmbedding,
|
||||
r#"
|
||||
SELECT id, face_region_id, model_id, embedding
|
||||
FROM face_embeddings
|
||||
WHERE face_region_id = $1
|
||||
"#,
|
||||
face_region_id
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||
|
||||
Ok(pg_embedding.map(FaceEmbedding::from))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod album_repository;
|
||||
pub mod album_share_repository;
|
||||
pub mod face_embedding_repository;
|
||||
pub mod face_region_repository;
|
||||
pub mod media_import_repository;
|
||||
pub mod media_metadata_repository;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
use libertas_core::{error::{CoreError, CoreResult}, models::Person, repositories::PersonRepository};
|
||||
use libertas_core::{
|
||||
error::{CoreError, CoreResult},
|
||||
models::Person,
|
||||
repositories::PersonRepository,
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -95,4 +99,20 @@ impl PersonRepository for PostgresPersonRepository {
|
||||
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_thumbnail_media_id(&self, person_id: Uuid, media_id: Uuid) -> CoreResult<()> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE people
|
||||
SET thumbnail_media_id = $1
|
||||
WHERE id = $2
|
||||
"#,
|
||||
media_id,
|
||||
person_id
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user