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:
@@ -860,8 +860,26 @@ class Importer:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _transparency_pct(self, source: Path) -> float:
|
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:
|
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 (
|
if im.mode not in ("RGBA", "LA") and not (
|
||||||
im.mode == "P" and "transparency" in im.info
|
im.mode == "P" and "transparency" in im.info
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -13,8 +13,21 @@ HASH_SIZE = 8
|
|||||||
|
|
||||||
def compute_phash(pil_image) -> str | None:
|
def compute_phash(pil_image) -> str | None:
|
||||||
"""Perceptual hash of an opened PIL image, as a hex string. None on any
|
"""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:
|
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))
|
return str(imagehash.phash(pil_image, hash_size=HASH_SIZE))
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user