diff --git a/frontend/src/lib/components/GalleryView.svelte b/frontend/src/lib/components/GalleryView.svelte index 657a88a..d55286a 100644 --- a/frontend/src/lib/components/GalleryView.svelte +++ b/frontend/src/lib/components/GalleryView.svelte @@ -14,6 +14,7 @@ let deleting = $state(false); let compiling = $state(false); let videoExists = $state(false); + let cacheBust = $state(Date.now()); let selectedCount = $derived(selectedImages.size); let allSelected = $derived(images.length > 0 && selectedImages.size === images.length); @@ -21,6 +22,7 @@ async function loadImages() { loading = true; error = null; + cacheBust = Date.now(); try { const res = await fetch(`/api/output/${encodeURIComponent(folderName)}/images`); if (!res.ok) { @@ -197,7 +199,7 @@ {selectedImages.has(image.filename) ? '✓' : ''} {image.filename} diff --git a/frontend/src/lib/components/ResultsView.svelte b/frontend/src/lib/components/ResultsView.svelte index c358944..7829f2c 100644 --- a/frontend/src/lib/components/ResultsView.svelte +++ b/frontend/src/lib/components/ResultsView.svelte @@ -7,9 +7,10 @@ let { folderName = null } = $props(); + let cacheBust = $state(Date.now()); let videoUrl = $derived( folderName - ? `/output/${encodeURIComponent(folderName)}/${encodeURIComponent(folderName)}.mp4` + ? `/output/${encodeURIComponent(folderName)}/${encodeURIComponent(folderName)}.mp4?v=${cacheBust}` : null ); let videoError = $state(false); diff --git a/src/config.rs b/src/config.rs index 846dbe8..ebd9a51 100644 --- a/src/config.rs +++ b/src/config.rs @@ -316,9 +316,7 @@ pub struct AlignmentConfig { impl Default for AlignmentConfig { fn default() -> Self { - Self { - eye_distance: 0.3, - } + Self { eye_distance: 0.3 } } } @@ -341,8 +339,7 @@ impl AlignmentConfig { } if self.eye_distance < 0.1 || self.eye_distance > 0.5 { return Err(Error::Config( - "Alignment eye_distance should be between 0.1 and 0.5 for best results" - .to_string(), + "Alignment eye_distance should be between 0.1 and 0.5 for best results".to_string(), )); } Ok(()) diff --git a/src/pipeline/crop_utils.rs b/src/pipeline/crop_utils.rs index 760b4df..ea987d4 100644 --- a/src/pipeline/crop_utils.rs +++ b/src/pipeline/crop_utils.rs @@ -27,6 +27,7 @@ pub fn crop_face_with_intermediate( img: &DynamicImage, face_data: &FaceData, output_size: u32, + eye_distance: f32, ) -> Result { let (img_width, img_height) = img.dimensions(); @@ -50,20 +51,20 @@ pub fn crop_face_with_intermediate( )); } - // Expand the crop area to include some context around the face - // and make it square for consistent output - let face_size = face_width.max(face_height); - let padding = face_size / 2; // 50% padding on each side - let crop_size = face_size + padding * 2; + // Derive crop size from eye_distance so the initial crop captures + // the right amount of context for the alignment step. + const IPD_RATIO: f32 = 0.37; + const SAFETY_MARGIN: f32 = 1.30; + + let face_size = face_width.max(face_height) as f32; + let estimated_ipd = IPD_RATIO * face_width as f32; + let ideal_crop = (estimated_ipd / eye_distance) * SAFETY_MARGIN; + let crop_size = ideal_crop.max(face_size * 1.5).max(10.0).round() as u32; // Calculate center of face let center_x = (x1 + x2) / 2; let center_y = (y1 + y2) / 2; - if crop_size < 10 { - return Err(Error::ImageProcessing("Crop area too small".to_string())); - } - // Ideal crop region centered on the face (may extend outside image bounds) let ideal_x1 = center_x as i32 - crop_size as i32 / 2; let ideal_y1 = center_y as i32 - crop_size as i32 / 2; diff --git a/src/pipeline/steps/alignment.rs b/src/pipeline/steps/alignment.rs index 8be5305..cc036d9 100644 --- a/src/pipeline/steps/alignment.rs +++ b/src/pipeline/steps/alignment.rs @@ -73,15 +73,6 @@ impl ProcessingStep for AlignmentStep { ctx.image = Some(aligned); - tracing::trace!( - "Aligned: left_eye=({:.1},{:.1}), right_eye=({:.1},{:.1}), target_size={}", - left_eye.x, - left_eye.y, - right_eye.x, - right_eye.y, - output_size - ); - StepOutcome::Continue(ctx) } } @@ -178,10 +169,7 @@ fn calculate_eye_alignment_transform( let left_eye_x = alignment_config.left_eye_x(); let left_eye_y = alignment_config.left_eye_y(); - let left_eye_target = Point::new( - output_size_f * left_eye_x, - output_size_f * left_eye_y, - ); + let left_eye_target = Point::new(output_size_f * left_eye_x, output_size_f * left_eye_y); let right_eye_target = Point::new( output_size_f * (1.0 - left_eye_x), output_size_f * left_eye_y, diff --git a/src/pipeline/steps/crop_and_resize.rs b/src/pipeline/steps/crop_and_resize.rs index 99c201f..54f1aeb 100644 --- a/src/pipeline/steps/crop_and_resize.rs +++ b/src/pipeline/steps/crop_and_resize.rs @@ -34,9 +34,10 @@ impl ProcessingStep for CropAndResizeStep { }; let output_size = config.processing.output.size; + let eye_distance = config.processing.alignment.eye_distance; // Crop returns CropResult with cropped images and face rectangle in crop coordinates - match crop_face_with_intermediate(image, &ctx.face_data, output_size) { + match crop_face_with_intermediate(image, &ctx.face_data, output_size, eye_distance) { Ok(crop_result) => { // Use the pre-resized image from the crop function let cropped_size = crop_result.cropped.width(); diff --git a/src/web/handlers/mod.rs b/src/web/handlers/mod.rs index 39ac9ab..b869a22 100644 --- a/src/web/handlers/mod.rs +++ b/src/web/handlers/mod.rs @@ -95,13 +95,14 @@ pub fn create_router(state: AppState) -> Router { ) .route("/api/config", get(get_config)) .route("/api/config", put(update_config)) - // Serve output files (video, images) with cache headers + // Serve output files (video, images) — no-cache so browsers revalidate + // after reprocessing (filenames stay the same across runs) .nest_service( "/output", ServiceBuilder::new() - .layer(SetResponseHeaderLayer::if_not_present( + .layer(SetResponseHeaderLayer::overriding( header::CACHE_CONTROL, - header::HeaderValue::from_static("public, max-age=86400"), + header::HeaderValue::from_static("no-store"), )) .service(ServeDir::new(output_dir)), )