Fix documentation and default settings

This commit is contained in:
Arnaud_Cayrol 2026-02-12 22:39:31 +01:00
parent c6f2a0b346
commit bbe6cd4d01
6 changed files with 51 additions and 147 deletions

View file

@ -58,7 +58,6 @@
enabled: config.processing.head_pose.enabled,
max_yaw: Number(config.processing.head_pose.max_yaw),
max_pitch: Number(config.processing.head_pose.max_pitch),
max_roll: Number(config.processing.head_pose.max_roll),
},
eye_filter: {
enabled: config.processing.eye_filter.enabled,
@ -301,23 +300,6 @@
</div>
</div>
<div class="setting-row sub-setting">
<label for="max-roll">
<span class="setting-label">Max Roll</span>
<span class="setting-hint">Maximum head tilt angle</span>
</label>
<div class="setting-control">
<input
id="max-roll"
type="range"
bind:value={config.processing.head_pose.max_roll}
min="5"
max="90"
step="5"
/>
<span class="value">{config.processing.head_pose.max_roll.toFixed(0)}°</span>
</div>
</div>
{/if}
</div>

View file

@ -67,7 +67,6 @@ export const DEFAULT_CONFIG = {
enabled: true,
max_yaw: 35.0,
max_pitch: 35.0,
max_roll: 25.0,
},
eye_filter: {
enabled: false,

View file

@ -4,20 +4,6 @@
//! 1. TOML file (config.toml)
//! 2. Environment variables (prefixed with IMMICH_)
//! 3. Programmatic overrides
//!
//! ## Step Configuration Pattern
//!
//! Each pipeline step has its own optional sub-config with an `enabled` flag:
//! ```ignore
//! processing:
//! face_resolution:
//! enabled: true
//! min_size: 80
//! brightness:
//! enabled: false
//! min_brightness: 0.1
//! max_brightness: 0.95
//! ```
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
@ -129,40 +115,6 @@ impl FaceResolutionConfig {
}
}
/// Blur detection configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlurConfig {
/// Whether blur detection is enabled.
pub enabled: bool,
/// Minimum gradient magnitude threshold.
/// Images with gradient magnitude below this are considered blurry.
/// Uses Sobel operator for robust edge detection.
/// Typical values: 10-20 for strict filtering, 5-10 for moderate filtering.
pub min_sharpness: f32,
}
impl Default for BlurConfig {
fn default() -> Self {
Self {
enabled: false,
min_sharpness: 15.0,
}
}
}
impl BlurConfig {
/// Validate the configuration values.
pub fn validate(&self) -> Result<()> {
if self.enabled && self.min_sharpness < 0.0 {
return Err(Error::Config(
"Blur min_sharpness must be non-negative".to_string(),
));
}
Ok(())
}
}
/// Brightness validation configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrightnessConfig {
@ -213,6 +165,39 @@ impl BrightnessConfig {
}
}
/// Blur detection configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlurConfig {
/// Whether blur detection is enabled.
pub enabled: bool,
/// Minimum gradient magnitude threshold.
/// Images with gradient magnitude below this are considered blurry.
/// Uses Sobel operator for robust edge detection.
pub min_sharpness: f32,
}
impl Default for BlurConfig {
fn default() -> Self {
Self {
enabled: false,
min_sharpness: 25.0,
}
}
}
impl BlurConfig {
/// Validate the configuration values.
pub fn validate(&self) -> Result<()> {
if self.enabled && self.min_sharpness < 0.0 {
return Err(Error::Config(
"Blur min_sharpness must be non-negative".to_string(),
));
}
Ok(())
}
}
/// Output/resize configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
@ -263,9 +248,6 @@ pub struct HeadPoseConfig {
/// Maximum allowed pitch angle (up/down tilt) in degrees.
pub max_pitch: f32,
/// Maximum allowed roll angle (head tilt) in degrees.
pub max_roll: f32,
}
impl Default for HeadPoseConfig {
@ -274,7 +256,6 @@ impl Default for HeadPoseConfig {
enabled: true,
max_yaw: 35.0,
max_pitch: 35.0,
max_roll: 25.0,
}
}
}
@ -293,11 +274,6 @@ impl HeadPoseConfig {
"Head pose max_pitch must be between 0 and 90 degrees".to_string(),
));
}
if self.max_roll < 0.0 || self.max_roll > 90.0 {
return Err(Error::Config(
"Head pose max_roll must be between 0 and 90 degrees".to_string(),
));
}
}
Ok(())
}
@ -343,16 +319,16 @@ impl EyeFilterConfig {
/// Aligns faces based on eye positions detected from facial landmarks,
/// ensuring consistent eye placement across all images for smoother timelapses.
///
/// Uses a single `eye_distance` parameter to control framing. Eye positions
/// are derived by estimating the visual face center (approximately at the nose
/// tip) from standard face proportions, then placing that point at the image
/// center with a small downward offset to show more neck.
/// Uses a single `eye_distance` parameter to control framing. Eye positions are derived by
/// estimating the visual face center (see `FACE_CENTER_OFFSET_IN_IPD`) from standard face
/// proportions, then placing that point at the image center with a small downward offset to
/// show more neck.
///
/// Note: Face alignment is always enabled and is a core part of the pipeline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlignmentConfig {
/// Distance between left and right eye centers as a fraction of output
/// image width (0.0-1.0). Default 0.3 means the eyes span 30% of the
/// image width (0.0-1.0). Default 0.15 means the eyes span 15% of the
/// output width. Larger values zoom in (bigger face), smaller values
/// zoom out (more background).
pub eye_distance: f32,
@ -360,18 +336,20 @@ pub struct AlignmentConfig {
impl Default for AlignmentConfig {
fn default() -> Self {
Self { eye_distance: 0.3 }
Self { eye_distance: 0.15 }
}
}
impl AlignmentConfig {
/// Offset from the eye line to the approximate visual face center
/// (with a slight neck bias), expressed as a multiple of the inter-eye
/// distance. The visible face center (hairline-to-chin midpoint) is
/// about 0.4× IPD below the eyes; adding a small neck offset brings
/// this to 0.5× IPD. This matches the standard used by dlib and OpenCV
/// face alignment (desiredLeftEye = (0.35, 0.35) with IPD = 0.3).
/// This point is placed at the image center (0.5, 0.5).
/// How far below the eye line the face center sits, in units of
/// inter-pupillary distance (IPD). Value of 0.5 means the face center
/// is 0.5 × IPD below the eyes.
///
/// From face proportions: the vertical midpoint between hairline and chin
/// is ~0.4× IPD below the eyes; adding a small neck bias brings it to
/// 0.5× IPD. The alignment formula places this point at image center (y=0.5):
///
/// eye_y = 0.5 - eye_distance * FACE_CENTER_OFFSET_IN_IPD
const FACE_CENTER_BELOW_EYES: f32 = 0.5;
/// Validate the configuration values.
@ -702,68 +680,18 @@ impl Config {
/// Recognized variables:
/// - IMMICH_API_KEY - API key for Immich
/// - IMMICH_BASE_URL - Base URL of Immich instance
/// - OUTPUT_DIR - Output directory for processed images/video
/// - RESIZE_SIZE - Output image size in pixels
/// - FACE_RESOLUTION_THRESHOLD - Minimum face size in pixels
/// - MAX_WORKERS - Number of parallel processing workers
/// - VIDEO_FRAMERATE - Output video framerate
/// - VIDEO_ENABLED - Whether to compile video (true/false)
/// - KEEP_INTERMEDIATES - Keep debug images (true/false)
pub fn from_env() -> Self {
Config::default().with_env()
}
/// Merge environment variables into an existing config.
pub fn with_env(mut self) -> Self {
// API settings
if let Ok(key) = std::env::var("IMMICH_API_KEY") {
self.api.api_key = key;
}
if let Ok(url) = std::env::var("IMMICH_BASE_URL") {
self.api.base_url = url;
}
// Output directory
if let Ok(dir) = std::env::var("OUTPUT_DIR") {
self.output_dir = PathBuf::from(dir);
}
// Processing settings
if let Ok(val) = std::env::var("RESIZE_SIZE") {
if let Ok(size) = val.parse() {
self.processing.output.size = size;
}
}
if let Ok(val) = std::env::var("FACE_RESOLUTION_THRESHOLD") {
if let Ok(threshold) = val.parse() {
self.processing.face_resolution.min_size = threshold;
}
}
if let Ok(val) = std::env::var("MAX_WORKERS") {
if let Ok(workers) = val.parse() {
self.processing.max_workers = workers;
}
}
if let Ok(val) = std::env::var("KEEP_INTERMEDIATES") {
self.processing.output.keep_intermediates =
val.eq_ignore_ascii_case("true") || val == "1";
}
// Video settings
if let Ok(val) = std::env::var("VIDEO_FRAMERATE") {
if let Ok(fps) = val.parse() {
self.video.framerate = fps;
}
}
if let Ok(val) = std::env::var("VIDEO_ENABLED") {
self.video.enabled = val.eq_ignore_ascii_case("true") || val == "1";
}
if let Ok(val) = std::env::var("VIDEO_CRF") {
if let Ok(crf) = val.parse() {
self.video.crf = crf;
}
}
self
}

View file

@ -484,7 +484,7 @@ fn find_face_for_person(asset: &Asset, person_id: &str) -> Option<FaceData> {
for person in people {
if person.id == person_id {
if let Some(faces) = &person.faces {
// Return the first face (usually there's only one per person per image)
// Return the first face (Immich shouldn't detect the same person more than once per image)
return faces.first().cloned();
}
}

View file

@ -70,7 +70,8 @@ fn load_config() -> anyhow::Result<Config> {
Config::from_env()
};
// Validation is optional at startup - API key might be set via web UI later
// Warn but don't abort if config is incomplete - the server can still start
// but jobs will fail until IMMICH_API_KEY and IMMICH_BASE_URL are set
if let Err(e) = config.validate() {
tracing::warn!(
"Configuration incomplete: {} - some features may not work",

View file

@ -146,12 +146,6 @@ fn validate_processing_config(proc: &ProcessingConfigUpdate) -> Result<(), Valid
format!("must be between 0 and 90, got {}", hp.max_pitch),
));
}
if hp.max_roll < 0.0 || hp.max_roll > 90.0 {
return Err(ValidationError::new(
"processing.head_pose.max_roll",
format!("must be between 0 and 90, got {}", hp.max_roll),
));
}
}
}