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 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 @@
<span class="checkbox">{selectedImages.has(image.filename) ? '✓' : ''}</span>
</div>
<img
src="/output/{encodeURIComponent(folderName)}/images/{encodeURIComponent(image.filename)}"
src="/output/{encodeURIComponent(folderName)}/images/{encodeURIComponent(image.filename)}?v={cacheBust}"
alt={image.filename}
loading="lazy"
/>

View file

@ -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);

View file

@ -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(())

View file

@ -27,6 +27,7 @@ pub fn crop_face_with_intermediate(
img: &DynamicImage,
face_data: &FaceData,
output_size: u32,
eye_distance: f32,
) -> Result<CropResult> {
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;

View file

@ -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,

View file

@ -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();

View file

@ -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)),
)