fix(importer): skip transparency check on animated images (operator-flagged 2026-05-26: animated WebP triggered 5+ min PIL multi-frame decode → Celery hard-timeout SIGKILL); compute_phash seeks frame 0 defensively — Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

This commit is contained in:
2026-05-26 00:45:08 -04:00
parent 06a2f60c08
commit c0c9e56fb9
2 changed files with 33 additions and 2 deletions
+19 -1
View File
@@ -860,8 +860,26 @@ class Importer:
pass
def _transparency_pct(self, source: Path) -> float:
"""Fraction of fully-transparent pixels in the image. 0.0 if no alpha."""
"""Fraction of fully-transparent pixels in the image. 0.0 if no alpha.
For animated formats (multi-frame WebP / GIF / APNG), short-circuit
to 0.0 instead of decoding every frame. PIL's `getchannel("A")`
forces a full decode of all frames in an animated image, which for
a large animated WebP takes 5+ minutes and blows past the Celery
soft+hard time limits (300s/360s → SIGKILL). Operator-flagged
2026-05-26. Transparency analysis on a multi-frame image isn't
meaningful for art-curation purposes anyway — different frames
have different alpha — so the existing too_transparent skip rule
is bypassed entirely for animated content.
"""
with Image.open(source) as im:
if getattr(im, "is_animated", False):
log.info(
"skipping transparency check for animated image %s "
"(n_frames=%d) — avoids multi-frame decode timeout",
source, getattr(im, "n_frames", 0),
)
return 0.0
if im.mode not in ("RGBA", "LA") and not (
im.mode == "P" and "transparency" in im.info
):
+14 -1
View File
@@ -13,8 +13,21 @@ HASH_SIZE = 8
def compute_phash(pil_image) -> str | None:
"""Perceptual hash of an opened PIL image, as a hex string. None on any
failure (videos/unreadable/non-image)."""
failure (videos/unreadable/non-image).
For animated images (multi-frame WebP/GIF/APNG), explicitly seek to
frame 0 first. Without this, some PIL operations downstream of
imagehash.phash (convert("L"), resize) can iterate all frames and
blow past Celery's hard time limit on large animations
(operator-flagged 2026-05-26 against animated WebPs). The pHash of
frame 0 is the conventional choice for animated content.
"""
try:
if getattr(pil_image, "is_animated", False):
try:
pil_image.seek(0)
except Exception:
pass
return str(imagehash.phash(pil_image, hash_size=HASH_SIZE))
except Exception:
return None