Merge pull request 'Release: agent perf (batched crop embeds) + UI (full-width, copy logs, quiet noise) + truncated-image fix' (#160) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m32s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m32s
This commit was merged in pull request #160.
This commit is contained in:
+16
-2
@@ -86,7 +86,7 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
body{font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;margin:0;
|
body{font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;margin:0;
|
||||||
background:radial-gradient(1200px 600px at 50% -10%,#1a2029,#0f1216);color:var(--fg)}
|
background:radial-gradient(1200px 600px at 50% -10%,#1a2029,#0f1216);color:var(--fg)}
|
||||||
.wrap{max-width:780px;margin:0 auto;padding:28px 20px 48px}
|
.wrap{max-width:none;margin:0;padding:28px 28px 48px}
|
||||||
header{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
|
header{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
|
||||||
.brand{display:flex;align-items:center;gap:10px;font-size:19px;font-weight:700;letter-spacing:.2px}
|
.brand{display:flex;align-items:center;gap:10px;font-size:19px;font-weight:700;letter-spacing:.2px}
|
||||||
.logo{color:var(--acc);font-size:20px}
|
.logo{color:var(--acc);font-size:20px}
|
||||||
@@ -137,6 +137,11 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|||||||
.queue{font:13px ui-monospace,monospace;color:var(--mut)}
|
.queue{font:13px ui-monospace,monospace;color:var(--mut)}
|
||||||
.banner{margin:0 0 14px;padding:.7rem .9rem;border-radius:10px;background:#3a2f12;
|
.banner{margin:0 0 14px;padding:.7rem .9rem;border-radius:10px;background:#3a2f12;
|
||||||
border:1px solid #5a4a17;color:#ffd98a;font-size:13px}
|
border:1px solid #5a4a17;color:#ffd98a;font-size:13px}
|
||||||
|
.logs-h{display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.copybtn{font:600 11px system-ui;letter-spacing:.04em;text-transform:uppercase;
|
||||||
|
background:#262c34;color:var(--fg);border:1px solid var(--bd);border-radius:7px;
|
||||||
|
padding:5px 11px;cursor:pointer}
|
||||||
|
.copybtn:hover{border-color:var(--acc)}
|
||||||
.logs{margin:0;background:#0b0e12;border:1px solid var(--bd);border-radius:10px;padding:12px;
|
.logs{margin:0;background:#0b0e12;border:1px solid var(--bd);border-radius:10px;padding:12px;
|
||||||
height:230px;overflow:auto;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;
|
height:230px;overflow:auto;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;
|
||||||
color:#b9c4d0;white-space:pre-wrap;word-break:break-word}
|
color:#b9c4d0;white-space:pre-wrap;word-break:break-word}
|
||||||
@@ -187,7 +192,9 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class=card>
|
<section class=card>
|
||||||
<div class=card-h>Logs</div>
|
<div class="card-h logs-h">Logs
|
||||||
|
<button class=copybtn id=copybtn onclick=copyLogs()>Copy</button>
|
||||||
|
</div>
|
||||||
<pre class=logs id=logs>waiting for activity…</pre>
|
<pre class=logs id=logs>waiting for activity…</pre>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,6 +245,13 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|||||||
if(atBottom) el.scrollTop=el.scrollHeight
|
if(atBottom) el.scrollTop=el.scrollHeight
|
||||||
}catch{}
|
}catch{}
|
||||||
}
|
}
|
||||||
|
async function copyLogs(){
|
||||||
|
const txt=logs.textContent||''
|
||||||
|
try{ await navigator.clipboard.writeText(txt) }
|
||||||
|
catch{ const t=document.createElement('textarea'); t.value=txt; document.body.appendChild(t);
|
||||||
|
t.select(); try{document.execCommand('copy')}catch{}; t.remove() }
|
||||||
|
copybtn.textContent='Copied'; setTimeout(()=>{copybtn.textContent='Copy'},1200)
|
||||||
|
}
|
||||||
refresh(); refreshLogs()
|
refresh(); refreshLogs()
|
||||||
setInterval(refresh,3000); setInterval(refreshLogs,2500)
|
setInterval(refresh,3000); setInterval(refreshLogs,2500)
|
||||||
</script></body></html>"""
|
</script></body></html>"""
|
||||||
|
|||||||
@@ -58,12 +58,20 @@ class CropEmbedder:
|
|||||||
|
|
||||||
def embed(self, image: Image.Image) -> list[float]:
|
def embed(self, image: Image.Image) -> list[float]:
|
||||||
"""A crop → its embedding as a plain float list, ready to POST."""
|
"""A crop → its embedding as a plain float list, ready to POST."""
|
||||||
|
return self.embed_batch([image])[0]
|
||||||
|
|
||||||
|
def embed_batch(self, images: list) -> list[list[float]]:
|
||||||
|
"""Embed many crops in ONE forward pass — far better GPU utilisation +
|
||||||
|
only one lock acquisition than embedding each crop separately (which
|
||||||
|
starved the GPU and serialised the whole pool)."""
|
||||||
|
if not images:
|
||||||
|
return []
|
||||||
self.load()
|
self.load()
|
||||||
torch = self._torch
|
torch = self._torch
|
||||||
enc = self._processor(images=image, return_tensors="pt")
|
enc = self._processor(images=images, return_tensors="pt")
|
||||||
pixel_values = enc["pixel_values"].to(self._device, self._dt)
|
pixel_values = enc["pixel_values"].to(self._device, self._dt)
|
||||||
with self._infer_lock, torch.no_grad():
|
with self._infer_lock, torch.no_grad():
|
||||||
out = self._model.get_image_features(pixel_values=pixel_values)
|
out = self._model.get_image_features(pixel_values=pixel_values)
|
||||||
pooled = out.pooler_output if hasattr(out, "pooler_output") else out
|
pooled = out.pooler_output if hasattr(out, "pooler_output") else out
|
||||||
vec = pooled[0].float().cpu().numpy().astype(np.float32).reshape(-1)
|
arr = pooled.float().cpu().numpy().astype(np.float32)
|
||||||
return vec.tolist()
|
return [row.reshape(-1).tolist() for row in arr]
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ def install(level: int = logging.INFO) -> None:
|
|||||||
root.addHandler(h)
|
root.addHandler(h)
|
||||||
if root.level == logging.NOTSET or root.level > level:
|
if root.level == logging.NOTSET or root.level > level:
|
||||||
root.setLevel(level)
|
root.setLevel(level)
|
||||||
# Keep the buffer signal-rich: drop the per-request access spam + ultralytics
|
# Keep the buffer signal-rich: silence the chatty HTTP/download libs (every
|
||||||
# banner noise to WARNING.
|
# HF model fetch logs per-request) so the console shows agent activity —
|
||||||
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
# detector loads, job errors, autoscale moves — not request spam.
|
||||||
logging.getLogger("ultralytics").setLevel(logging.WARNING)
|
for noisy in (
|
||||||
|
"uvicorn.access", "ultralytics", "httpx", "httpcore",
|
||||||
|
"huggingface_hub", "urllib3", "filelock",
|
||||||
|
):
|
||||||
|
logging.getLogger(noisy).setLevel(logging.WARNING)
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, ImageFile
|
||||||
|
|
||||||
|
# Load slightly-truncated images (a few missing trailing bytes) instead of
|
||||||
|
# raising — matches the server embedder. These are common in scraped libraries
|
||||||
|
# and would otherwise fail the job 3× then error (operator-flagged 2026-06-30).
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
def is_video(mime: str) -> bool:
|
def is_video(mime: str) -> bool:
|
||||||
|
|||||||
+27
-25
@@ -348,17 +348,6 @@ class Worker:
|
|||||||
ccip_ev = self.cfg.ccip_model or "ccip-default"
|
ccip_ev = self.cfg.ccip_model or "ccip-default"
|
||||||
dv = f"person-{self.cfg.detector_level}"
|
dv = f"person-{self.cfg.detector_level}"
|
||||||
|
|
||||||
def _concept(frame, bbox, t, score, detver, kind="concept"):
|
|
||||||
"""A SigLIP region for one crop (None if below the size floor)."""
|
|
||||||
crop = crop_region(frame, bbox)
|
|
||||||
if crop is None:
|
|
||||||
return None
|
|
||||||
return {
|
|
||||||
"kind": kind, "bbox": list(bbox), "frame_time": t,
|
|
||||||
"score": score, "siglip_embedding": embedder.embed(crop),
|
|
||||||
"embedding_version": embed_version, "detector_version": detver,
|
|
||||||
}
|
|
||||||
|
|
||||||
for t, frame in frames:
|
for t, frame in frames:
|
||||||
# FIGURE boxes: imgutils detect_person ∪ general COCO person,
|
# FIGURE boxes: imgutils detect_person ∪ general COCO person,
|
||||||
# NMS-merged → CCIP identity (+ a concept crop). Covers anime +
|
# NMS-merged → CCIP identity (+ a concept crop). Covers anime +
|
||||||
@@ -367,6 +356,11 @@ class Worker:
|
|||||||
figs = proposers.figures(frame, base)
|
figs = proposers.figures(frame, base)
|
||||||
if not figs:
|
if not figs:
|
||||||
figs = [((0.0, 0.0, 1.0, 1.0), 1.0, "whole")] # whole-frame fallback
|
figs = [((0.0, 0.0, 1.0, 1.0), 1.0, "whole")] # whole-frame fallback
|
||||||
|
|
||||||
|
# Collect every crop that needs a SigLIP embedding, then embed
|
||||||
|
# them in ONE batched forward pass (huge GPU-util + throughput
|
||||||
|
# win vs one forward per crop). CCIP runs per figure inline.
|
||||||
|
pending = [] # (crop, region-template-without-embedding)
|
||||||
for bbox, score, _label in figs:
|
for bbox, score, _label in figs:
|
||||||
crop = crop_region(frame, bbox)
|
crop = crop_region(frame, bbox)
|
||||||
if crop is None:
|
if crop is None:
|
||||||
@@ -381,25 +375,33 @@ class Worker:
|
|||||||
"embedding_version": ccip_ev, "detector_version": dv,
|
"embedding_version": ccip_ev, "detector_version": dv,
|
||||||
})
|
})
|
||||||
if want_siglip:
|
if want_siglip:
|
||||||
regions.append({
|
pending.append((crop, {
|
||||||
"kind": "concept", "bbox": list(bbox), "frame_time": t,
|
"kind": "concept", "bbox": list(bbox), "frame_time": t,
|
||||||
"score": score,
|
"score": score, "detector_version": dv,
|
||||||
"siglip_embedding": embedder.embed(crop),
|
}))
|
||||||
"embedding_version": embed_version, "detector_version": dv,
|
|
||||||
})
|
|
||||||
if not want_siglip:
|
if not want_siglip:
|
||||||
continue
|
continue
|
||||||
# ANATOMY components (booru_yolo: head/cat-head/anatomy/…) →
|
# ANATOMY components (booru_yolo) + PANELS → concept/panel crops.
|
||||||
# concept crops only (not full characters, so no CCIP).
|
|
||||||
for bbox, score, label in proposers.components(frame):
|
for bbox, score, label in proposers.components(frame):
|
||||||
r = _concept(frame, bbox, t, score, f"booru:{label}")
|
crop = crop_region(frame, bbox)
|
||||||
if r is not None:
|
if crop is not None:
|
||||||
regions.append(r)
|
pending.append((crop, {
|
||||||
# PANEL crops (comic page → panels) → kind='panel' (still SigLIP).
|
"kind": "concept", "bbox": list(bbox), "frame_time": t,
|
||||||
|
"score": score, "detector_version": f"booru:{label}",
|
||||||
|
}))
|
||||||
for bbox, score, _label in proposers.panels(frame):
|
for bbox, score, _label in proposers.panels(frame):
|
||||||
r = _concept(frame, bbox, t, score, "panel", kind="panel")
|
crop = crop_region(frame, bbox)
|
||||||
if r is not None:
|
if crop is not None:
|
||||||
regions.append(r)
|
pending.append((crop, {
|
||||||
|
"kind": "panel", "bbox": list(bbox), "frame_time": t,
|
||||||
|
"score": score, "detector_version": "panel",
|
||||||
|
}))
|
||||||
|
if pending:
|
||||||
|
vecs = embedder.embed_batch([c for c, _ in pending])
|
||||||
|
for (_c, tmpl), vec in zip(pending, vecs):
|
||||||
|
tmpl["siglip_embedding"] = vec
|
||||||
|
tmpl["embedding_version"] = embed_version
|
||||||
|
regions.append(tmpl)
|
||||||
self.client.submit(job["job_id"], regions, replace_kinds)
|
self.client.submit(job["job_id"], regions, replace_kinds)
|
||||||
self._bump(processed=1)
|
self._bump(processed=1)
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user