Merge pull request 'Agent: huge-image load + figure/region caps + stale-active fix' (#171) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m25s

This commit was merged in pull request #171.
This commit is contained in:
2026-06-30 22:47:48 -04:00
5 changed files with 33 additions and 4 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ from .worker import Worker
# Bump on every agent change. The page embeds this and /status reports it; the UI # Bump on every agent change. The page embeds this and /status reports it; the UI
# warns to reload when they differ — so a stale browser-cached page can't be # warns to reload when they differ — so a stale browser-cached page can't be
# mistaken for "the new image didn't deploy". (Belt-and-braces with no-store.) # mistaken for "the new image didn't deploy". (Belt-and-braces with no-store.)
VERSION = "2026-06-30.7 · ubuntu24.04-py312-cuda12.9" VERSION = "2026-06-30.9 · region-caps + active-reset"
logbuf.install() logbuf.install()
cfg = Config.from_env() cfg = Config.from_env()
+4
View File
@@ -34,6 +34,8 @@ class Config:
panel_conf: float panel_conf: float
max_components: int # cap anatomy component crops per frame max_components: int # cap anatomy component crops per frame
max_panels: int # cap panel crops per frame max_panels: int # cap panel crops per frame
max_figures: int # cap figure boxes per frame (each = a CCIP call + crop)
max_regions: int # hard cap on total regions per JOB (submit-size backstop)
@classmethod @classmethod
def from_env(cls) -> Config: def from_env(cls) -> Config:
@@ -58,4 +60,6 @@ class Config:
panel_conf=float(os.environ.get("PANEL_CONF", "0.30")), panel_conf=float(os.environ.get("PANEL_CONF", "0.30")),
max_components=int(os.environ.get("MAX_COMPONENTS", "8")), max_components=int(os.environ.get("MAX_COMPONENTS", "8")),
max_panels=int(os.environ.get("MAX_PANELS", "8")), max_panels=int(os.environ.get("MAX_PANELS", "8")),
max_figures=int(os.environ.get("MAX_FIGURES", "8")),
max_regions=int(os.environ.get("MAX_REGIONS", "128")),
) )
+5 -2
View File
@@ -144,11 +144,14 @@ class Proposers:
def figures(self, image, base_boxes): def figures(self, image, base_boxes):
"""Merge imgutils person boxes (base_boxes: [(bbox, score)]) with the """Merge imgutils person boxes (base_boxes: [(bbox, score)]) with the
general COCO person detector → NMS'd figure boxes [(bbox, score, label)].""" general COCO person detector → NMS'd figure boxes [(bbox, score, label)],
capped to the highest-scoring max_figures. Uncapped, a busy/huge image
(many characters) yields hundreds of boxes → hundreds of per-figure CCIP
calls + crops → a 30s+ job and an oversized submit (operator-flagged)."""
boxes = [(bb, sc if sc is not None else 1.0, "person") for bb, sc in base_boxes] boxes = [(bb, sc if sc is not None else 1.0, "person") for bb, sc in base_boxes]
if self._person is not None: if self._person is not None:
boxes += self._person.detect(image) boxes += self._person.detect(image)
return nms_merge(boxes) return nms_merge(boxes)[: self.cfg.max_figures] # nms_merge is score-desc
def components(self, image): def components(self, image):
if self._anatomy is None: if self._anatomy is None:
+7
View File
@@ -13,6 +13,13 @@ from PIL import Image, ImageFile
# and would otherwise fail the job 3× then error (operator-flagged 2026-06-30). # and would otherwise fail the job 3× then error (operator-flagged 2026-06-30).
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
# Disable PIL's decompression-bomb guard: this is a TRUSTED local library, not an
# untrusted upload surface, so a legitimately huge image (high-res scans/prints,
# 90M+ pixels) must load. The default 89M-pixel limit only WARNS, but PIL raises
# DecompressionBombError at 2× (~179M px) — which would fail those jobs outright
# (operator-flagged 2026-06-30, images of 9095M px).
Image.MAX_IMAGE_PIXELS = None
def is_video(mime: str) -> bool: def is_video(mime: str) -> bool:
return bool(mime) and (mime.startswith("video/") or mime in {"image/gif"}) return bool(mime) and (mime.startswith("video/") or mime in {"image/gif"})
+16 -1
View File
@@ -210,6 +210,8 @@ class Worker:
self._ctrl_stop.set() self._ctrl_stop.set()
with self._lock: with self._lock:
slots, self._slots = self._slots, [] slots, self._slots = self._slots, []
self._active = 0 # no slots left → the meter reads 0 at once; any
# lagging decrement is clamped (see _bump)
for s in slots: for s in slots:
s.stop.set() # each slot releases its inflight on exit s.stop.set() # each slot releases its inflight on exit
@@ -265,7 +267,9 @@ class Worker:
self.processed += processed self.processed += processed
self.errors += errors self.errors += errors
self.transient += transient self.transient += transient
self._active += active # Clamp at 0: a Stop resets _active to 0, so a slot that was mid-image
# decrements afterwards — that must not drive the meter negative.
self._active = max(0, self._active + active)
# --- per-slot loop ----------------------------------------------------- # --- per-slot loop -----------------------------------------------------
def _loop(self, slot: _Slot): def _loop(self, slot: _Slot):
@@ -556,6 +560,17 @@ class Worker:
tmpl["embedding_version"] = embed_version tmpl["embedding_version"] = embed_version
regions.append(tmpl) regions.append(tmpl)
self._record("gpu", time.monotonic() - _t_gpu) self._record("gpu", time.monotonic() - _t_gpu)
# Backstop: never submit an unbounded pile of regions (a pathological
# image / long video). Keep the highest-scoring max_regions so the
# POST body stays sane — curator rejects an oversized one with 413
# (operator-flagged: image 81602 looped on 413).
if len(regions) > self.cfg.max_regions:
regions.sort(key=lambda r: r.get("score", 0.0) or 0.0, reverse=True)
dropped = len(regions) - self.cfg.max_regions
regions = regions[: self.cfg.max_regions]
log.info("job %s: capped regions %d%d (dropped %d)",
job.get("job_id"), len(regions) + dropped,
len(regions), dropped)
_t = time.monotonic() _t = time.monotonic()
self.client.submit(job["job_id"], regions, replace_kinds) self.client.submit(job["job_id"], regions, replace_kinds)
self._record("submit", time.monotonic() - _t) self._record("submit", time.monotonic() - _t)