odysseus-dev/odysseus · error · HTTPException
Invalid mask image: {e}
Error message
Invalid mask image: {e} What it means
Raised by _write_bridge_mask in scripts/mlx_image_server.py when PIL cannot decode the uploaded mask, cannot extract the alpha channel, or cannot save the normalized binary mask PNG; the chained exception text is embedded in an HTTP 400. RGBA masks are binarized via alpha (<128 -> 255), other modes are converted to grayscale.
Source
Thrown at scripts/mlx_image_server.py:202
def _write_bridge_mask(raw: bytes, out_path: Path) -> None:
try:
from PIL import Image
import io
except Exception as e:
raise HTTPException(503, "Pillow is required for MLX image edit bridge masks.") from e
try:
img = Image.open(io.BytesIO(raw))
if img.mode == "RGBA":
# OpenAI edits mask convention: transparent = regenerate.
alpha = img.getchannel("A")
mask = alpha.point(lambda p: 255 if p < 128 else 0)
else:
mask = img.convert("L")
mask.save(out_path, format="PNG")
except Exception as e:
raise HTTPException(400, f"Invalid mask image: {e}") from e
def _run_bridge(cmd: list[str]) -> None:
env = os.environ.copy()
proc = subprocess.run(cmd, env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
detail = (proc.stderr or proc.stdout or "MLX Swift bridge failed").strip()
logger.error("MLX Swift bridge failed (%s): %s\n%s", proc.returncode, " ".join(cmd), detail[-4000:])
raise HTTPException(500, detail[-4000:])
def _run_ddcolor_bridge(model: str, image_raw: bytes, out_path: Path) -> None:
bridge = _resolve_bridge(["odysseus-mlx-colorize", "mlx-ddcolor-serve"])
if not bridge:
raise _unsupported_swift_mlx_runtime(model)
with tempfile.TemporaryDirectory(prefix="odysseus-ddcolor-") as td:
inp = Path(td) / "input.png"
_write_bridge_input_image(image_raw, inp)View on GitHub (pinned to f9235ebbf1)
Solutions
- Verify locally: PIL can open the mask and it is grayscale or RGBA; re-save as PNG before upload
- Ensure the multipart field is named 'mask' and contains binary image data
- Send an RGBA PNG where transparent pixels mark the region to regenerate (OpenAI convention), or a plain L-mode mask
- If no mask is needed, omit it for DDColor, but note LaMa/MI-GAN requires one (error 948)
Example fix
# before
files = {'image': img, 'mask': io.StringIO('mask data')}
# after
files = {'image': open('img.png','rb'), 'mask': open('mask.png','rb')} # real PNG mask Defensive patterns
Strategy: validation
Validate before calling
from PIL import Image
m = Image.open(io.BytesIO(mask_bytes))
assert m.mode in ('L','RGBA','RGB'), m.mode
Image.open(io.BytesIO(mask_bytes)).verify()
Image.frombytes(m.mode, m.size, m.tobytes()).save(io.BytesIO(), format='PNG') Prevention
- Export masks as L-mode or RGBA PNG from the editor
- Binary-encode the mask in multipart, never as string
- Test mask round-trip locally before each integration run
When it happens
Trigger: Uploading a mask that is not a decodable image: corrupt bytes, empty upload, wrong field, or an RGBA file whose processing raises (e.g. truncated data stream detected only at getchannel/save time).
Common situations: Client sends the mask as raw base64 text instead of binary in multipart; mask exported from an editor with an unusual bit depth or ICC profile that PIL rejects; field name mismatch sends prompt text as the mask.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/5ca84c0cc97dd9a6.
Report an issue: GitHub.