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, because identifiers already in use are not renamed. Its unit is the one stated above.
confidence is
It is the detector's score, from 0.5 to 1, for its own left-or-right call on each detected hand. The label in hand is set by where the wrist sits in the frame, not by that call. The 21 keypoints carry no score of their own, and the floor is 0.5 because the score chooses between two classes.
The reference clip lists the fields below as generated rather than observed. They were produced by a template, not a model: its caption_source reads heuristic_stub. Hand keypoints, position, velocity and confidence are derived as well, as the Inside a clip block marks them. 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.
Inside a clip
The outlined panel marks where a clip's video goes. The labels set out what its FH-Ego v1 annotation records.
Each label lists the fields that hold its signal. Measured values are read from the video file. Derived values are produced by a model or a rule.
Video Measured
Egocentric video of the task. The annotation states its frame count and frame rate.
end_framescalar · no unitfpsscalar · hertz
Hands
Each detected hand gets one entry per sampled frame in tracks.kinematics_3d[], stamped with its frame_idx and timestamp_sec. The clip states its own sampling rate in tracks.hand_control.kinematic_sample_rate_hz.
-
21 hand keypoints Derived
They mark the wrist and four points along each finger and the thumb. Each point's x and y are fractions of the frame's width and height, and its z is relative to the wrist.
keypoints_2121 x {x, y, z} · no unit
-
3D hand position and velocity Derived
Position is the wrist's x, y and z: x and y come from where the wrist sits in the frame, and z from the hand model. Velocity is the change in that position since a hand with the same left or right label was last detected, multiplied by the frame rate.
position_3d_m{x, y, z} · no unitvelocity_3d_mps{vx, vy, vz} · no unit
-
Per-hand confidence Derived
The detector scores how sure it is of its own left-or-right call for each hand. The label in
handis set by where the wrist sits in the frame, not by that call.confidencescalar, 0.5–1 · no unithandscalar
Actions
Each window gets one entry in actions[]. The clip names the captioner that wrote them in provenance.caption_source.
-
Action windows Derived
The captioner cuts the clip's timeline into windows. Each one records its start frame, its end frame, the hand in use and one verb-noun code, such as
grasp_object.start_framescalar · no unitend_framescalar · no unithandscalaractionscalar
-
Captions Derived
Each window carries one caption, and the clip also collects them into one list.
descriptionscalartracks.dense_captionslist
| 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. Frames are numbered from zero, so the last frame is end_frame minus one: on the reference clip end_frame is 1843 and the final action window 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 on request
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 window is given a verb-noun code. The windows are derived: their length follows from the frame rate, not from what happens in the footage.
| 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. Read from the action codes, not from object detection; no object detector runs in the pipeline. Derived, and listed in the clip's derived-fields list.
action_verbs — Distinct action codes in the clip. Derived, and listed in the clip's derived-fields list. When the caption source is a stub, they come from a fixed pool of five codes.
dense_captions — One caption per action window. Derived, and listed in the clip's derived-fields list. The caption-source field names what wrote them; a stub value means a template, 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 windows: 'left', 'right' or 'both'. Computed from the action track, not from the hand tracker, so it lists hand codes rather than detected hands.
total_frames_annotated — Frames in the source clip
kinematic_sample_frames — The number of entries in kinematics_3d: one per detected hand per sampled frame, so two hands in view give two entries for one frame. On the reference clip: 294 entries across 185 distinct frames.
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, set by where the wrist sits in the frame: with two or more hands in view the leftmost is left, and with one hand left of centre is left. The detector's own left-or-right score is in confidence.
position_3d_m — Position of the wrist. x and y are its place in the frame, rescaled by fixed constants; z is a fixed offset plus the hand model's depth for the wrist. No physical unit: the values are not metres, and no camera intrinsics are published.
velocity_3d_mps — The change in position_3d_m since a hand with the same left or right label was last detected, multiplied by the frame rate. Tracking runs on every frame, before sampling. No physical unit: position's units per second, not m/s.
confidence — The hand detector's score, from 0.5 to 1, for its own left-or-right call on this hand. One value per detected hand; the 21 keypoints carry no score of their own. The hand label is set by wrist position, not by this call.
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 Derived · fixed four-second windows |
| Kinematics entries | 294 across 185 distinct frames |
| Sample interval | every 10th frame · 3.0 Hz |
| Entries with no landmarks | 0 |
| Consent receipt | none — recorded before the consent flow |
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://fourthuman.ai/SHA256SUMS
curl -sO https://fourthuman.ai/reference-run.json
sha256sum -c SHA256SUMS # or on a Mac: shasum -a 256 -c SHA256SUMS
A checksum list, not a signature. It detects accidental change, corruption and damage in transport. The list is served from the same origin as the files it lists, so a matching digest shows that the file you hold is byte-for-byte the file this site serves.