Merge pull request 'Release: GPU re-process trigger (apply new crop detectors to the existing library)' (#156) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m22s
Build images / build-agent (push) Successful in 3m35s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m22s
Build images / build-agent (push) Successful in 3m35s
This commit was merged in pull request #156.
This commit is contained in:
@@ -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"])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -71,6 +71,16 @@
|
||||
images get these automatically; this catches the back-catalogue.
|
||||
</p>
|
||||
|
||||
<v-btn
|
||||
class="mt-3" color="warning" variant="tonal" rounded="pill" size="small"
|
||||
prepend-icon="mdi-backup-restore" :loading="reprocessing" @click="onReprocess"
|
||||
>Re-process library (re-detect + re-crop)</v-btn>
|
||||
<p class="fc-muted text-caption mt-2 mb-0">
|
||||
Re-runs the FULL pipeline (figure detection + CCIP + concept/panel crops) on
|
||||
<b>every</b> image — use after changing crop detectors so the back-catalogue
|
||||
gets re-cropped, not just new images. Heavy: re-processes the whole library.
|
||||
</p>
|
||||
|
||||
<!-- Match strictness -->
|
||||
<div class="fc-section-h mt-5 mb-1">Character-match strictness</div>
|
||||
<div v-if="ml.settings" class="d-flex align-center" style="gap:12px">
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -29,5 +29,11 @@ export const useGpuStore = defineStore('gpu', () => {
|
||||
return await api.post('/api/gpu/backfill', { body: { task } })
|
||||
}
|
||||
|
||||
return { token, rotateToken, status, backfill }
|
||||
// Reset every done/error `task` job to pending → re-run the WHOLE library
|
||||
// under the current pipeline (e.g. after adding crop detectors).
|
||||
async function reprocess(task = 'ccip') {
|
||||
return await api.post('/api/gpu/reprocess', { body: { task } })
|
||||
}
|
||||
|
||||
return { token, rotateToken, status, backfill, reprocess }
|
||||
})
|
||||
|
||||
@@ -91,6 +91,24 @@ async def test_enqueue_embed_backfill_selects_stale_and_unembedded(db):
|
||||
assert current.id not in queued
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reprocess_resets_done_jobs_to_pending(db):
|
||||
# Re-process (#1202): done/error jobs of a task go back to pending so the
|
||||
# agent re-runs the whole library under the current pipeline.
|
||||
from backend.app.tasks.ml import reprocess_gpu_jobs
|
||||
|
||||
img = await _img(db, "r1" * 32)
|
||||
job = await GpuJobService(db).enqueue(img.id, "ccip")
|
||||
job.status = "done"
|
||||
job.attempts = 2
|
||||
await db.commit()
|
||||
|
||||
assert reprocess_gpu_jobs("ccip") >= 1
|
||||
await db.refresh(job)
|
||||
assert job.status == "pending"
|
||||
assert job.attempts == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enqueue_dedupes_same_pair(db):
|
||||
img = await _img(db, "a" * 64)
|
||||
|
||||
Reference in New Issue
Block a user