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

The field whose name is wrong

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.

What 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.

Which values were not measured

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[].action
  • actions[].description
  • actions[].scene_semantic
  • actions[].camera_motion
  • actions[].start_frame
  • actions[].end_frame
  • tracks.object_interactions
  • tracks.action_verbs
  • tracks.dense_captions

Tier 1 · Task / session

The clip

One object per delivered clip. Everything else hangs off it.

13 fields on EgoSchemaDatasetSchema
FieldTypeRequiredUnitShapeRate
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.

10 fields on ClipProvenanceModel
FieldTypeRequiredUnitShapeRate
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.

7 fields on ActionSegmentModel
FieldTypeRequiredUnitShapeRate
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.

5 fields on TracksModel
FieldTypeRequiredUnitShapeRate
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.

5 fields on HandControlTrackModel
FieldTypeRequiredUnitShapeRate
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.

7 fields on HandKinematicsFrameModel
FieldTypeRequiredUnitShapeRate
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.

ClipFH1-DEMO-20260813-4b13ff60
Source video1,843 frames at 30 fps
Action segments16
Kinematics entries294 across 185 distinct frames
Sample intervalevery 10th frame · 3.0 Hz
Entries with no landmarks0
Consent receiptnone — 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

Check what you downloaded

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.