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 + } +}