GPU Backend β Output Schema
:::tip TL;DR
- Three primary output files:
*_analysis.json(schema 1.5.0),insights.json, andevents_timeline.json *_analysis.jsoncontains the full structured analysis consumed by the Flutter appevents_timeline.jsonis a compact frame-index list used for annotation comparison- All spatial coordinates use meters (ITF court standard), not pixels :::
Output Directory Structureβ
Every analysis run produces a timestamped output directory:
.dev/output/<video_name>_<timestamp>/
βββ output_video.mp4 # Annotated video (local only, skipped in cloud)
βββ analysis_report.pdf # Human-readable performance report
βββ <video_name>_analysis.json # Canonical analysis JSON (schema 1.5.0, embeds "insights")
βββ insights.json # Ranked insight candidates (Phase 6)
βββ ball_trajectory_3d.json # Physics-based 3D trajectory
βββ events_timeline.json # Compact event frame index
βββ shots/ # Per-shot data
βββ shot_001_metadata.json
βββ shot_001_pose.json
βββ shot_001.mp4 (if --save-shot-videos)
In cloud/serverless mode, outputs are uploaded to Firebase Storage at:
results/{uid}/{sessionId}/{sessionId}_combined.json
*_analysis.json β Schema 1.5.0β
The canonical analysis file consumed by the Flutter app. Written by src/export/analysis_export.py.
Top-level structureβ
{
"schema_version": "1.5.0",
"generated_at": "2026-04-14T10:30:00.000Z",
"video": { ... },
"court": { ... },
"session": { ... },
"physics": { ... },
"stats": { ... },
"ball_trajectory": { ... },
"bounces": [ ... ],
"point_ends": [ ... ],
"players": { ... },
"shots": [ ... ],
"speed_heatmap": [ ... ],
"rallies": [ ... ],
"insights": { ... },
"processing": { ... }
}
The insights object embeds the same ranked candidates written standalone to insights.json (produced by the Phase 6 insight engine, src/pipeline/phase6_outputs.py::_emit_insights).
videoβ
{
"fps": 30,
"total_frames": 900,
"duration_ms": 30000,
"width": 1920,
"height": 1080
}
courtβ
All spatial data in the file uses this coordinate system.
{
"width_m": 10.97,
"length_m": 23.77,
"net_y_m": 11.885,
"coordinate_system": "meters",
"origin": "far_baseline_left_sideline",
"notes": "x: 0=left sideline, 10.97=right. y: 0=far baseline, 23.77=near baseline. Net at y=11.885"
}
| Axis | Range | Description |
|---|---|---|
x | 0 β 10.97 m | Left sideline β right sideline |
y | 0 β 23.77 m | Far baseline β near baseline |
| Net | y = 11.885 m | Centre of court |
ball_trajectoryβ
Structure-of-arrays (SoA) for efficient charting. Only frames with a detected ball position are included.
{
"frames": [0, 1, 3, 4, 5],
"timestamp_ms": [0, 33, 100, 133, 167],
"video_x": [0.451, 0.463, 0.487, 0.501, 0.512],
"video_y": [0.318, 0.309, 0.295, 0.287, 0.280],
"court_x": [5.21, 5.34, 5.59, 5.74, 5.86],
"court_y": [12.45, 12.68, 13.14, 13.37, 13.62]
}
| Field | Type | Description |
|---|---|---|
frames | int[] | Frame indices (chronological, sparse) |
timestamp_ms | int[] | Wall-clock ms from video start |
video_x, video_y | float[] | Normalized video coordinates (0β1) |
court_x, court_y | float | null[] | Court meters; null when homography unavailable |
bouncesβ
Array of bounce events. Only bounces that survive the shot-collision filter are included (see Stage 6).
[
{ "frame": 142, "timestamp_ms": 4733, "x": 8.12, "y": 19.34 },
{ "frame": 287, "timestamp_ms": 9567, "x": 3.45, "y": 6.78 }
]
x and y are court meters at the bounce frame; null if ball was not tracked at that frame.
point_endsβ
[
{ "frame": 389, "timestamp_ms": 12967 },
{ "frame": 712, "timestamp_ms": 23733 }
]
Deduplicated: events within 12 frames of each other are collapsed to the first.
shotsβ
Array of detected shot objects, sorted chronologically by frame_idx.
[
{
"shot_id": 1,
"frame_idx": 245,
"end_frame": 262,
"timestamp_ms": 8167,
"is_serve": false,
"serve_frame_idx": null,
"player_position": "bottom",
"classification": {
"type": "forehand",
"confidence": 0.91
},
"physics": {
"speed_kmh": 42.5,
"trajectory_angle_deg": 14.2
},
"landing": {
"position": { "x": 7.81, "y": 18.94 }
},
"pose_sequence": [
{
"frame": 245,
"keypoints": [
{ "name": "left_shoulder", "x": 0.42, "y": 0.31, "visibility": 0.97 },
{ "name": "right_wrist", "x": 0.63, "y": 0.51, "visibility": 0.89 }
]
}
]
}
]
Key shot fields:
| Field | Description |
|---|---|
frame_idx | Frame of racket contact |
is_serve | true when overhead serve motion detected |
serve_frame_idx | Frame of ball toss (serves only) |
player_position | "top" (far court) or "bottom" (near court) |
classification.type | forehand, backhand, serve, volley, unknown |
physics.speed_kmh | Ball speed immediately after contact (clamped β€ 263 km/h) |
pose_sequence | MediaPipe keypoints; sampled every pose_frame_skip frames |
statsβ
Aggregate session statistics.
{
"total_shots": 24,
"serve_count": 4,
"rally_count": 4,
"average_rally_length": 5.0,
"average_ball_speed_kmh": 38.2,
"max_ball_speed_kmh": 52.1,
"consistency_score": 0.74,
"shot_distribution": {
"forehand": 12,
"backhand": 8,
"serve": 4
}
}
consistency_score is normalized to 0.0β1.0 (internal 0β100 scale divided by 100).
speed_heatmapβ
Flattened shot list for visualizing landing positions weighted by speed.
[
{ "x": 7.81, "y": 18.94, "speed_kmh": 42.5, "is_serve": false, "shot_type": "forehand" },
{ "x": 2.14, "y": 4.32, "speed_kmh": 58.3, "is_serve": true, "shot_type": "serve" }
]
Only shots with a detected landing position are included.
ralliesβ
Consecutive shots between serves, grouped into rally sequences.
[
{
"rally_id": 1,
"start_frame": 245,
"end_frame": 612,
"shot_count": 5,
"shots": [245, 312, 389, 467, 542]
}
]
playersβ
Sparse player position data, split by court half.
{
"far_court": [
{
"frame": 100,
"timestamp_ms": 3333,
"players": [
{
"player_idx": 0,
"detected": true,
"bounding_box": { "x1": 812.1, "y1": 201.4, "x2": 924.7, "y2": 510.2 },
"court_position": { "x": 5.4, "y": 3.2 }
}
]
}
],
"near_court": [ ... ]
}
far_court = players on the far baseline (top of video frame). near_court = players on the near baseline (bottom of frame). Only frames with at least one detected player are stored.
processingβ
Timing data for performance monitoring.
{
"ball_tracking": 28.4,
"court_detection": 9.8,
"player_detection": 41.2,
"analysis": 7.1,
"export": 2.3
}
Values are seconds rounded to 2 decimal places.
events_timeline.jsonβ
A compact frame-index list written alongside the analysis JSON. Used by the annotation comparison script (scripts/compare_events.py) to validate pipeline accuracy against human labels.
{
"fps": 30.0,
"total_frames": 900,
"serve_contact_frames": [45, 412],
"serve_toss_frames": [38, 405],
"shot_frames": [112, 189, 267, 344, 421, 498],
"bounce_frames": [124, 201, 279, 356, 433, 510],
"point_end_frames": [367, 734]
}
| Field | Description |
|---|---|
serve_contact_frames | Frames of racketβball contact for serves; de-duplicated within 30-frame window |
serve_toss_frames | Frames of ball toss (serve wind-up) |
shot_frames | Frames of non-serve shot contact; rally-capped at 6 per rally |
bounce_frames | Ground bounces after shot-collision filtering |
point_end_frames | Point-end events; de-duplicated within 12-frame window |
Shot and bounce frame lists never overlap: if a bounce frame falls within 80ms of a shot frame, the bounce is dropped.
Next Stepsβ
- Pipeline Stages β how each output is computed
- GPU Backend Setup β run the pipeline locally
- Firestore Data Model β how results are stored in Firestore
- Analysis Schema Reference β annotate tool schema