Measure the frame rate rather than believing the container

`probe` took `r_frame_rate` whenever it was at or under the cap, on the
grounds that it is the rate that keeps every distinct source frame. It is
not a claim about frames at all: ordinary iPhone footage declares 120 over
a stream whose timestamps are 1/30s apart, and resampling it up turned an
11-second clip into 1293 proxy frames instead of 323 — four times the
encode, four times the tracing stills (91MB against 23MB), four times the
blobs and the rows, for 970 frames that are copies of their neighbours.

So `_measured_rate` reads the timestamps and `_choose_rate` keeps whichever
declared rate they bear out. Two details carry it: the times are sorted
before differencing, because an HEVC stream arrives in decode order and
differencing that measures the reordering delay instead of the rate; and
the statistic is the MEDIAN interval, which is what keeps the property the
nominal rate was being taken for — a take held on one frame still reports
the rate of the parts that move, so no distinct frame is dropped. A genuine
120fps capture still extracts at 120, and there is a test on that
specifically.

`Source.probe` also stopped being the place a reading goes to be preserved.
The facts are a pure function of bytes that are the row's own identity, so
a re-upload re-reads them: otherwise every already-uploaded source would
have gone on resampling to four times the frames with no way to correct it
short of deleting the row.

Already-extracted footage is untouched — `extraction_key` still says
scheme 3, so those jobs stay done and reachable. Bumping it re-extracts
everything at the corrected rate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-10-04 23:22:50 -04:00
parent 8d20097e61
commit e26ad723fa
3 changed files with 210 additions and 7 deletions

View file

@ -218,6 +218,17 @@ def sources(request):
"media_type": upload.content_type or "video/mp4"})
row, created = Source.objects.get_or_create(
blob=blob, defaults={"filename": Path(upload.name).name[:255], "probe": facts})
if not created and row.probe != facts:
# THE FACTS ARE RE-READ, NOT REMEMBERED. They are a pure function of
# the bytes, and the bytes are this row's identity — so a
# disagreement means the server reads the file differently now from
# whenever it first saw it, and the fresh reading is the one to keep.
# Storing the first reading forever pins a source to a rate the code
# no longer believes in, and makes it unfixable without deleting the
# row: `extraction.probe` got better at phone footage and every
# already-uploaded source would have gone on being wrong.
row.probe = facts
row.save(update_fields=["probe"])
return JsonResponse({"id": str(row.id), "digest": digest,
"filename": row.filename, "probe": row.probe,
"created": created}, status=201 if created else 200)