odysseus-dev/odysseus · error · HTTPException

Face enhancement failed

Error message

Face enhancement failed

What it means

HTTP 500 from enhance-face: the PIL fallback path (median filter denoise, unsharp mask, contrast/color/brightness enhancement, PNG save) threw an exception. This fires only when GFPGAN was unavailable or failed and the code fell back to pure-PIL enhancement, which then also failed — usually on a corrupt/unsupported image or a PIL build without the needed filters.

Source

Thrown at routes/gallery/gallery_routes.py:2120

            result_img.save(buf, format="PNG")
            return {"image": base64.b64encode(buf.getvalue()).decode()}

        except ImportError:
            # GFPGAN not available — use PIL-based enhancement (no AI, but works everywhere)
            logger.info("GFPGAN not available — using PIL enhancement fallback")
            # Multi-step enhancement: denoise → sharpen → contrast → color boost
            enhanced = img.filter(ImageFilter.MedianFilter(size=3))  # light denoise
            enhanced = enhanced.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))  # sharpen
            enhanced = ImageEnhance.Contrast(enhanced).enhance(1.15)  # slight contrast boost
            enhanced = ImageEnhance.Color(enhanced).enhance(1.1)  # subtle color boost
            enhanced = ImageEnhance.Brightness(enhanced).enhance(1.05)  # slight brightness lift

            buf = io.BytesIO()
            enhanced.save(buf, format="PNG")
            return {"image": base64.b64encode(buf.getvalue()).decode(), "method": "pil"}
        except Exception:
            logger.exception("enhance_face: failed")
            raise HTTPException(500, "Face enhancement failed")

    # ---- Album management (path-param routes) ----

    def _get_or_404_album(db, album_id: str, user):
        album = db.query(GalleryAlbum).filter(GalleryAlbum.id == album_id).first()
        if not album:
            raise HTTPException(404, "Album not found")
        if not user or album.owner != user:
            raise HTTPException(404, "Album not found")
        return album

    def _get_or_404_image(db, image_id: str, user):
        img = db.query(GalleryImage).filter(GalleryImage.id == image_id).first()
        if not img:
            raise HTTPException(404, "Image not found")
        if not user or img.owner != user:
            raise HTTPException(404, "Image not found")
        return img

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Check server logs for the 'enhance_face: failed' traceback to get the root cause
  2. Verify the image opens and re-saves locally with PIL before sending: Image.open(...).convert('RGB').save(...)
  3. Re-encode the image to a plain PNG/JPEG before upload to strip exotic modes
  4. If GFPGAN is intended, install it so the primary path runs instead of the fragile fallback

Example fix

# client pre-flight
from PIL import Image
img = Image.open(io.BytesIO(base64.b64decode(b64))).convert('RGB')
assert img.size[0] > 0 and img.size[1] > 0
Defensive patterns

Strategy: try-catch

Validate before calling

# verify the payload decodes and re-encodes cleanly before the call
from PIL import Image
img = Image.open(io.BytesIO(base64.b64decode(image_b64))).convert('RGB')
assert img.size[0] > 0 and img.size[1] > 0
img.save(io.BytesIO(), format='PNG')

Try / catch

try:
    resp = requests.post(f'{base}/api/image/enhance-face', json=payload, timeout=60)
    resp.raise_for_status()
except requests.HTTPError:
    if resp.status_code == 500:
        payload['image'] = reencode_png(payload['image'])  # normalize exotic modes
        resp = requests.post(f'{base}/api/image/enhance-face', json=payload, timeout=60)
        resp.raise_for_status()

Prevention

When it happens

Trigger: POST an image that decodes to RGB but is truncated (PIL raises on filter/save), an exotic format, or zero-dimension images; the except logs 'enhance_face: failed' with the traceback.

Common situations: Client sends a partially-uploaded base64 blob; image is a CMYK/16-bit TIFF that convert('RGB') mishandles; disk full so PNG save to BytesIO is fine but earlier temp-file GFPGAN path corrupted state.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/c5cec421a9b2e9c9. Report an issue: GitHub.