The schema
FH-Ego v1, field by field
This is the whole of what we deliver, one row per field, read out of the code that writes it. 47 fields across three tiers. Where a value has no unit, the table says so rather than leaving the cell empty.
The tables are generated from annotationModel.py, which is the same file the pipeline validates every clip against. There is no second copy of the unit, shape, rate and tier information to drift from this one, and a field added without them fails our build rather than arriving here as a blank cell.
Read this before the tables
position_3d_m ends in _m and is not in metres. Its x and y are image-plane fractions rescaled by two constants chosen by hand; its z is a fixed offset plus a hand-relative depth with the sign discarded. We publish no camera intrinsics, so nothing here converts to a distance. Over the clip below, z varies by 0.071 across the whole sixty-one seconds. Read as metres — which the key invites and the code forbids — that is seven centimetres of hand travel in a minute of work. That is the reduction to absurdity: the number is dimensionless, and treating it as a distance produces a false one.
The key keeps its name under our rule that identifiers already in use are not renamed retroactively. Renaming it is a founder's call, not one this page makes. The unit is not covered by that rule, and this page is where that is said out loud.
confidence is
It is the detector's own score for which hand this is — left or right — one value per detected hand. We do not use that call. The hand field you receive is assigned by where the wrist sits in the frame, so this score qualifies a judgement we discard rather than the label we deliver. It is not a per-joint confidence and not a positional one, the 21 landmarks carry no confidence at all, and its floor is 0.5 rather than 0 because it is a two-class score.
Every clip lists the fields that were generated rather than observed, and none is presented as measurement. On the reference clip they were produced by a template, not a model — its caption_source reads heuristic_stub. The list is:
actions[].actionactions[].descriptionactions[].scene_semanticactions[].camera_motionactions[].start_frameactions[].end_frametracks.object_interactionstracks.action_verbstracks.dense_captions
Tier 1 · Task / session
The clip
One object per delivered clip. Everything else hangs off it.
| Field | Type | Required | Unit | Shape | Rate |
|---|---|---|---|---|---|
task_id |
string | required | none | scalar | once per clip |
title |
string | required | none | scalar | once per clip |
task_label |
string | required | none | scalar | once per clip |
task_name |
string | required | none | scalar | once per clip |
details |
string | required | none | scalar | once per clip |
scene |
string | required | none | scalar | once per clip |
usable |
bool | optional | none | scalar | once per clip |
start_frame |
int | required | none | scalar | once per clip |
end_frame |
int | required | none | scalar | once per clip |
fps |
int | required | hertz | scalar | once per clip |
actions |
array of ActionSegment | required | none | list | see the segment fields below |
tracks |
Tracks | required | none | object | once per clip |
provenance |
ClipProvenance | optional | none | object | once per clip |
task_id — Internal identifier for the annotation run
title — Human-readable title for the clip
task_label — Taxonomy code for the task performed
task_name — Readable name of the task performed
details — Free-text description of the session
scene — Environment the capture was made in
usable — Whether the clip passed QA and may be delivered
start_frame — First frame of the clip
end_frame — The clip's frame COUNT, not the index of its last frame. Frames are numbered from zero, so the last one is end_frame minus one — on the reference clip this reads 1843 while the final action segment ends at 1842
fps — Frames per second of the source video
actions — The task-bounded action segments this clip was cut into
tracks — The parallel supervision layers
provenance — Where this clip came from — see the provenance fields below
Tier 1 · Task / session
Where the clip came from
Carried inside the annotation. The hash lets you verify the bytes offline by rehashing them; the consent receipt is the one handle that resolves against our API rather than in your hand.
| Field | Type | Required | Unit | Shape | Rate |
|---|---|---|---|---|---|
clip_id |
string | required | none | scalar | once per clip |
scheme_version |
string | optional | none | scalar | once per clip |
content_sha256 |
string | optional | none | scalar | once per clip |
cohort |
string | optional | none | scalar | once per clip |
captured_at |
string | optional | none | scalar | once per clip |
consent_receipt_id |
string | optional | none | scalar | once per clip |
qa_verdict |
string | optional | none | scalar | once per clip |
qa_reject_reasons |
array of string | optional | none | list | once per clip |
derived_fields |
array of string | optional | none | list | once per clip |
caption_source |
string | optional | none | scalar | once per clip |
clip_id — Canonical FH1 clip identifier; the tail is derived from the clip's SHA-256
scheme_version — Identifier scheme version
content_sha256 — SHA-256 of the capture. Rehash the file to verify this annotation describes the bytes you hold
cohort — Capture cohort
captured_at — When the capture was recorded (partner-declared)
consent_receipt_id — Consent ledger receipt covering this clip; verifiable at GET /api/v1/consent/receipt/{id}
qa_verdict — Automated QA outcome for this clip
qa_reject_reasons — Closed-vocabulary reject reasons, empty when accepted
derived_fields — Fields on this record that are model-generated, never measured (AGENTS.md 4.1)
caption_source — Which captioner produced the derived caption fields: a model id (e.g. claude-opus-5) or 'heuristic_stub' for development filler. CARRY C.6
Tier 2 · Action segment
One action segment
The clip is divided into fixed windows and each is given a verb-noun code. On the clip below that division is a template, not detection — content-aware segmentation is specified and not built.
| Field | Type | Required | Unit | Shape | Rate |
|---|---|---|---|---|---|
start_frame |
int | required | none | scalar | once per action segment |
end_frame |
int | required | none | scalar | once per action segment |
hand |
string | optional | none | scalar | once per action segment |
scene_semantic |
string | optional | none | scalar | once per action segment |
camera_motion |
string | optional | none | scalar | once per action segment |
action |
string | required | none | scalar | once per action segment |
description |
string | required | none | scalar | once per action segment |
start_frame — First frame index of action chunk
end_frame — Last frame index of action chunk
hand — Operative hand: 'left', 'right', or 'both'
scene_semantic — Interacting surface/workstation area
camera_motion — Camera motion classification
action — Standardized verb-noun action code (e.g. scoop_food)
description — Dense natural language sentence describing physical interaction
Tier 3 · Timeline track
The parallel tracks
Supervision layers that run the length of the clip alongside each other.
| Field | Type | Required | Unit | Shape | Rate |
|---|---|---|---|---|---|
hand_control |
HandControlTrack | required | none | object | once per clip |
object_interactions |
array of string | required | none | list | once per clip |
action_verbs |
array of string | required | none | list | once per clip |
dense_captions |
array of string | required | none | list | one per action segment |
kinematics_3d |
array of HandKinematicsFrame | required | none | list | see the entry fields below |
hand_control — Header describing the kinematics track
object_interactions — The noun half of each action code, de-duplicated. This is NOT object detection — no object detector runs anywhere in the pipeline, and these words are read off the action codes. Generated rather than observed, and listed in the clip's own derived-fields list
action_verbs — Distinct action codes appearing in the clip. Generated rather than observed, and listed in the clip's own derived-fields list. Where the clip records its caption source as a stub, these come from a fixed five-item pool rather than from a model
dense_captions — One caption per action segment. Generated rather than observed, and listed in the clip's own derived-fields list. Each clip records what produced them in its caption-source field; a stub value there means a template wrote them, not a model
kinematics_3d — The sampled wrist track, one entry per sampled frame per detected hand
Tier 3 · Timeline track
The kinematics track header
Describes the sampled track below it — including the sample interval, so the file states its own rate.
| Field | Type | Required | Unit | Shape | Rate |
|---|---|---|---|---|---|
hand_dominance |
array of string | required | none | list | once per clip |
total_frames_annotated |
int | required | none | scalar | once per clip |
kinematic_sample_frames |
int | required | none | scalar | once per clip |
kinematic_sample_every_n_frames |
int | optional | none | scalar | once per clip |
kinematic_sample_rate_hz |
float | optional | hertz | scalar | once per clip |
hand_dominance — Distinct operative-hand codes across this clip's ACTION SEGMENTS — 'left', 'right' or 'both'. Not a list of detected hands: 'both' is not a hand, and it is computed from the generated action track rather than from the tracker
total_frames_annotated — Frames in the source clip
kinematic_sample_frames — The number of ENTRIES in kinematics_3d, not a count of frames — one entry is written per detected hand per sampled frame, so a clip with two hands in view produces more entries than frames. On the reference clip it reads 294 across 185 distinct frames. The name is misleading and is kept only because it is already in use
kinematic_sample_every_n_frames — Every Nth frame was sampled into kinematics_3d.
kinematic_sample_rate_hz — Derived: clip fps ÷ kinematic_sample_every_n_frames.
Tier 3 · Timeline track
One kinematics sample
One entry per sampled frame per detected hand. This is the layer that carries the detector's output.
| Field | Type | Required | Unit | Shape | Rate |
|---|---|---|---|---|---|
frame_idx |
int | required | none | scalar | once per sampled frame per detected hand — the clip states its own interval at tracks.hand_control.kinematic_sample_every_n_frames |
timestamp_sec |
float | required | seconds | scalar | once per sampled frame per detected hand — the clip states its own interval at tracks.hand_control.kinematic_sample_every_n_frames |
hand |
string | required | none | scalar | once per sampled frame per detected hand — the clip states its own interval at tracks.hand_control.kinematic_sample_every_n_frames |
position_3d_m |
object of float | required | none | {x, y, z} | once per sampled frame per detected hand — the clip states its own interval at tracks.hand_control.kinematic_sample_every_n_frames |
velocity_3d_mps |
object of float | required | none | {vx, vy, vz} | once per sampled frame per detected hand — the clip states its own interval at tracks.hand_control.kinematic_sample_every_n_frames |
confidence |
float | required | none | scalar, 0.5–1 — a two-class score, so 0.5 is the floor, not 0 | once per sampled frame per detected hand — the clip states its own interval at tracks.hand_control.kinematic_sample_every_n_frames |
keypoints_21 |
array of object of float | optional | none | 21 x {x, y, z} | once per sampled frame per detected hand — the clip states its own interval at tracks.hand_control.kinematic_sample_every_n_frames |
frame_idx — Index of the source video frame this sample was taken from
timestamp_sec — Time of this sample from the start of the clip
hand — Which hand this sample describes. Assigned by where the wrist sits in the frame, not by the detector's own left/right call — the leftmost hand is labelled left, and with a single hand in view, left of centre is left. The detector's handedness judgement is computed and then discarded
position_3d_m — Camera-relative 3D position of the wrist. NOT METRES despite the key name — x and y are image-plane fractions rescaled by hardcoded constants, z is an arbitrary offset plus a hand-relative depth. No camera intrinsics are published. The key keeps its name under V1
velocity_3d_mps — First difference of position_3d_m over the sample interval. Inherits position's unit problem: it is that dimensionless quantity per second, not m/s
confidence — Per-hand confidence — MediaPipe's HANDEDNESS classifier score — how sure the detector was about its own left/right call. We do not use that call: the hand field below is assigned by wrist position, so this score qualifies a judgement we discard rather than the label we deliver. It is not positional and not per-joint, and the 21 landmarks carry no confidence of their own
keypoints_21 — 21 MediaPipe hand landmarks, normalised image coordinates [x, y, z] per joint, in MediaPipe's canonical joint order (0 = wrist, 4 = thumb tip, 8 = index tip, … 20 = pinky tip)
What one real clip holds
Numbers below are counted from the published reference clip at build time, not typed.
| Clip | FH1-DEMO-20260813-4b13ff60 |
|---|---|
| Source video | 1,843 frames at 30 fps |
| Action segments | 16 |
| Kinematics entries | 294 across 185 distinct frames |
| Sample interval | every 10th frame · 3.0 Hz |
| Entries with no landmarks | 0 |
| Consent receipt | none — this clip predates our deployed consent flow, and it is the next thing we fix |
The sample interval is recorded on every clip rather than assumed, so a file can always be checked against the rate it claims. A track sampled every tenth frame of thirty is a three-hertz track, and calling it thirty would be the easiest false number on this page to publish.
Open the clip these numbers come from · What arrives, and in what container · Every number we publish, and where it comes from
Every JSON file this site publishes is listed with its SHA-256 in /SHA256SUMS. Fetch both and check them with the tool your machine already has:
curl -sO https://fourthhuman.com/SHA256SUMS
curl -sO https://fourthhuman.com/reference-run.json
sha256sum -c SHA256SUMS # or on a Mac: shasum -a 256 -c SHA256SUMS
This is not a signature, and it is worth saying so. Anyone who can change a file here can change the list beside it, so it catches accident, corruption and damage in transport — not somebody who controls this origin. We hold no third-party attestation and claim none. What it does give you lasts: the file you have is byte-for-byte the file we published, and a digest you quote today can be checked against that list a year from now.