From 6ba592a84eedc51b9fa32d668cb3a21ab70388f9 Mon Sep 17 00:00:00 2001 From: Arnaud_Cayrol Date: Thu, 5 Feb 2026 21:55:10 +0100 Subject: [PATCH] Fix clippy warnings and apply auto formatting. --- src/config.rs | 16 +++-- src/main.rs | 10 ++- src/models/dlib_landmarks.rs | 5 +- src/pipeline/crop_utils.rs | 7 +- src/pipeline/debug_utils.rs | 100 +++++++++++++++++++------- src/pipeline/steps/alignment.rs | 17 ++--- src/pipeline/steps/brightness.rs | 9 ++- src/pipeline/steps/crop_and_resize.rs | 11 ++- src/pipeline/steps/decode.rs | 6 +- src/pipeline/steps/eye_filter.rs | 34 +++++---- src/pipeline/steps/face_resolution.rs | 4 +- src/pipeline/steps/head_pose.rs | 16 +++-- src/pipeline/steps/landmarks.rs | 9 ++- src/pipeline/traits.rs | 30 ++++---- src/utils.rs | 15 ++-- src/video/ffmpeg.rs | 12 ++-- src/web/handlers/config.rs | 6 +- src/web/handlers/output.rs | 61 ++++++++-------- src/web/handlers/processing.rs | 6 +- src/web/handlers/ws.rs | 5 +- src/web/state.rs | 13 ++-- 21 files changed, 239 insertions(+), 153 deletions(-) diff --git a/src/config.rs b/src/config.rs index 16da4e5..5b12b23 100644 --- a/src/config.rs +++ b/src/config.rs @@ -330,12 +330,14 @@ impl AlignmentConfig { } if self.eye_y_position < 0.2 || self.eye_y_position > 0.5 { return Err(Error::Config( - "Alignment eye_y_position should be between 0.2 and 0.5 for best results".to_string(), + "Alignment eye_y_position should be between 0.2 and 0.5 for best results" + .to_string(), )); } if self.inter_eye_distance <= 0.0 { return Err(Error::Config( - "Alignment inter_eye_distance must be greater than 0 to prevent division by zero".to_string(), + "Alignment inter_eye_distance must be greater than 0 to prevent division by zero" + .to_string(), )); } if self.inter_eye_distance >= 1.0 { @@ -345,7 +347,8 @@ impl AlignmentConfig { } if self.inter_eye_distance < 0.2 || self.inter_eye_distance > 0.5 { return Err(Error::Config( - "Alignment inter_eye_distance should be between 0.2 and 0.5 for best results".to_string(), + "Alignment inter_eye_distance should be between 0.2 and 0.5 for best results" + .to_string(), )); } Ok(()) @@ -509,8 +512,7 @@ impl Config { /// Load configuration from a TOML file. pub fn from_file(path: impl AsRef) -> Result { let content = std::fs::read_to_string(path)?; - let config: Config = - toml::from_str(&content).map_err(|e| Error::Config(e.to_string()))?; + let config: Config = toml::from_str(&content).map_err(|e| Error::Config(e.to_string()))?; Ok(config) } @@ -708,7 +710,9 @@ mod tests { let temp_dir = std::env::temp_dir(); let temp_path = temp_dir.join("test_config_new.toml"); - config.save_to_file(&temp_path).expect("Failed to save config"); + config + .save_to_file(&temp_path) + .expect("Failed to save config"); // Verify file was created and contains expected content let content = std::fs::read_to_string(&temp_path).expect("Failed to read config"); diff --git a/src/main.rs b/src/main.rs index 2ee11d3..ff7d260 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,10 @@ async fn main() -> anyhow::Result<()> { // Pre-load ML models to avoid loading during processing match DlibLandmarks::init() { Ok(_) => tracing::info!("Dlib landmarks model loaded"), - Err(e) => tracing::warn!("Dlib landmarks model not available: {} - landmark detection will be skipped", e), + Err(e) => tracing::warn!( + "Dlib landmarks model not available: {} - landmark detection will be skipped", + e + ), } // Create application state @@ -69,7 +72,10 @@ fn load_config() -> anyhow::Result { // Validation is optional at startup - API key might be set via web UI later if let Err(e) = config.validate() { - tracing::warn!("Configuration incomplete: {} - some features may not work", e); + tracing::warn!( + "Configuration incomplete: {} - some features may not work", + e + ); } Ok(config) diff --git a/src/models/dlib_landmarks.rs b/src/models/dlib_landmarks.rs index dbabca6..ba2041f 100644 --- a/src/models/dlib_landmarks.rs +++ b/src/models/dlib_landmarks.rs @@ -129,9 +129,8 @@ impl DlibLandmarks { .map(|p| Point::new(p.x() as f32, p.y() as f32)) .collect(); - Landmarks::new(points).ok_or_else(|| { - Error::Model("Could not detect 68 facial landmarks".to_string()) - }) + Landmarks::new(points) + .ok_or_else(|| Error::Model("Could not detect 68 facial landmarks".to_string())) } } diff --git a/src/pipeline/crop_utils.rs b/src/pipeline/crop_utils.rs index ec74da8..760b4df 100644 --- a/src/pipeline/crop_utils.rs +++ b/src/pipeline/crop_utils.rs @@ -94,7 +94,12 @@ pub fn crop_face_with_intermediate( /// /// When the crop region extends past the image borders, edge pixels are repeated /// (e.g., column -1 uses column 0, column -2 uses column 0, etc.). -fn crop_with_replicate_fill(img: &DynamicImage, x_offset: i32, y_offset: i32, size: u32) -> DynamicImage { +fn crop_with_replicate_fill( + img: &DynamicImage, + x_offset: i32, + y_offset: i32, + size: u32, +) -> DynamicImage { let rgb = img.to_rgb8(); let (w, h) = (rgb.width() as i32, rgb.height() as i32); diff --git a/src/pipeline/debug_utils.rs b/src/pipeline/debug_utils.rs index b610fed..9cb04c8 100644 --- a/src/pipeline/debug_utils.rs +++ b/src/pipeline/debug_utils.rs @@ -36,31 +36,81 @@ pub fn draw_simple_text(img: &mut RgbImage, x: u32, y: u32, text: &str, color: R /// Get a 5x7 pixel pattern for a character. fn get_char_pattern(ch: char) -> [u8; 7] { match ch { - '0' => [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110], - '1' => [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110], - '2' => [0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111], - '3' => [0b01110, 0b10001, 0b00001, 0b00110, 0b00001, 0b10001, 0b01110], - '4' => [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010], - '5' => [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110], - '6' => [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110], - '7' => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000], - '8' => [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110], - '9' => [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100], - 'A' => [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001], - 'B' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110], - 'E' => [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111], - 'H' => [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001], - 'L' => [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111], - 'P' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000], - 'R' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001], - 'Y' => [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100], - ':' => [0b00000, 0b00100, 0b00000, 0b00000, 0b00100, 0b00000, 0b00000], - '.' => [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100], - '=' => [0b00000, 0b00000, 0b11111, 0b00000, 0b11111, 0b00000, 0b00000], - '-' => [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000], - '°' => [0b00110, 0b01001, 0b01001, 0b00110, 0b00000, 0b00000, 0b00000], - ' ' => [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000], - _ => [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000], + '0' => [ + 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110, + ], + '1' => [ + 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110, + ], + '2' => [ + 0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111, + ], + '3' => [ + 0b01110, 0b10001, 0b00001, 0b00110, 0b00001, 0b10001, 0b01110, + ], + '4' => [ + 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010, + ], + '5' => [ + 0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110, + ], + '6' => [ + 0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110, + ], + '7' => [ + 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000, + ], + '8' => [ + 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110, + ], + '9' => [ + 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100, + ], + 'A' => [ + 0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001, + ], + 'B' => [ + 0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110, + ], + 'E' => [ + 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111, + ], + 'H' => [ + 0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001, + ], + 'L' => [ + 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111, + ], + 'P' => [ + 0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000, + ], + 'R' => [ + 0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001, + ], + 'Y' => [ + 0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100, + ], + ':' => [ + 0b00000, 0b00100, 0b00000, 0b00000, 0b00100, 0b00000, 0b00000, + ], + '.' => [ + 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, + ], + '=' => [ + 0b00000, 0b00000, 0b11111, 0b00000, 0b11111, 0b00000, 0b00000, + ], + '-' => [ + 0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000, + ], + '°' => [ + 0b00110, 0b01001, 0b01001, 0b00110, 0b00000, 0b00000, 0b00000, + ], + ' ' => [ + 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, + ], + _ => [ + 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, + ], } } diff --git a/src/pipeline/steps/alignment.rs b/src/pipeline/steps/alignment.rs index a51a6be..8237948 100644 --- a/src/pipeline/steps/alignment.rs +++ b/src/pipeline/steps/alignment.rs @@ -4,7 +4,7 @@ //! across all images in the timelapse. use crate::config::Config; -use crate::pipeline::{computed_keys, Point, PipelineContext, ProcessingStep, StepOutcome}; +use crate::pipeline::{computed_keys, PipelineContext, Point, ProcessingStep, StepOutcome}; use async_trait::async_trait; use image::{DynamicImage, GenericImageView, Rgb}; use imageproc::geometric_transformations::{rotate_about_center, Interpolation}; @@ -39,7 +39,8 @@ impl ProcessingStep for AlignmentStep { // Landmarks should always be available since LandmarksStep is mandatory return StepOutcome::Error { ctx, - error: "Landmarks not available for alignment - pipeline misconfigured".to_string(), + error: "Landmarks not available for alignment - pipeline misconfigured" + .to_string(), }; } }; @@ -101,10 +102,8 @@ impl ProcessingStep for AlignmentStep { // Rotate eye_center around image center let dx = eye_center.x - cx; let dy = eye_center.y - cy; - let rotated_eye_center = Point::new( - cx + dx * cos_a + dy * sin_a, - cy - dx * sin_a + dy * cos_a, - ); + let rotated_eye_center = + Point::new(cx + dx * cos_a + dy * sin_a, cy - dx * sin_a + dy * cos_a); // Now calculate crop region to achieve the desired scale and positioning // We want the eye center at (output_size/2, target_eye_y) @@ -116,8 +115,10 @@ impl ProcessingStep for AlignmentStep { let crop_size = (output_size as f32 / scale) as u32; // Crop center in source image (accounting for where we want eyes to end up) - let crop_center_x = rotated_eye_center.x - (target_center_x - output_size as f32 / 2.0) / scale; - let crop_center_y = rotated_eye_center.y + (target_eye_y - output_size as f32 / 2.0) / scale; + let crop_center_x = + rotated_eye_center.x - (target_center_x - output_size as f32 / 2.0) / scale; + let crop_center_y = + rotated_eye_center.y + (target_eye_y - output_size as f32 / 2.0) / scale; // Calculate crop bounds let crop_x = (crop_center_x - crop_size as f32 / 2.0).max(0.0) as u32; diff --git a/src/pipeline/steps/brightness.rs b/src/pipeline/steps/brightness.rs index 8eaa8dd..ea09915 100644 --- a/src/pipeline/steps/brightness.rs +++ b/src/pipeline/steps/brightness.rs @@ -3,7 +3,9 @@ //! Calculates average image brightness and skips images that are too dark or too bright. use crate::config::Config; -use crate::pipeline::{computed_keys, draw_simple_text, ComputedValue, PipelineContext, ProcessingStep, StepOutcome}; +use crate::pipeline::{ + computed_keys, draw_simple_text, ComputedValue, PipelineContext, ProcessingStep, StepOutcome, +}; use async_trait::async_trait; use image::{DynamicImage, Rgb}; @@ -173,7 +175,7 @@ fn brightness_to_color(brightness: f32) -> Rgb { mod tests { use super::*; use crate::immich_api::FaceData; - use image::{DynamicImage, RgbImage, Rgb}; + use image::{DynamicImage, Rgb, RgbImage}; fn make_ctx_with_image(image: DynamicImage) -> PipelineContext { let face_data = FaceData { @@ -225,7 +227,8 @@ mod tests { match step.execute(ctx, &config).await { StepOutcome::Continue(new_ctx) => { // Should still compute brightness even when disabled - let brightness = new_ctx.get_computed(computed_keys::BRIGHTNESS) + let brightness = new_ctx + .get_computed(computed_keys::BRIGHTNESS) .and_then(|v| v.as_float()) .unwrap(); assert!(brightness > 0.45 && brightness < 0.55); diff --git a/src/pipeline/steps/crop_and_resize.rs b/src/pipeline/steps/crop_and_resize.rs index 8d3a7d5..99c201f 100644 --- a/src/pipeline/steps/crop_and_resize.rs +++ b/src/pipeline/steps/crop_and_resize.rs @@ -5,7 +5,9 @@ use crate::config::Config; use crate::pipeline::crop_face_with_intermediate; -use crate::pipeline::{computed_keys, BoundingBox, ComputedValue, PipelineContext, ProcessingStep, StepOutcome}; +use crate::pipeline::{ + computed_keys, BoundingBox, ComputedValue, PipelineContext, ProcessingStep, StepOutcome, +}; use async_trait::async_trait; /// Crops the face region from the full image and resizes it. @@ -50,7 +52,10 @@ impl ProcessingStep for CropAndResizeStep { }; // Store the scaled face rectangle for later steps - ctx.set_computed(computed_keys::FACE_RECT, ComputedValue::FaceRect(scaled_face_rect)); + ctx.set_computed( + computed_keys::FACE_RECT, + ComputedValue::FaceRect(scaled_face_rect), + ); StepOutcome::Continue(ctx) } @@ -67,7 +72,7 @@ impl ProcessingStep for CropAndResizeStep { mod tests { use super::*; use crate::immich_api::FaceData; - use image::{DynamicImage, RgbImage, Rgb}; + use image::{DynamicImage, Rgb, RgbImage}; fn make_ctx_with_image(image: DynamicImage) -> PipelineContext { // Face in the center of a 100x100 image diff --git a/src/pipeline/steps/decode.rs b/src/pipeline/steps/decode.rs index e80f1ae..a55e962 100644 --- a/src/pipeline/steps/decode.rs +++ b/src/pipeline/steps/decode.rs @@ -52,8 +52,8 @@ impl ProcessingStep for DecodeImageStep { #[cfg(test)] mod tests { use super::*; - use bytes::Bytes; use crate::immich_api::FaceData; + use bytes::Bytes; use image::{DynamicImage, RgbImage}; use std::io::Cursor; @@ -63,7 +63,9 @@ mod tests { let dynamic = DynamicImage::ImageRgb8(img); let mut buffer = Cursor::new(Vec::new()); - dynamic.write_to(&mut buffer, image::ImageFormat::Jpeg).unwrap(); + dynamic + .write_to(&mut buffer, image::ImageFormat::Jpeg) + .unwrap(); Bytes::from(buffer.into_inner()) } diff --git a/src/pipeline/steps/eye_filter.rs b/src/pipeline/steps/eye_filter.rs index f4913a1..d285f15 100644 --- a/src/pipeline/steps/eye_filter.rs +++ b/src/pipeline/steps/eye_filter.rs @@ -4,7 +4,9 @@ //! from facial landmarks. use crate::config::Config; -use crate::pipeline::{computed_keys, draw_simple_text, Landmarks, PipelineContext, ProcessingStep, StepOutcome}; +use crate::pipeline::{ + computed_keys, draw_simple_text, Landmarks, PipelineContext, ProcessingStep, StepOutcome, +}; use async_trait::async_trait; use image::{DynamicImage, Rgb, RgbImage}; @@ -33,7 +35,10 @@ impl ProcessingStep for EyeFilterStep { } // Get EAR from computed values (set by LandmarksStep) - let avg_ear = match ctx.get_computed(computed_keys::EAR).and_then(|v| v.as_float()) { + let avg_ear = match ctx + .get_computed(computed_keys::EAR) + .and_then(|v| v.as_float()) + { Some(ear) => ear, None => { // No EAR available - landmarks step must have been skipped @@ -85,8 +90,7 @@ impl ProcessingStep for EyeFilterStep { Rgb([255, 0, 0]) // Red - closed }; - for i in 36..42 { - let point = &points[i]; + for point in points.iter().skip(36).take(6) { draw_cross(&mut debug_img, point.x as u32, point.y as u32, left_color); } @@ -97,16 +101,25 @@ impl ProcessingStep for EyeFilterStep { Rgb([255, 0, 0]) // Red - closed }; - for i in 42..48 { - let point = &points[i]; + for point in points.iter().skip(42).take(6) { draw_cross(&mut debug_img, point.x as u32, point.y as u32, right_color); } // Draw eye centers let left_eye = landmarks.left_eye_center(); let right_eye = landmarks.right_eye_center(); - draw_marker(&mut debug_img, left_eye.x as u32, left_eye.y as u32, Rgb([0, 255, 255])); - draw_marker(&mut debug_img, right_eye.x as u32, right_eye.y as u32, Rgb([0, 255, 255])); + draw_marker( + &mut debug_img, + left_eye.x as u32, + left_eye.y as u32, + Rgb([0, 255, 255]), + ); + draw_marker( + &mut debug_img, + right_eye.x as u32, + right_eye.y as u32, + Rgb([0, 255, 255]), + ); // Draw info bar at bottom let bar_height = 20u32; @@ -120,10 +133,7 @@ impl ProcessingStep for EyeFilterStep { } // Draw EAR values - let text = format!( - "L:{:.2} R:{:.2} Avg:{:.2}", - ear.left, ear.right, avg_ear - ); + let text = format!("L:{:.2} R:{:.2} Avg:{:.2}", ear.left, ear.right, avg_ear); draw_simple_text(&mut debug_img, 5, bar_y + 6, &text, Rgb([255, 255, 255])); Some(DynamicImage::ImageRgb8(debug_img)) diff --git a/src/pipeline/steps/face_resolution.rs b/src/pipeline/steps/face_resolution.rs index add16e4..811084a 100644 --- a/src/pipeline/steps/face_resolution.rs +++ b/src/pipeline/steps/face_resolution.rs @@ -96,7 +96,9 @@ mod tests { StepOutcome::Continue(new_ctx) => { // Should have stored face_size assert_eq!( - new_ctx.get_computed(computed_keys::FACE_SIZE).and_then(|v| v.as_int()), + new_ctx + .get_computed(computed_keys::FACE_SIZE) + .and_then(|v| v.as_int()), Some(100) ); } diff --git a/src/pipeline/steps/head_pose.rs b/src/pipeline/steps/head_pose.rs index 89c3990..0444f26 100644 --- a/src/pipeline/steps/head_pose.rs +++ b/src/pipeline/steps/head_pose.rs @@ -85,9 +85,9 @@ fn draw_pose_axes( // Y axis (green) - points down (image coordinates) // Z axis (blue) - points out of screen (towards camera) let axes = [ - (Point3D::new(axis_length, 0.0, 0.0), Rgb([255, 0, 0])), // X - red - (Point3D::new(0.0, axis_length, 0.0), Rgb([0, 255, 0])), // Y - green - (Point3D::new(0.0, 0.0, -axis_length), Rgb([0, 0, 255])), // Z - blue (negative = towards camera) + (Point3D::new(axis_length, 0.0, 0.0), Rgb([255, 0, 0])), // X - red + (Point3D::new(0.0, axis_length, 0.0), Rgb([0, 255, 0])), // Y - green + (Point3D::new(0.0, 0.0, -axis_length), Rgb([0, 0, 255])), // Z - blue (negative = towards camera) ]; // Negate roll and pitch to convert from model convention to image coordinates @@ -333,7 +333,15 @@ impl ProcessingStep for HeadPoseStep { let cx = (x1 + x2) as f32 / 2.0; let cy = (y1 + y2) as f32 / 2.0; let axis_length = ((x2 - x1).max(y2 - y1) as f32) * 0.6; - draw_pose_axes(&mut debug_img, cx, cy, axis_length, pose.yaw, pose.pitch, pose.roll); + draw_pose_axes( + &mut debug_img, + cx, + cy, + axis_length, + pose.yaw, + pose.pitch, + pose.roll, + ); } // Draw text background bar at bottom for pose values diff --git a/src/pipeline/steps/landmarks.rs b/src/pipeline/steps/landmarks.rs index 601edb8..ea7fc3c 100644 --- a/src/pipeline/steps/landmarks.rs +++ b/src/pipeline/steps/landmarks.rs @@ -4,7 +4,9 @@ use crate::config::Config; use crate::models::DlibLandmarks; -use crate::pipeline::{computed_keys, ComputedValue, Landmarks, PipelineContext, ProcessingStep, StepOutcome}; +use crate::pipeline::{ + computed_keys, ComputedValue, Landmarks, PipelineContext, ProcessingStep, StepOutcome, +}; use async_trait::async_trait; use tokio::task; @@ -90,7 +92,10 @@ impl ProcessingStep for LandmarksStep { ctx.set_computed(computed_keys::EAR, ComputedValue::Float(avg_ear)); // Store landmarks - ctx.set_computed(computed_keys::LANDMARKS, ComputedValue::Landmarks(Box::new(landmarks))); + ctx.set_computed( + computed_keys::LANDMARKS, + ComputedValue::Landmarks(Box::new(landmarks)), + ); tracing::trace!( "Landmarks detected: EAR left={:.3}, right={:.3}, avg={:.3}", diff --git a/src/pipeline/traits.rs b/src/pipeline/traits.rs index df5b328..f0a4e98 100644 --- a/src/pipeline/traits.rs +++ b/src/pipeline/traits.rs @@ -4,8 +4,8 @@ //! extensible image processing pipelines. use crate::config::Config; -use crate::pipeline::types::{BoundingBox, HeadPose, Landmarks}; use crate::immich_api::FaceData; +use crate::pipeline::types::{BoundingBox, HeadPose, Landmarks}; use async_trait::async_trait; use bytes::Bytes; use image::DynamicImage; @@ -41,10 +41,7 @@ pub enum StepOutcome { detail: Option, }, /// An error occurred during processing. Context is preserved for debug visualization. - Error { - ctx: PipelineContext, - error: String, - }, + Error { ctx: PipelineContext, error: String }, } /// Values computed by pipeline steps that can be shared with subsequent steps. @@ -232,8 +229,14 @@ impl PipelineContext { /// * `step_id` - The identifier of the step that generated this image /// * `image` - The debug visualization image /// * `passed` - Whether the step passed (true) or failed/skipped (false) - pub fn add_debug_image(&mut self, step_id: impl Into, image: DynamicImage, passed: bool) { - self.debug_images.insert(step_id.into(), DebugImage::new(image, passed)); + pub fn add_debug_image( + &mut self, + step_id: impl Into, + image: DynamicImage, + passed: bool, + ) { + self.debug_images + .insert(step_id.into(), DebugImage::new(image, passed)); } } @@ -328,21 +331,20 @@ mod tests { image_height: 1080, }; - let mut ctx = PipelineContext::new( - "asset123".to_string(), - "2024-01-15".to_string(), - face_data, - ); + let mut ctx = + PipelineContext::new("asset123".to_string(), "2024-01-15".to_string(), face_data); ctx.set_computed(computed_keys::BRIGHTNESS, ComputedValue::Float(0.65)); ctx.set_computed(computed_keys::FACE_SIZE, ComputedValue::Int(150)); assert_eq!( - ctx.get_computed(computed_keys::BRIGHTNESS).and_then(|v| v.as_float()), + ctx.get_computed(computed_keys::BRIGHTNESS) + .and_then(|v| v.as_float()), Some(0.65) ); assert_eq!( - ctx.get_computed(computed_keys::FACE_SIZE).and_then(|v| v.as_int()), + ctx.get_computed(computed_keys::FACE_SIZE) + .and_then(|v| v.as_int()), Some(150) ); assert!(ctx.get_computed("nonexistent").is_none()); diff --git a/src/utils.rs b/src/utils.rs index 17ba79f..4d64e8d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -19,10 +19,7 @@ pub fn sanitize_folder_name(name: Option<&str>, id: &str) -> String { let base = name.filter(|n| !n.is_empty()).unwrap_or(id); // Remove accents by decomposing to NFD and filtering combining marks - let without_accents: String = base - .nfd() - .filter(|c| !is_combining_mark(*c)) - .collect(); + let without_accents: String = base.nfd().filter(|c| !is_combining_mark(*c)).collect(); // Convert to lowercase and replace unsafe characters with underscores let sanitized: String = without_accents @@ -71,10 +68,7 @@ mod tests { #[test] fn test_sanitize_folder_name_trims_whitespace() { - assert_eq!( - sanitize_folder_name(Some(" John Doe "), "id"), - "john_doe" - ); + assert_eq!(sanitize_folder_name(Some(" John Doe "), "id"), "john_doe"); } #[test] @@ -99,6 +93,9 @@ mod tests { assert_eq!(sanitize_folder_name(Some("Peña"), "id"), "pena"); // Mixed accents - assert_eq!(sanitize_folder_name(Some("Élève Français"), "id"), "eleve_francais"); + assert_eq!( + sanitize_folder_name(Some("Élève Français"), "id"), + "eleve_francais" + ); } } diff --git a/src/video/ffmpeg.rs b/src/video/ffmpeg.rs index 0f1ac7d..61c10ad 100644 --- a/src/video/ffmpeg.rs +++ b/src/video/ffmpeg.rs @@ -94,12 +94,9 @@ where .stdout(Stdio::piped()) .stderr(Stdio::piped()); - let mut child = cmd.spawn().map_err(|e| { - Error::FFmpeg(format!( - "Failed to spawn ffmpeg (is it installed?): {}", - e - )) - })?; + let mut child = cmd + .spawn() + .map_err(|e| Error::FFmpeg(format!("Failed to spawn ffmpeg (is it installed?): {}", e)))?; // Capture stdout for progress and stderr for errors let stdout = child @@ -149,8 +146,7 @@ where return Err(Error::FFmpeg(format!( "ffmpeg exited with status {}\n{}", - status, - error_detail + status, error_detail ))); } diff --git a/src/web/handlers/config.rs b/src/web/handlers/config.rs index d2cbe79..6922cf6 100644 --- a/src/web/handlers/config.rs +++ b/src/web/handlers/config.rs @@ -192,11 +192,7 @@ fn validate_video_config(vid: &VideoConfigUpdate) -> Result<(), ValidationError> if !valid_codecs.contains(&v.as_str()) { return Err(ValidationError::new( "video.codec", - format!( - "must be one of: {}, got '{}'", - valid_codecs.join(", "), - v - ), + format!("must be one of: {}, got '{}'", valid_codecs.join(", "), v), )); } } diff --git a/src/web/handlers/output.rs b/src/web/handlers/output.rs index 514065a..b8f48bc 100644 --- a/src/web/handlers/output.rs +++ b/src/web/handlers/output.rs @@ -395,11 +395,7 @@ pub async fn delete_images_bulk( let mut remaining_images = 0u32; if let Ok(mut entries) = tokio::fs::read_dir(&images_dir).await { while let Ok(Some(entry)) = entries.next_entry().await { - if entry - .path() - .extension() - .is_some_and(|ext| ext == "jpg") - { + if entry.path().extension().is_some_and(|ext| ext == "jpg") { remaining_images += 1; } } @@ -448,11 +444,7 @@ pub async fn compile_folder_video( let mut image_count = 0u32; if let Ok(mut entries) = tokio::fs::read_dir(&images_dir).await { while let Ok(Some(entry)) = entries.next_entry().await { - if entry - .path() - .extension() - .is_some_and(|ext| ext == "jpg") - { + if entry.path().extension().is_some_and(|ext| ext == "jpg") { image_count += 1; } } @@ -490,29 +482,34 @@ pub async fn compile_folder_video( // Spawn the compilation job in the background tokio::spawn(async move { - let result = crate::video::compile_timelapse(&images_dir, &output_path, &video_config, |current, total| { - // Check for cancellation - if cancel_token.is_cancelled() { - return; - } + let result = crate::video::compile_timelapse( + &images_dir, + &output_path, + &video_config, + |current, total| { + // Check for cancellation + if cancel_token.is_cancelled() { + return; + } - // Update progress (fire and forget since we're in sync callback) - let state_clone = job_state.clone(); - let folder_clone = folder_name_clone.clone(); - tokio::spawn(async move { - state_clone - .update_progress(Progress { - status: JobStatus::CompilingVideo, - completed: current, - total, - message: Some(format!("Compiling video for {}...", folder_clone)), - skip_stats: SkipStats::default(), - person_id: None, - person_name: Some(folder_clone), - }) - .await; - }); - }) + // Update progress (fire and forget since we're in sync callback) + let state_clone = job_state.clone(); + let folder_clone = folder_name_clone.clone(); + tokio::spawn(async move { + state_clone + .update_progress(Progress { + status: JobStatus::CompilingVideo, + completed: current, + total, + message: Some(format!("Compiling video for {}...", folder_clone)), + skip_stats: SkipStats::default(), + person_id: None, + person_name: Some(folder_clone), + }) + .await; + }); + }, + ) .await; // Update final status diff --git a/src/web/handlers/processing.rs b/src/web/handlers/processing.rs index bca1a53..d1f3d10 100644 --- a/src/web/handlers/processing.rs +++ b/src/web/handlers/processing.rs @@ -2,11 +2,7 @@ use crate::job::{run_job, JobParams}; use crate::web::state::{AppState, JobStatus, Progress, SkipStats}; -use axum::{ - extract::State, - http::StatusCode, - response::Json, -}; +use axum::{extract::State, http::StatusCode, response::Json}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/src/web/handlers/ws.rs b/src/web/handlers/ws.rs index 552e61a..9464784 100644 --- a/src/web/handlers/ws.rs +++ b/src/web/handlers/ws.rs @@ -12,10 +12,7 @@ use axum::{ use futures_util::{stream::StreamExt, SinkExt}; /// WebSocket upgrade handler. -pub async fn ws_handler( - ws: WebSocketUpgrade, - State(state): State, -) -> impl IntoResponse { +pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State) -> impl IntoResponse { ws.on_upgrade(|socket| handle_socket(socket, state)) } diff --git a/src/web/state.rs b/src/web/state.rs index 0eda17b..bf9bb50 100644 --- a/src/web/state.rs +++ b/src/web/state.rs @@ -23,7 +23,10 @@ impl JobStatus { /// Returns true if this is a terminal status (job has finished). /// Terminal statuses should not be overwritten by non-terminal statuses. pub fn is_terminal(&self) -> bool { - matches!(self, JobStatus::Completed | JobStatus::Cancelled | JobStatus::Error(_)) + matches!( + self, + JobStatus::Completed | JobStatus::Cancelled | JobStatus::Error(_) + ) } } @@ -221,8 +224,7 @@ impl AppState { // Update progress to show cancelling state immediately. // Only change to Cancelling if in an active (non-terminal) state. let mut progress = self.progress.write().await; - if progress.status == JobStatus::Running - || progress.status == JobStatus::CompilingVideo + if progress.status == JobStatus::Running || progress.status == JobStatus::CompilingVideo { progress.status = JobStatus::Cancelling; progress.message = Some("Cancelling...".to_string()); @@ -273,7 +275,10 @@ impl std::error::Error for JobRunningError {} /// Validate that a path component (folder name or filename) is safe. /// /// Returns an error if the path contains traversal sequences or separators. -pub fn validate_path_component(name: &str, component_type: &str) -> Result<(), PathValidationError> { +pub fn validate_path_component( + name: &str, + component_type: &str, +) -> Result<(), PathValidationError> { if name.contains("..") || name.contains('/') || name.contains('\\') { Err(PathValidationError { component_type: component_type.to_string(),