The method

How a recording is specified, consented and made

A brief names the task, the environment and the device before anything is recorded. The Capture Partner consents on their own phone, and the receipt is written and verified against the ledger before the first byte of the Recording reaches us.

The brief

A capture brief is a row in one file, fourthuman_engine_api/config/capture_briefs.json. Eight briefs live there today. The API opens that file at import time and reads each brief's target volume and availability from it, and the build inlines the same document into the web bundle, so those two figures cannot disagree between the catalogue and the API. The wording of a title is not covered by that: the API keeps its own titles, and the two surfaces word one of them differently today, which the code says beside the entry that does it.

Each brief carries up to eleven fields and nothing else. brief_id names it. title, domain and tag say what the task family is and where it happens. device, resolution and spatial_tracks state the rig and what it must produce. tiers lists which annotation tiers apply. target_volume and availability describe what a commission would produce and whether we can take one. note is optional and carries a sentence a Commissioning lab should read; three of the eight have none.

The file declares its own provenance as target. Every target_volume is a number a commission would aim at rather than footage we hold, and the catalogue renders it behind a Target badge for exactly that reason.

Two things are settled outside the brief, and both matter before anyone plans against one. The stages a Recording passes through are set by the pipeline, and they are the same for every brief. The terms under which a delivery may be used are settled in the engagement.

The four availability values are defined once, in that file, and the definitions below are its own.

  • VALIDATED records that the annotation pipeline has been run end to end on a real recording of that task family. Exactly one brief may carry it.
  • PILOT records that a Capture Partner hub is being stood up for that domain, and that the brief is commissionable now.
  • ON_DEMAND records that the brief is scoped and quoted per engagement, with no hub standing up for that domain.
  • RESEARCH records that the brief is under evaluation and not yet commissionable.

The device

A brief's device field is a specification, agreed before anything is recorded. The eight briefs name six devices between them. Five are rigs — Ray-Ban Meta, GoPro Hero 12, a stereo global-shutter head rig, iPhone LiDAR and a simulator. The sixth is a human feedback portal on the RLHF brief, which records no footage at all; its resolution field reads that the video arrives from the Commissioning lab.

The one recording that has been annotated end to end measures 640×360 at 30 frames per second and runs 1,843 frames. That shape is the recording's own, not a brief's: it is the evidence behind the single VALIDATED badge, and it is evidence about the annotation pipeline rather than about any rig.

Frame 150 of the reference clip as recorded: the wearer's viewpoint, the left hand aligning fabric on a workstation beside a sewing machine. The same frame with FH-Ego v1 annotation drawn on: the left hand's tracked joints and connecting skeleton, its wrist axes, and a burnt-in header. Only the left hand is annotated in this frame.
One frame, twice. Left of the divider is what the device recorded; right of it is the same pixels with the published annotation drawn on. Both are frame 150 of the clip whose full annotation is published with its SHA-256 on the reference clip page.

The device field is a specification rather than a runtime check. A search of the backend for a read of it returns nothing, so the specification travels to a Commissioning lab as a statement of intent, and no code compares footage against it.

What the system does record about a device is one identifier on the consent entry. The app generates it locally as DEV_PWA_ followed by twelve hexadecimal characters from six random bytes, stores it, and reuses it across sessions so the consent and the upload carry the same one. It identifies the installation rather than the handset: we store no make, no model, no operating-system version and no hardware fingerprint.

Consent, at the point of capture

The app moves a Capture Partner through four screens in a fixed order — the brief list, the brief, consent, then record. Consent is not a step at the end, and the screen states that in its own words: this is recorded before you record anything.

Four agreements sit on that screen, and they are separate rows because they are separate permissions. Three are required and one is not.

The button gates on the first, third and fourth, and deliberately not on the second, so declining training use stays a real choice rather than a dead end. An unticked optional box is sent to the server explicitly as false, and the ledger column is nullable with a null read as not granted, so silence never reads as agreement.

The receipt appears on screen the moment the entry is written. It carries the receipt id, the entry's position in the ledger, the timestamp, the entry's own SHA-256, and the hash of the entry before it. Each entry hashes its predecessor, and verify_chain() recomputes every hash and every link from the genesis value rather than trusting a stored flag, so a deleted, reordered or edited row is reported with the sequence number it broke at.

Fourthuman commits to processing a withdrawal request within seven business days of the asking. Business days are Monday to Friday, counted over IST calendar dates from the day the request was submitted. We hold no Indian public-holiday calendar, so a holiday counts as a working day and a request reads older than it is, which errs early rather than late. Asking does not withdraw the consent by itself: it stays active and the footage stays where it is until the request is processed, and the withdrawal is then appended to the same ledger with its own timestamp.

What reaches a Commissioning lab from that entry is a reduced record. ConsentLedgerAuditRecord drops the partner identifier, the device id and the city, and keeps the receipt id, the task, the status, the timestamps, the policy text, the hash, the sequence and the previous hash. A buyer can check from that shape that consent exists, covers this task, is active and has not been silently edited, and none of those questions needs the name of the person who gave it.

The chain binds our own record-keeping too. Entries written before 14 August 2026 carry policy text naming a redaction product we do not run and claiming licence-plate coverage the pipeline does not provide. They are not rewritten. Each entry's SHA-256 covers that text and every later entry's prev_hash covers that hash, so correcting one would either break the chain or silently re-hash it, which is precisely the tamper this ledger exists to make detectable. The remedy is forward-only, and what to do about the entries already issued is a founder decision that has not been made.

No count of those entries is published. The ledger this engine writes to is a local runtime file that is not in the repository, so a number counted from it on one machine would not reproduce on another, and no figure on this page is taken from it.

  • The first agreement covers recording this brief for Fourthuman. There is no capture without it.
  • The second agreement is optional and covers using the footage to train robot models. A Capture Partner can leave it unticked, record anyway and receive the same pay; the Clip is then refused from any training release, and datasetReleaseService.create_release returns 422 naming the declined Clip.
  • The third agreement states what redaction does and does not do. Faces are blurred before footage is delivered to any buyer. Licence plates are not blurred. Sound is recorded and is not removed from the copy we keep, though the copy a buyer receives carries no sound at all.
  • The fourth agreement states that withdrawal is a request a person processes, and not a switch that acts on its own.

The upload

The upload speaks tus 1.0.0, and it implements a subset of it: core, creation and termination. Creation-with-upload, expiration, parallel chunks and the checksum extension are not implemented. Core is the base protocol rather than an extension, so the Tus-Extension header advertises exactly two values — creation and termination — and software written against the standard negotiates correctly instead of discovering the gap mid-transfer.

Resumability is the design constraint here rather than a refinement. Capture Partners will upload multi-gigabyte Recordings over Indian mobile networks, where a connection that survives four gigabytes uninterrupted is the exception. Nobody has yet uploaded a Recording through this path.

An older single-shot ingestion route is still mounted, and it reads the whole file into memory before a byte reaches disk. Its own docstring says it is retained for small clips and for the test harness and is not suitable for field capture, which is why the capture app uses the resumable route instead.

The offset is always the real size of the partial file on disk, read at request time, and never a counter that could disagree with the bytes we hold. That is what makes an interrupted transfer safe: whatever reached disk is the new offset, and the transfer continues from there without anything being reconciled.

The uploader is hand-rolled rather than a dependency, because the resume story is specific to us — the offset has to be reconciled against IndexedDB and against the server on every restart, which is a wrapper around any library we might have used. It sends each chunk over XHR, so a partner watching a three-gigabyte upload sees byte-level progress instead of a frozen bar. Chunks start at 8 MiB and halve down to a floor of 512 KiB after repeated failure, because retrying 8 MiB forever on a link that cannot hold it is how an upload never finishes. The server stays the authority on the offset: every resume begins with a HEAD, and a 409 mid-flight triggers a re-HEAD and a retry from the true offset.

The queue lives in IndexedDB, which stores the file itself alongside the upload URL and the byte offset reached so far. The work therefore survives a tab close, a browser kill and a phone reboot. localStorage would have made resumable mean resumable until you lock your screen.

Consent is checked more than once, because a multi-gigabyte transfer runs for hours. Creation refuses outright without a consent receipt id, and that id is verified against the ledger rather than trusted from the request. A transfer already in flight re-asks the ledger every 300 seconds, and the finaliser re-checks unconditionally before anything is minted. A withdrawal that lands mid-transfer stops the upload and discards the staged bytes rather than keeping them.

Several refusals happen before a byte moves. A non-video is rejected at creation rather than after four gigabytes. An upload longer than the 16 GiB Tus-Max-Size is rejected. A cohort code that names a place is rejected, and the refusal names the rule instead of saying invalid, because the cohort code reaches a filename that reaches a buyer.

Both halves are tested. test_resumable_upload.py reports 45 passed and 0 failed in process. test_resumable_upload_http.py reports 33 passed and 0 failed over HTTP. These two numbers are not transcribed from a terminal: a gate re-runs both suites before every push and refuses the push if either stops matching this sentence.

Two limits are worth stating against that list. The per-upload lock is single-process, so the rule that forbids concurrent writes to one upload holds within one server process and not across a fleet. And we implement no checksum extension, so no chunk is verified in transit; the SHA-256 a Clip id carries is computed on the finished file at mint, which catches a corrupted transfer at the end rather than during it.

  • Bytes that landed before a dropped connection are kept, and the offset equals the real bytes on disk rather than a counter.
  • Another Capture Partner cannot append to this upload, terminate it, or even read its offset.
  • An over-send is truncated at the declared length, and the file never grows past it.
  • A completed upload cannot be terminated, and retrying its last chunk is a no-op rather than a 409.
  • A withdrawal mid-transfer stops the upload, and the withdrawn bytes are discarded rather than kept.
  • A ledger we cannot read fails loudly, and never as a withdrawal.
  • An outage at our end does not destroy the partner's staged bytes.

What a Capture Partner sees

The app checks a recording on the phone before it uploads it. Rejecting a Clip after a four-gigabyte upload costs a partner an hour of mobile data; rejecting it ten seconds after recording, while the scene is still in front of them, costs neither of us and lets them shoot it again.

The check copies the server's method rather than approximating it. It samples frames, converts each to greyscale, and computes blur as the variance of the 3×3 Laplacian response and lux as the mean greyscale intensity, against the same thresholds the server's gate uses — 80.0 and 30.0, mirrored from one configuration and changed together. The phone samples twelve frames where the server samples up to fifty.

Laplacian variance depends on resolution, so the sampling never resamples. Frames are drawn at native pixel scale and a 3×3 grid of full-resolution tiles is read back. The grid is deliberate: egocentric footage is sharpest at the centre, where the hands and the action are, so a centre crop would flatter every clip and pass ones the server rejects.

The browser decodes video through its own colour path, whose matrix and range may differ from the server's. Rather than pretend the two agree, anything within 25% of a threshold reports as BORDERLINE instead of a verdict, and calibrating that band against real QA results is work we have not done.

Dropped frames are checked on the server rather than on the phone: the server detects them from decoder read failures, and a browser gives no equivalent signal.

preflight.test.mjs reports 27 passed and 0 failed, re-run 05-09-2026. Three of those assertions score a fixed sixteen-by-sixteen tile in the browser code and compare the result against numbers captured from the server's own algorithm in Python. Those numbers are a frozen reference written into the test file, not a live run, so the three catch a change on the browser side and would not catch one on the Python side. A fourth asserts that downscaling changes the sharpness score, which is what holds the measurement at native scale.

Beyond the check, a partner reads their consent receipt on the record screen, their consents with each one's current status in its own panel, and the device id that every consent entry carries.

One thing a partner cannot do is read their own queries back from us. The engine accepts a partner Query by POST and has no route that returns one, so a pending withdrawal request appears on the phone it was asked from and nowhere else. The consent screen says it in those words rather than promising an account view.

What this does not cover

Everything above describes code that runs today. This section names what does not.

  • We have operated none of the five rigs the eight briefs name. A device is a specification, and the catalogue and each of the eight brief pages say so at the point of use.
  • No cohort has recorded against any brief. Cohort codes are sequence codes such as C01, and the one published reference Clip carries the reserved code DEMO.
  • The published reference Clip carries consent_receipt_id: null. It predates the consent link, so it demonstrates the annotation pipeline rather than the consent-to-upload join; the test suites above are what evidence that join.
  • The Capture Partner app is not in the deployed public build. routes.json marks it private, private routes are not deployed, and the single exception is the offline page.
  • The app reaches a development host today. isAllowedApiBase accepts this origin, this host on the default port, or loopback, and refuses everything else.
  • Redaction covers faces and does not cover licence plates. Which detector runs depends on the machine: YuNet runs when OpenCV exposes FaceDetectorYN and the weights file is present, a Haar cascade runs when it does not, and the stage raises rather than emitting an unredacted clip when neither loads. The policy text written into a consent receipt names YuNet without that condition, which is an open defect we have filed. What either detector means for a delivery belongs on the redaction page.
  • Sound is recorded and stays on the copy we keep. The copy a buyer receives carries no sound, because the writer we use writes video only, and the pipeline asserts that on the output rather than assuming it.
  • No acceptance rate is published, and the code refuses to invent one: when nothing has been through QA the acceptance report returns no rate rather than a zero, and it will not call any rate sufficient for pricing until its confidence interval is narrow enough. The store it reads is a local runtime file that is not in the repository, so no figure on this page is counted from it.
  • We publish no price, no delivery date and no certification. We hold no third-party attestation of any kind.