fix(agent): cap figures + global region cap + reset active on stop
Three safety/robustness fixes from the operator's run logs: - Cap figures per frame (MAX_FIGURES, default 8) like components/panels already are. Uncapped, a huge/busy image yielded hundreds of figure boxes → hundreds of per-figure CCIP calls + crops → a 38s job AND a submit too big to accept (image 81602 looped on 413). This is the acute fix. - Global per-JOB backstop (MAX_REGIONS, default 128): if total regions still exceed the cap (long video), keep the highest-scoring and log the drop, so a submit body can never blow past curator's limit. - Stale "active" meter: stop() now resets _active to 0 (no slots remain, so the meter must read 0 at once), and _bump clamps at 0 so a slot finishing after the reset can't drive it negative. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -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.8 · ubuntu24.04-py312 · big-images"
|
VERSION = "2026-06-30.9 · region-caps + active-reset"
|
||||||
|
|
||||||
logbuf.install()
|
logbuf.install()
|
||||||
cfg = Config.from_env()
|
cfg = Config.from_env()
|
||||||
|
|||||||
@@ -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")),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user