feat(agent): store ffmpeg's actual failure reason in the job's error field
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m28s

sample_frames_from_url now returns (frames, reason) — reason carries the
SPECIFIC cause on failure (ffmpeg's stderr tail, e.g. "moov atom not found",
or the timeout) instead of only logging it agent-side. The worker folds it
into the failure it reports, so curator's GpuJob.error reads e.g.

  no frames sampled from video — ffmpeg exit 183: moov atom not found ...

instead of the bare "(unprocessable)". The errored-jobs list becomes
self-describing: after a retry sweep, surviving errors name their real
defect without needing the agent log. Return-value plumbing (not shared
state) so concurrent downloaders stay isolated. Agent VERSION → 2026-07-02.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:01:20 -04:00
parent 686808d3f3
commit 1b1d3732dc
3 changed files with 27 additions and 17 deletions
+8 -5
View File
@@ -632,7 +632,7 @@ class Worker:
# mid-download was the failure loop). Environment-agnostic + resilient.
url = f"{self.cfg.fc_url}{job['image_url']}"
with self._timed("decode"):
frames = media.sample_frames_from_url(
frames, ffmpeg_err = media.sample_frames_from_url(
url, job.get("frame_interval_seconds", 4.0),
job.get("max_frames", 64),
headers=self._auth_header, timeout=self.cfg.ffmpeg_timeout,
@@ -644,11 +644,14 @@ class Worker:
if self._stopped(stop_evt):
raise requests.ConnectionError("stopped during video sampling")
# Else couldn't sample. If curator is up, the file is unprocessable
# → a job fault (fail it, don't re-lease forever). If curator is
# unreachable, it's transient → let the loop back off + retry
# (survives a redeploy). ConnectionError is caught as transient.
# → a job fault: fail it WITH ffmpeg's reason, so the job's stored
# error says e.g. "moov atom not found" instead of a bare
# "unprocessable". If curator is unreachable, it's transient → let
# the loop back off + retry (ConnectionError is caught as such).
if self.client.is_reachable():
raise RuntimeError("no frames sampled from video (unprocessable)")
raise RuntimeError(
f"no frames sampled from video — {ffmpeg_err or 'unknown reason'}"
)
raise requests.ConnectionError("curator unreachable during video sampling")
# Temporal dedup: a near-static video re-runs the whole detect+embed
# chain on ~identical frames — drop near-dups HERE (CPU) pre-GPU.