Fixed an issue with caching. Initial resize is tied to eye distance setting.

This commit is contained in:
Arnaud_Cayrol 2026-02-07 22:53:39 +01:00
parent 250cbd491a
commit 4f4f64aab1
7 changed files with 24 additions and 33 deletions

View file

@ -14,6 +14,7 @@
let deleting = $state(false); let deleting = $state(false);
let compiling = $state(false); let compiling = $state(false);
let videoExists = $state(false); let videoExists = $state(false);
let cacheBust = $state(Date.now());
let selectedCount = $derived(selectedImages.size); let selectedCount = $derived(selectedImages.size);
let allSelected = $derived(images.length > 0 && selectedImages.size === images.length); let allSelected = $derived(images.length > 0 && selectedImages.size === images.length);
@ -21,6 +22,7 @@
async function loadImages() { async function loadImages() {
loading = true; loading = true;
error = null; error = null;
cacheBust = Date.now();
try { try {
const res = await fetch(`/api/output/${encodeURIComponent(folderName)}/images`); const res = await fetch(`/api/output/${encodeURIComponent(folderName)}/images`);
if (!res.ok) { if (!res.ok) {
@ -197,7 +199,7 @@
<span class="checkbox">{selectedImages.has(image.filename) ? '✓' : ''}</span> <span class="checkbox">{selectedImages.has(image.filename) ? '✓' : ''}</span>
</div> </div>
<img <img
src="/output/{encodeURIComponent(folderName)}/images/{encodeURIComponent(image.filename)}" src="/output/{encodeURIComponent(folderName)}/images/{encodeURIComponent(image.filename)}?v={cacheBust}"
alt={image.filename} alt={image.filename}
loading="lazy" loading="lazy"
/> />

View file

@ -7,9 +7,10 @@
let { folderName = null } = $props(); let { folderName = null } = $props();
let cacheBust = $state(Date.now());
let videoUrl = $derived( let videoUrl = $derived(
folderName folderName
? `/output/${encodeURIComponent(folderName)}/${encodeURIComponent(folderName)}.mp4` ? `/output/${encodeURIComponent(folderName)}/${encodeURIComponent(folderName)}.mp4?v=${cacheBust}`
: null : null
); );
let videoError = $state(false); let videoError = $state(false);

View file

@ -316,9 +316,7 @@ pub struct AlignmentConfig {
impl Default for AlignmentConfig { impl Default for AlignmentConfig {
fn default() -> Self { fn default() -> Self {
Self { Self { eye_distance: 0.3 }
eye_distance: 0.3,
}
} }
} }
@ -341,8 +339,7 @@ impl AlignmentConfig {
} }
if self.eye_distance < 0.1 || self.eye_distance > 0.5 { if self.eye_distance < 0.1 || self.eye_distance > 0.5 {
return Err(Error::Config( return Err(Error::Config(
"Alignment eye_distance should be between 0.1 and 0.5 for best results" "Alignment eye_distance should be between 0.1 and 0.5 for best results".to_string(),
.to_string(),
)); ));
} }
Ok(()) Ok(())

View file

@ -27,6 +27,7 @@ pub fn crop_face_with_intermediate(
img: &DynamicImage, img: &DynamicImage,
face_data: &FaceData, face_data: &FaceData,
output_size: u32, output_size: u32,
eye_distance: f32,
) -> Result<CropResult> { ) -> Result<CropResult> {
let (img_width, img_height) = img.dimensions(); 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 // Derive crop size from eye_distance so the initial crop captures
// and make it square for consistent output // the right amount of context for the alignment step.
let face_size = face_width.max(face_height); const IPD_RATIO: f32 = 0.37;
let padding = face_size / 2; // 50% padding on each side const SAFETY_MARGIN: f32 = 1.30;
let crop_size = face_size + padding * 2;
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 // Calculate center of face
let center_x = (x1 + x2) / 2; let center_x = (x1 + x2) / 2;
let center_y = (y1 + y2) / 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) // 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_x1 = center_x as i32 - crop_size as i32 / 2;
let ideal_y1 = center_y as i32 - crop_size as i32 / 2; let ideal_y1 = center_y as i32 - crop_size as i32 / 2;

View file

@ -73,15 +73,6 @@ impl ProcessingStep for AlignmentStep {
ctx.image = Some(aligned); 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) StepOutcome::Continue(ctx)
} }
} }
@ -178,10 +169,7 @@ fn calculate_eye_alignment_transform(
let left_eye_x = alignment_config.left_eye_x(); let left_eye_x = alignment_config.left_eye_x();
let left_eye_y = alignment_config.left_eye_y(); let left_eye_y = alignment_config.left_eye_y();
let left_eye_target = Point::new( let left_eye_target = Point::new(output_size_f * left_eye_x, output_size_f * left_eye_y);
output_size_f * left_eye_x,
output_size_f * left_eye_y,
);
let right_eye_target = Point::new( let right_eye_target = Point::new(
output_size_f * (1.0 - left_eye_x), output_size_f * (1.0 - left_eye_x),
output_size_f * left_eye_y, output_size_f * left_eye_y,

View file

@ -34,9 +34,10 @@ impl ProcessingStep for CropAndResizeStep {
}; };
let output_size = config.processing.output.size; 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 // 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) => { Ok(crop_result) => {
// Use the pre-resized image from the crop function // Use the pre-resized image from the crop function
let cropped_size = crop_result.cropped.width(); let cropped_size = crop_result.cropped.width();

View file

@ -95,13 +95,14 @@ pub fn create_router(state: AppState) -> Router {
) )
.route("/api/config", get(get_config)) .route("/api/config", get(get_config))
.route("/api/config", put(update_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( .nest_service(
"/output", "/output",
ServiceBuilder::new() ServiceBuilder::new()
.layer(SetResponseHeaderLayer::if_not_present( .layer(SetResponseHeaderLayer::overriding(
header::CACHE_CONTROL, header::CACHE_CONTROL,
header::HeaderValue::from_static("public, max-age=86400"), header::HeaderValue::from_static("no-store"),
)) ))
.service(ServeDir::new(output_dir)), .service(ServeDir::new(output_dir)),
) )