Fix documentation and default settings
This commit is contained in:
parent
c6f2a0b346
commit
bbe6cd4d01
6 changed files with 51 additions and 147 deletions
|
|
@ -58,7 +58,6 @@
|
||||||
enabled: config.processing.head_pose.enabled,
|
enabled: config.processing.head_pose.enabled,
|
||||||
max_yaw: Number(config.processing.head_pose.max_yaw),
|
max_yaw: Number(config.processing.head_pose.max_yaw),
|
||||||
max_pitch: Number(config.processing.head_pose.max_pitch),
|
max_pitch: Number(config.processing.head_pose.max_pitch),
|
||||||
max_roll: Number(config.processing.head_pose.max_roll),
|
|
||||||
},
|
},
|
||||||
eye_filter: {
|
eye_filter: {
|
||||||
enabled: config.processing.eye_filter.enabled,
|
enabled: config.processing.eye_filter.enabled,
|
||||||
|
|
@ -301,23 +300,6 @@
|
||||||
</div>
|
</div>
|
||||||
</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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ export const DEFAULT_CONFIG = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
max_yaw: 35.0,
|
max_yaw: 35.0,
|
||||||
max_pitch: 35.0,
|
max_pitch: 35.0,
|
||||||
max_roll: 25.0,
|
|
||||||
},
|
},
|
||||||
eye_filter: {
|
eye_filter: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
|
||||||
168
src/config.rs
168
src/config.rs
|
|
@ -4,20 +4,6 @@
|
||||||
//! 1. TOML file (config.toml)
|
//! 1. TOML file (config.toml)
|
||||||
//! 2. Environment variables (prefixed with IMMICH_)
|
//! 2. Environment variables (prefixed with IMMICH_)
|
||||||
//! 3. Programmatic overrides
|
//! 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 serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
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.
|
/// Brightness validation configuration.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BrightnessConfig {
|
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.
|
/// Output/resize configuration.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OutputConfig {
|
pub struct OutputConfig {
|
||||||
|
|
@ -263,9 +248,6 @@ pub struct HeadPoseConfig {
|
||||||
|
|
||||||
/// Maximum allowed pitch angle (up/down tilt) in degrees.
|
/// Maximum allowed pitch angle (up/down tilt) in degrees.
|
||||||
pub max_pitch: f32,
|
pub max_pitch: f32,
|
||||||
|
|
||||||
/// Maximum allowed roll angle (head tilt) in degrees.
|
|
||||||
pub max_roll: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HeadPoseConfig {
|
impl Default for HeadPoseConfig {
|
||||||
|
|
@ -274,7 +256,6 @@ impl Default for HeadPoseConfig {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
max_yaw: 35.0,
|
max_yaw: 35.0,
|
||||||
max_pitch: 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(),
|
"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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -343,16 +319,16 @@ impl EyeFilterConfig {
|
||||||
/// Aligns faces based on eye positions detected from facial landmarks,
|
/// Aligns faces based on eye positions detected from facial landmarks,
|
||||||
/// ensuring consistent eye placement across all images for smoother timelapses.
|
/// ensuring consistent eye placement across all images for smoother timelapses.
|
||||||
///
|
///
|
||||||
/// Uses a single `eye_distance` parameter to control framing. Eye positions
|
/// Uses a single `eye_distance` parameter to control framing. Eye positions are derived by
|
||||||
/// are derived by estimating the visual face center (approximately at the nose
|
/// estimating the visual face center (see `FACE_CENTER_OFFSET_IN_IPD`) from standard face
|
||||||
/// tip) from standard face proportions, then placing that point at the image
|
/// proportions, then placing that point at the image center with a small downward offset to
|
||||||
/// center with a small downward offset to show more neck.
|
/// show more neck.
|
||||||
///
|
///
|
||||||
/// Note: Face alignment is always enabled and is a core part of the pipeline.
|
/// Note: Face alignment is always enabled and is a core part of the pipeline.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AlignmentConfig {
|
pub struct AlignmentConfig {
|
||||||
/// Distance between left and right eye centers as a fraction of output
|
/// 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
|
/// output width. Larger values zoom in (bigger face), smaller values
|
||||||
/// zoom out (more background).
|
/// zoom out (more background).
|
||||||
pub eye_distance: f32,
|
pub eye_distance: f32,
|
||||||
|
|
@ -360,18 +336,20 @@ pub struct AlignmentConfig {
|
||||||
|
|
||||||
impl Default for AlignmentConfig {
|
impl Default for AlignmentConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { eye_distance: 0.3 }
|
Self { eye_distance: 0.15 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlignmentConfig {
|
impl AlignmentConfig {
|
||||||
/// Offset from the eye line to the approximate visual face center
|
/// How far below the eye line the face center sits, in units of
|
||||||
/// (with a slight neck bias), expressed as a multiple of the inter-eye
|
/// inter-pupillary distance (IPD). Value of 0.5 means the face center
|
||||||
/// distance. The visible face center (hairline-to-chin midpoint) is
|
/// is 0.5 × IPD below the eyes.
|
||||||
/// 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
|
/// From face proportions: the vertical midpoint between hairline and chin
|
||||||
/// face alignment (desiredLeftEye = (0.35, 0.35) with IPD = 0.3).
|
/// is ~0.4× IPD below the eyes; adding a small neck bias brings it to
|
||||||
/// This point is placed at the image center (0.5, 0.5).
|
/// 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;
|
const FACE_CENTER_BELOW_EYES: f32 = 0.5;
|
||||||
|
|
||||||
/// Validate the configuration values.
|
/// Validate the configuration values.
|
||||||
|
|
@ -702,68 +680,18 @@ impl Config {
|
||||||
/// Recognized variables:
|
/// Recognized variables:
|
||||||
/// - IMMICH_API_KEY - API key for Immich
|
/// - IMMICH_API_KEY - API key for Immich
|
||||||
/// - IMMICH_BASE_URL - Base URL of Immich instance
|
/// - 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 {
|
pub fn from_env() -> Self {
|
||||||
Config::default().with_env()
|
Config::default().with_env()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge environment variables into an existing config.
|
/// Merge environment variables into an existing config.
|
||||||
pub fn with_env(mut self) -> Self {
|
pub fn with_env(mut self) -> Self {
|
||||||
// API settings
|
|
||||||
if let Ok(key) = std::env::var("IMMICH_API_KEY") {
|
if let Ok(key) = std::env::var("IMMICH_API_KEY") {
|
||||||
self.api.api_key = key;
|
self.api.api_key = key;
|
||||||
}
|
}
|
||||||
if let Ok(url) = std::env::var("IMMICH_BASE_URL") {
|
if let Ok(url) = std::env::var("IMMICH_BASE_URL") {
|
||||||
self.api.base_url = 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -484,7 +484,7 @@ fn find_face_for_person(asset: &Asset, person_id: &str) -> Option<FaceData> {
|
||||||
for person in people {
|
for person in people {
|
||||||
if person.id == person_id {
|
if person.id == person_id {
|
||||||
if let Some(faces) = &person.faces {
|
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();
|
return faces.first().cloned();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ fn load_config() -> anyhow::Result<Config> {
|
||||||
Config::from_env()
|
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() {
|
if let Err(e) = config.validate() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Configuration incomplete: {} - some features may not work",
|
"Configuration incomplete: {} - some features may not work",
|
||||||
|
|
|
||||||
|
|
@ -146,12 +146,6 @@ fn validate_processing_config(proc: &ProcessingConfigUpdate) -> Result<(), Valid
|
||||||
format!("must be between 0 and 90, got {}", hp.max_pitch),
|
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),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue