Skip to main content

GPU Backend β€” Output Schema

:::tip TL;DR

  • Three primary output files: *_analysis.json (schema 1.5.0), insights.json, and events_timeline.json
  • *_analysis.json contains the full structured analysis consumed by the Flutter app
  • events_timeline.json is 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"
}
AxisRangeDescription
x0 – 10.97 mLeft sideline β†’ right sideline
y0 – 23.77 mFar baseline β†’ near baseline
Nety = 11.885 mCentre 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]
}
FieldTypeDescription
framesint[]Frame indices (chronological, sparse)
timestamp_msint[]Wall-clock ms from video start
video_x, video_yfloat[]Normalized video coordinates (0–1)
court_x, court_yfloat | 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:

FieldDescription
frame_idxFrame of racket contact
is_servetrue when overhead serve motion detected
serve_frame_idxFrame of ball toss (serves only)
player_position"top" (far court) or "bottom" (near court)
classification.typeforehand, backhand, serve, volley, unknown
physics.speed_kmhBall speed immediately after contact (clamped ≀ 263 km/h)
pose_sequenceMediaPipe 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]
}
FieldDescription
serve_contact_framesFrames of racket–ball contact for serves; de-duplicated within 30-frame window
serve_toss_framesFrames of ball toss (serve wind-up)
shot_framesFrames of non-serve shot contact; rally-capped at 6 per rally
bounce_framesGround bounces after shot-collision filtering
point_end_framesPoint-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​