The method
How one recording becomes usable clips
This page uses “segmentation” in exactly one sense: the cut of one continuous upload into task-bounded Clips, made at QA time. Two other things in our own records carry the same word, and the first section separates all three.
Three things carry this word. This page means one.
The cut described here takes one Recording — a single continuous upload from a Capture Partner — and produces the Clips inside it, each bounded to one task. It runs at QA time, after redaction and before anything reaches a Commissioning lab. Everything below describes that cut and nothing else.
The other two senses name different things, and confusing them with this one would misread what we deliver. We name them here so that a reader never has to guess which is meant.
The distinction matters to a buyer for a plain reason: the first sense decides how many Clips a Recording yields and how many of its seconds are recorded as accepted work, the second decides what the annotation says happened inside a Clip, and the third is not part of what we deliver.
- The cut of one Recording into Clips. This page. It writes time boundaries and mints new Clips, each one carrying the seconds it came from.
- Temporal action segmentation inside a single Clip: verb-noun spans in the exported annotation. On the published reference Clip those spans are fixed four-second windows, and the artifact records its caption source as
heuristic_stub. - Per-pixel masks — a label on every pixel of every frame. Our exporter emits none, and no brief offers them.
The cut is classical, and the output says so
One decode pass reads the redacted copy of the Recording. Each sampled frame becomes greyscale, shrinks to 160×90, and is compared with the frame sampled before it; the mean absolute difference between the two, normalised to a 0–1 scale, is that moment's activity score. Sampling runs at four frames a second by default, which is an analysis rate rather than the frame rate of anything we deliver.
Two further measurements come off the same pass, read from the full-size greyscale frame. Mean luminance records how much light reached the sensor. The variance of the Laplacian is a standard sharpness measure, and it falls when a frame smears under motion. Both are stored per span, as numbers, beside the verdict they informed.
cv2 and numpy are the only third-party libraries this module imports; everything else it reaches for is our own code and the standard library. No model runs in this path. Every result carries the string opencv_motion_v1 in a method field, so any span we show can be traced back to what produced it.
The spans it writes, and where they are recorded
A span is a run of activity with a start second, an end second, a duration, a usable flag, a reason, and the three measurements behind it. Runs are joined across a quiet gap shorter than 1.5 seconds by default, so a Capture Partner who pauses to reposition their hands has one task counted once rather than twice.
A span ends at its last active sample, never at the end of the tolerated gap, so quiet seconds at the end of a run are not counted as work. Accepted span seconds are the quantity the pay record is keyed on: each minted Clip's record carries its own span's duration, not the parent Recording's.
Dead time between runs is written as its own span, with its own reason, rather than dropped. The summary the pass writes therefore accounts for the whole Recording, including where we judged that nothing was happening — which is a different statement from silence.
Every usable span becomes a Clip. Each Clip is written to the origin record at the moment it is minted, carrying the parent recording id, its position in the sequence, and the exact start and end seconds it was cut from. That table refuses UPDATE and DELETE by database trigger, so a later stage — or a contributor who has not read the file — cannot quietly re-point a Clip at different seconds.
Each Clip inherits the Recording's consent receipt. Consent is granted over the upload, and a withdrawal covers all of it, so a Clip that did not carry the parent's receipt would be footage our deletion path could not find. A row with no parent id records an upload that was itself the Clip.
The cut re-encodes rather than copying the stream. A stream copy is faster and can only land on a keyframe, which would start a delivered Clip up to a group of frames before the boundary we measured. Re-encoding costs processor time and buys an exact edge, and every downstream number is derived from that edge. The writer carries no audio track, and every cut file is checked for one before it is returned — a bystander conversation in someone's kitchen is personal data exactly as a face is, and this is the point in the pipeline where that track reliably stops existing.
Minting a Clip does not start the rest of the pipeline. Each new Clip job is written already stopped, before hand tracking, and stays there until a person releases that cohort by name. Hand tracking, captioning and export wait for that release.
max(fps × 4, 60) window over a five-verb round-robin, so their number is derived from the frame count, and QA is nowhere in the call path. Drawn from reference-run.json.frame 0frame 1842
| # | Frames | Seconds | Hand | Verb |
|---|---|---|---|---|
| 1 | 0–119 | 0.0–4.0s | left | grasp_object |
| 2 | 120–239 | 4.0–8.0s | left | organize_items |
| 3 | 240–359 | 8.0–12.0s | both | wipe_surface |
| 4 | 360–479 | 12.0–16.0s | both | inspect_component |
| 5 | 480–599 | 16.0–20.0s | both | place_item |
| 6 | 600–719 | 20.0–24.0s | left | grasp_object |
| 7 | 720–839 | 24.0–28.0s | left | organize_items |
| 8 | 840–959 | 28.0–32.0s | both | wipe_surface |
| 9 | 960–1079 | 32.0–36.0s | left | inspect_component |
| 10 | 1080–1199 | 36.0–40.0s | both | place_item |
| 11 | 1200–1319 | 40.0–44.0s | both | grasp_object |
| 12 | 1320–1439 | 44.0–48.0s | left | organize_items |
| 13 | 1440–1559 | 48.0–52.0s | both | wipe_surface |
| 14 | 1560–1679 | 52.0–56.0s | both | inspect_component |
| 15 | 1680–1799 | 56.0–60.0s | both | place_item |
| 16 | 1800–1842 | 60.0–61.4s | both | grasp_object |
What stops the cut, and what holds a Recording
Nothing here reports zero when it has not measured. Four conditions make the stage raise and retry instead: OpenCV not loading, the file not opening, no frame rate reported, and no readable frames decoded. A fifth path, a missing file, raises before any of that. A crashed analysis and a genuinely empty Recording would otherwise look identical from the outside, and only one of them belongs to the Capture Partner.
A different thing happens when the pass completes and finds no usable span at all: the Recording is held for a person to look at. It is neither rejected nor failed, and the hold is deliberately not an ending — the job records that it is waiting, and no Clip is minted, so nothing from that Recording enters the record accepted work is counted in. We built it this way because a segmenter fault and an unusable hour of footage are indistinguishable from here, and settling one of them at zero on a machine's word would take somebody's day from them.
The thresholds are ours. Three seconds of minimum span length, 1.5 seconds of gap tolerance, an activity threshold of 0.012, a minimum mean brightness of 30 and a minimum Laplacian variance of 80 are values we chose. Each span therefore stores its raw scores alongside the verdict, so a threshold argued differently next month can be re-applied to footage we have already processed without decoding it again.
Darkness is judged before length. Footage shot in poor light shows little frame-to-frame difference whatever the person is doing, so the activity threshold alone would call a busy but dark Recording empty. The two reasons ask for different fixes — one says the work did not count, the other says add a lamp — so brightness is decided first, on the evidence that detects it.
How to read the usable percentage
The usable percentage this stage emits is derived: a threshold applied to three measured scores — activity, luminance and sharpness — which are stored beside the verdict. It measures usable footage, which is a different figure from an acceptance rate.