faces: filter videos out of detection candidate set
The backlog drain pulls every hashed image_exif row, which includes videos. Sending them to Apollo just produces 422 decode_failed → status='failed' markers, burning a round-trip per video and inflating the FAILED stat. Widen filter_excluded to also drop anything is_image_file rejects. Covers both call sites (file-watch hook and per-tick backlog drain) without plumbing a second filter through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -415,22 +415,36 @@ fn try_auto_bind(
|
||||
/// Pulled out for unit testing — the same `PathExcluder` /memories uses,
|
||||
/// just applied at the face-detect candidate set instead of the memories
|
||||
/// listing. Skip @eaDir / .thumbnails / user-defined paths before we burn
|
||||
/// a detect call (and Apollo's GPU memory) on junk.
|
||||
/// a detect call (and Apollo's GPU memory) on junk. Also drops anything
|
||||
/// that isn't an image file — the backlog drain pulls every hashed row in
|
||||
/// `image_exif`, which includes videos; sending those to Apollo just
|
||||
/// produces `failed` markers and inflates the FAILED stat.
|
||||
pub(crate) fn filter_excluded(
|
||||
base: &Path,
|
||||
excluded_dirs: &[String],
|
||||
candidates: Vec<FaceCandidate>,
|
||||
library_name: Option<&str>,
|
||||
) -> Vec<FaceCandidate> {
|
||||
if excluded_dirs.is_empty() {
|
||||
return candidates;
|
||||
}
|
||||
let excluder = PathExcluder::new(base, excluded_dirs);
|
||||
let excluder = if excluded_dirs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathExcluder::new(base, excluded_dirs))
|
||||
};
|
||||
candidates
|
||||
.into_iter()
|
||||
.filter(|c| {
|
||||
let abs = base.join(&c.rel_path);
|
||||
if excluder.is_excluded(&abs) {
|
||||
if !file_types::is_image_file(&abs) {
|
||||
debug!(
|
||||
"face_watch: skipping non-image path {} (library {})",
|
||||
c.rel_path,
|
||||
library_name.unwrap_or("<unknown>")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if let Some(ex) = excluder.as_ref()
|
||||
&& ex.is_excluded(&abs)
|
||||
{
|
||||
debug!(
|
||||
"face_watch: skipping excluded path {} (library {})",
|
||||
c.rel_path,
|
||||
@@ -507,8 +521,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn filter_excluded_empty_rules_passes_all() {
|
||||
// Skip the PathExcluder build entirely on the common path where
|
||||
// EXCLUDED_DIRS is unset — saves an allocation per pass.
|
||||
// EXCLUDED_DIRS unset still lets every image through — only the
|
||||
// PathExcluder is skipped, the image-extension gate still runs.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let base = tmp.path();
|
||||
let candidates = vec![cand("a.jpg"), cand("b.jpg")];
|
||||
@@ -516,6 +530,25 @@ mod tests {
|
||||
assert_eq!(kept.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_excluded_drops_videos_and_non_media() {
|
||||
// Backlog drain pulls every hashed row in image_exif (videos
|
||||
// included). Videos must never reach Apollo — opencv can't
|
||||
// decode them, every call would 422 and write a `failed` marker.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let base = tmp.path();
|
||||
let candidates = vec![
|
||||
cand("photos/a.jpg"),
|
||||
cand("photos/clip.mp4"),
|
||||
cand("photos/clip.MOV"),
|
||||
cand("photos/notes.txt"),
|
||||
cand("photos/b.heic"),
|
||||
];
|
||||
let kept = filter_excluded(base, &[], candidates, Some("test"));
|
||||
let kept_paths: Vec<_> = kept.iter().map(|c| c.rel_path.as_str()).collect();
|
||||
assert_eq!(kept_paths, vec!["photos/a.jpg", "photos/b.heic"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_bytes_passes_through_for_jpeg() {
|
||||
// JPEG goes through plain read — we DON'T want to lose orientation
|
||||
|
||||
Reference in New Issue
Block a user