Skip to content

Manifest Format

manifest.json is the single interface between the two halves of the system. The app validates it strictly on import and refuses anything ambiguous.

You do not normally write this by hand — the preparation script produces it. This page is for troubleshooting an import error or building your own tooling.

{
"schema_version": 1,
"sport": "badminton_singles",
"source": {
"id": "2026-08-10-club-a",
"filename": "IMG_4471.MOV",
"fps": 119.88,
"width": 1920,
"height": 1080,
"recorded_at": "2026-08-10T19:22:00+08:00",
"camera": {
"viewpoint": "receiver",
"height_m": 1.7,
"notes": "centre line, 0.8 m behind baseline"
},
"opponent_id": "opp_003",
"consent": true
},
"shots": [
{
"id": "s_0001",
"contact_frame": 4127,
"contact_time_ms": 34426.09,
"zone": "BL",
"shot_type": "clear",
"detector": {
"source": "manual",
"confidence": 1.0,
"confirmed_by_human": false
},
"clip": {
"path": "clips/s_0001.mp4",
"duration_ms": 2500,
"lead_in_ms": 2500
}
}
]
}
FieldNotes
idIdentifies the footage set. Shown in the app’s Footage list
fpsExact frame rate from container metadata, as a float. Must be 1–1000
recorded_atISO 8601
camera.viewpointreceiver | side | corner | other. Anything but receiver triggers a warning
opponent_idOptional
consentAbsent is treated as false and warned about
FieldNotes
idUnique within the manifest
contact_frameInteger frame index of racket–shuttle contact
contact_time_msMust equal contact_frame / fps × 1000 within one frame
zoneMust be a code the sport config defines
shot_typeOptional, nullable, unused in analysis
detector.sourcetracknet_v3 | tracknet_v4 | manual
detector.confirmed_by_humanSet to true after annotation in the app
clip.pathRelative to the manifest. Absolute paths and .. are rejected
clip.duration_msMust be > 0. The clip ends on the contact frame
clip.lead_in_msMust not exceed duration_ms

Every clip ends exactly on its contact frame. Occlusion is stopping playback early, never trimming a file.

contact_time_ms must agree with contact_frame at the declared fps. The app checks this and rejects the import if they disagree by more than one frame — a mismatch means one of them is wrong, and there is no safe way to guess which.

H.264, same frame rate as source, CRF 20, -g 15 (short GOP), faststart, audio stripped.

The short GOP matters: it keeps decoding predictable so the player can be prerolled reliably before each trial.

The app reports these and aborts the import:

ErrorCause
Unsupported manifest schema versionScript and app versions differ. Re-cut with the matching script
Manifest is for sport “X” but “Y” was expectedWrong --sport when cutting
Zone code not definedWrong zone codes for the sport
contact_time_ms disagrees with contact_frameFrame rate was assumed rather than read
Duplicate shot id / clip pathMalformed shot list
This video has already been importedDelete the existing footage first