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) ? '✓' : ''}
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)),
)