From 80f8eb475640f8d6a7e2293a38423320ce11d8e2 Mon Sep 17 00:00:00 2001
From: Bryan Van Deusen
Date: Tue, 30 Jun 2026 16:09:37 -0400
Subject: [PATCH] feat(gpu): re-process trigger to apply new crop detectors to
the existing library (#1202)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The siglip/ccip backfills skip images that already have current-version regions,
so adding crop detectors only affected NEW images — the back-catalogue would
never be re-cropped. Add a reprocess trigger that resets every done/error job of
a task back to pending, so the agent re-runs the FULL pipeline (figure detection
+ CCIP + concept/panel crops) over the whole library under the current detectors.
- reprocess_gpu_jobs(task='ccip') task + POST /api/gpu/reprocess.
- gpu store reprocess() + GpuAgentCard "Re-process library (re-detect + re-crop)"
button with a confirm (it's heavy).
- Test: a done job resets to pending (attempts cleared).
Co-Authored-By: Claude Opus 4.8
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
---
backend/app/api/gpu.py | 13 ++++++++
backend/app/tasks/ml.py | 31 +++++++++++++++++++
.../src/components/settings/GpuAgentCard.vue | 25 +++++++++++++++
frontend/src/stores/gpu.js | 8 ++++-
tests/test_gpu_jobs.py | 18 +++++++++++
5 files changed, 94 insertions(+), 1 deletion(-)
diff --git a/backend/app/api/gpu.py b/backend/app/api/gpu.py
index 09a488f..26d8377 100644
--- a/backend/app/api/gpu.py
+++ b/backend/app/api/gpu.py
@@ -96,6 +96,19 @@ async def backfill():
return jsonify({"celery_task_id": r.id, "task": task}), 202
+@gpu_bp.route("/reprocess", methods=["POST"])
+async def reprocess():
+ """Reset every done/error job of `task` back to pending so the agent re-runs
+ the WHOLE library under the current pipeline (e.g. after adding crop
+ detectors). Heavy — the back-catalogue is otherwise skipped by the backfills."""
+ body = await request.get_json(silent=True) or {}
+ task = str(body.get("task") or "ccip")
+ from ..tasks.ml import reprocess_gpu_jobs
+
+ r = reprocess_gpu_jobs.delay(task)
+ return jsonify({"celery_task_id": r.id, "task": task}), 202
+
+
# --- Agent (bearer token): lease / submit / heartbeat / fail ------------
@gpu_bp.route("/jobs/lease", methods=["POST"])
diff --git a/backend/app/tasks/ml.py b/backend/app/tasks/ml.py
index 84ca0ee..3501c64 100644
--- a/backend/app/tasks/ml.py
+++ b/backend/app/tasks/ml.py
@@ -549,6 +549,37 @@ def recover_orphaned_gpu_jobs() -> int:
return res.rowcount or 0
+@celery.task(name="backend.app.tasks.ml.reprocess_gpu_jobs")
+def reprocess_gpu_jobs(task_name: str = "ccip") -> int:
+ """Reset every done/error job of `task_name` back to pending so the agent
+ re-runs the WHOLE library under the CURRENT pipeline — e.g. after adding crop
+ detectors (#1202), re-cropping existing images. Heavy + operator-triggered;
+ the back-catalogue won't otherwise re-process (the backfills skip images that
+ already have current-version regions). Returns the number reset."""
+ from datetime import UTC, datetime
+
+ from sqlalchemy import update
+
+ from ..models import GpuJob
+
+ SessionLocal = _sync_session_factory()
+ with SessionLocal() as session:
+ now = datetime.now(UTC)
+ res = session.execute(
+ update(GpuJob)
+ .where(
+ GpuJob.task == task_name,
+ GpuJob.status.in_(["done", "error"]),
+ )
+ .values(
+ status="pending", attempts=0, lease_token=None, leased_at=None,
+ lease_expires_at=None, updated_at=now,
+ )
+ )
+ session.commit()
+ return res.rowcount or 0
+
+
@celery.task(
name="backend.app.tasks.ml.scheduled_ccip_auto_apply",
soft_time_limit=1800, time_limit=2100,
diff --git a/frontend/src/components/settings/GpuAgentCard.vue b/frontend/src/components/settings/GpuAgentCard.vue
index b0da818..45f1638 100644
--- a/frontend/src/components/settings/GpuAgentCard.vue
+++ b/frontend/src/components/settings/GpuAgentCard.vue
@@ -71,6 +71,16 @@
images get these automatically; this catches the back-catalogue.
+ Re-process library (re-detect + re-crop)
+
+ Re-runs the FULL pipeline (figure detection + CCIP + concept/panel crops) on
+ every image — use after changing crop detectors so the back-catalogue
+ gets re-cropped, not just new images. Heavy: re-processes the whole library.
+
+
Character-match strictness
@@ -157,6 +167,7 @@ const masked = ref(true)
const rotating = ref(false)
const backfilling = ref(false)
const backfillingSiglip = ref(false)
+const reprocessing = ref(false)
const threshold = ref(0.85)
const savingThreshold = ref(false)
const autoApply = ref(true)
@@ -307,6 +318,20 @@ async function onBackfillSiglip() {
backfillingSiglip.value = false
}
}
+
+async function onReprocess() {
+ if (!window.confirm('Re-process the ENTIRE library (re-detect + re-crop every image)? This is heavy and runs on the GPU agent.')) return
+ reprocessing.value = true
+ try {
+ await store.reprocess('ccip')
+ toast({ text: 'Library queued for re-processing — run the agent to drain it', type: 'success' })
+ await refreshQueue()
+ } catch (e) {
+ toast({ text: `Could not start re-process: ${e.message}`, type: 'error' })
+ } finally {
+ reprocessing.value = false
+ }
+}