A modern Python application for generating visual overlays from FIT files created by sports devices like bike computers (Garmin, Magene, Wahoo) and GPS watches.
overlayer generate, overlayer preview, overlayer info, and overlayer modules run on the new v2 pipeline. The old FrameGenerator, legacy module runtime, and legacy FIT processor have been removed from the active codebase, so new development should target GenerateService and overlayer.v2.
- 🗺️ GPS Track Map - Visualize your route with current position
- 🚴 Speedometer - Horizontal or analog gauge showing current speed
- ❤️ Heart Rate Chart - Bar, line, or area heart-rate graph
- 📊 Statistics - Display all available metrics (heart rate, cadence, power, etc.)
- ⏱️ Time Display - Current activity time
- 🎛️ Module Variants - Mix themes with per-module rendering styles
- 🔌 Modular Architecture - Easy to extend with custom modules and renderer variants
- ⚙️ Flexible Configuration - JSON config or environment variables
# Install the command-line app globally
uv tool install overlayer
# If your shell does not see the command yet
uv tool update-shell
# Verify the install
overlayer --version
overlayer --help# Run the latest published version once
uvx overlayer --version
# Example: inspect a FIT file without a permanent install
uvx overlayer info ride.fituv add overlayerpip install overlayer# Clone the repository
git clone https://github.com/finnetrolle/overlayer.git
cd overlayer
# Install with uv (recommended)
uv sync --all-extras
# Run the local CLI from the virtual environment
uv run overlayer --help
# Or with pip
pip install -e ".[dev]"You need one FIT file, for example ride.fit, exported from a bike computer or sports watch.
# Install the CLI
uv tool install overlayer
# Create a starter config you can edit
overlayer config -o config.json
# Render one preview frame to tune the layout
overlayer preview ride.fit -c config.json -o preview.png
# Generate the full PNG frame sequence
overlayer generate ride.fit -c config.json -o framesAfter that:
- Open
preview.png - Tweak positions and variants in
config.json - Re-run
overlayer preview ...until the layout looks right - Run
overlayer generate ...for the full sequence
If you are using Overlayer for the first time, this is the simplest path from FIT file to finished overlay assets:
# 1. Inspect what is inside the FIT file
overlayer info ride.fit
# 2. Generate a starter config
overlayer config -o config.json
# 3. Render one preview frame and adjust the layout
overlayer preview ride.fit -c config.json -o preview.png
# 4. Render all transparent PNG frames
overlayer generate ride.fit -c config.json -o frames --fps 30
# 5. Optional: see which built-in modules are available
overlayer modulesThe generated frames are saved as transparent PNG files like frames/frame_000000.png.
If you want to place them over a video in ffmpeg, a typical command looks like this:
ffmpeg \
-i input.mp4 \
-framerate 30 -i frames/frame_%06d.png \
-filter_complex "[0:v][1:v]overlay=0:0" \
-c:a copy \
output-with-overlay.mp4Adjust the -framerate value so it matches the --fps value you used for overlayer generate.
# Generate overlays from a FIT file
overlayer generate ride.fit
# Specify output directory and FPS
overlayer generate ride.fit -o output_frames --fps 2
# Generate only specific modules
overlayer generate ride.fit -m map -m speedometer
# Use custom config file
overlayer generate ride.fit -c config.json
# Render one preview frame for layout tuning
overlayer preview ride.fit -c config.json -o preview.png
# Get info about a FIT file
overlayer info ride.fit
# List available modules
overlayer modules
# Generate default config
overlayer config -o config.json# Install once and use it as a normal command
uv tool install overlayer
overlayer --help
# Run it one time without installing
uvx overlayer modules
# Upgrade to the latest published version
uv tool upgrade overlayeroverlayer info ride.fitprints the FIT file duration, timestamps, track points, max speed, and available data fields.overlayer config -o config.jsonwrites a starter JSON config you can edit.overlayer preview ride.fit -c config.json -o preview.pngrenders one frame for fast layout tuning.overlayer generate ride.fit -c config.json -o framesrenders the full transparent PNG sequence.overlayer moduleslists the built-in overlay modules you can enable with-m.
from overlayer import AppConfig, GenerateService
# Load configuration
config = AppConfig.from_json("config.json")
# Generate frames
generator = GenerateService(config)
total_frames = generator.generate(
fit_file="ride.fit",
output_dir="frames",
fps=1,
duration=0, # 0 = full duration
)
print(f"Generated {total_frames} frames")Configuration is managed via JSON file or environment variables.
There are now two visual controls:
theme.variantchanges the shared color palette and drawing tokens.<module>.variantchanges how an individual built-in module is rendered.
Examples:
theme.variant = "street_racer"keeps the same modules, but changes the palette.gauge.variant = "analog_arc"changes the speedometer geometry.heart_rate_chart.variant = "line"changes the chart style without changing the theme.map.variant = "clean_trace"swaps the tactical HUD map for a cleaner route trace.*.variant = "ride_minimal"switches to the new low-chrome action-cam inspired style.
Available module variants:
time.variant,distance.variant,speed_display.variant:cyberpunk_panel,minimal,broadcast_bug,ride_minimalgauge.variant:cockpit_bar,analog_arc,ride_minimalmap.variant:tactical_panel,clean_trace,ride_minimalstats.variant:telemetry_cards,compact_strip,ride_minimalheart_rate_chart.variant,power_chart.variant:bars,line,area,ride_minimal
Use preview when you want to place modules on screen without generating a full frame sequence.
# Render one frame from the middle of the activity
overlayer preview ride.fit -c config.json -o preview.png
# Render a specific moment of the ride
overlayer preview ride.fit -c config.json -o preview.png --at-seconds 120
# Focus only on a few modules while tuning
overlayer preview ride.fit -c config.json -o preview.png -m speedometer -m stats -m mapFast workflow:
- Edit module positions and sizes in
config.json. - Run
overlayer preview .... - Open
preview.png. - Repeat until the layout looks right.
Useful layout fields:
map.x,map.y,map.width,map.heightgauge.panel_x,gauge.panel_y,gauge.panel_width,gauge.panel_heightspeed_display.x,speed_display.y,speed_display.width,speed_display.heighttime.x,time.y,time.width,time.heightdistance.x,distance.y,distance.width,distance.heightstats.x,stats.y,stats.card_width,stats.card_height,stats.columns,stats.gapheart_rate_chart.x,heart_rate_chart.y,heart_rate_chart.width,heart_rate_chart.heightpower_chart.x,power_chart.y,power_chart.width,power_chart.height
Create a config.json file (see config.example.json for a complete example). If you want to start directly with the new minimal style, use config.ride-minimal.json.
{
"frame": {
"width": 1920,
"height": 1080
},
"map": {
"variant": "tactical_panel",
"x": 1450,
"y": 610,
"width": 450,
"height": 450,
"margin": 20
},
"gauge": {
"variant": "cockpit_bar",
"center_x": 150,
"center_y": 930,
"radius": 120,
"start_angle": -135,
"end_angle": 135,
"panel_x": 320,
"panel_y": 850,
"panel_width": 1280,
"panel_height": 150
},
"time": {
"variant": "cyberpunk_panel",
"x": 1690,
"y": 24,
"width": 210,
"height": 68,
"font_scale": 1.0,
"color": [255, 255, 255, 255]
},
"distance": {
"variant": "cyberpunk_panel",
"x": 1690,
"y": 104,
"width": 210,
"height": 68,
"font_scale": 0.8,
"color": [255, 255, 255, 255]
},
"stats": {
"variant": "telemetry_cards",
"x": 10,
"y": 30,
"line_height": 30,
"font_scale": 0.7,
"card_width": 180,
"card_height": 96,
"columns": 3,
"gap": 18,
"max_cards": 6,
"color": [0, 255, 0, 255]
},
"speed_display": {
"variant": "broadcast_bug",
"x": 1390,
"y": 754,
"width": 210,
"height": 80
},
"heart_rate_chart": {
"variant": "line",
"x": 500,
"y": 900,
"width": 400,
"height": 150,
"history_seconds": 60,
"bar_gap": 3,
"zones": {
"zone1_max": 110,
"zone2_max": 130,
"zone3_max": 150,
"zone4_max": 165
}
},
"power_chart": {
"variant": "area",
"x": 930,
"y": 900,
"width": 400,
"height": 150,
"history_seconds": 60,
"bar_gap": 3
},
"theme": {
"variant": "neon_cockpit"
},
"modules": ["time", "distance", "map", "speedometer", "speed_display", "stats", "heart_rate_chart", "power_chart"],
"output_dir": "frames",
"duration": 0,
"fps": 1
}All configuration options can be set via environment variables with the OVERLAYER_ prefix:
export OVERLAYER_FRAME__WIDTH=1920
export OVERLAYER_FRAME__HEIGHT=1080
export OVERLAYER_FPS=2
export OVERLAYER_MODULES='["map", "speedometer"]'
export OVERLAYER_THEME__VARIANT=street_racer
export OVERLAYER_GAUGE__VARIANT=analog_arc
export OVERLAYER_HEART_RATE_CHART__VARIANT=lineoverlayer/
├── src/overlayer/
│ ├── __init__.py # Package exports
│ ├── __main__.py # Entry point (python -m overlayer)
│ ├── cli.py # Typer CLI commands
│ ├── core/
│ │ ├── config.py # Shared Pydantic configuration
│ │ ├── constants.py # Physical constants
│ │ └── __init__.py # Shared core exports
│ └── v2/
│ ├── fit_reader.py # FIT -> ActivityData
│ ├── timeline.py # Fast time-indexed access
│ ├── frame_state.py # Per-frame state for modules
│ ├── surface.py # RGBA surface abstraction
│ ├── compositor.py # Alpha compositing
│ ├── writer.py # PNG output
│ ├── view_models.py # Shared presenter/renderer models
│ ├── generate_service.py # Main v2 pipeline
│ ├── presenters/ # Build per-module view models
│ ├── renderers/ # Variant-specific renderers
│ ├── styles/ # Theme tokens and drawing primitives
│ └── modules/ # Semantic built-in modules
├── tests/
├── pyproject.toml # Project configuration
└── README.md
Create new modules against the v2 contract:
import cv2
from overlayer.v2 import BaseModule, FrameState, Surface
class MyCustomModule(BaseModule):
name = "custom"
def render(self, surface: Surface, frame_state: FrameState) -> None:
cv2.putText(
surface.pixels,
f"Speed: {frame_state.current_speed_kmh:.1f} km/h",
(100, 100),
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(255, 255, 255, 255),
2,
)Register the module in a ModuleRegistry and pass that registry to GenerateService.
Built-in modules now use an internal presenter + renderer + style split. For a quick custom module, subclassing BaseModule is still perfectly fine. If you want multiple rendering styles for one custom module, follow the same internal pattern used by the built-ins and keep data preparation separate from drawing.
# Install development dependencies
uv sync --all-extras
# Or with pip
pip install -e ".[dev]"# Run tests
pytest
# Run with coverage
pytest --cov=overlayermypy src/overlayerruff check src/overlayer
ruff format src/overlayer- Python 3.10+
- OpenCV (opencv-python)
- NumPy
- fitparse
- Pydantic v2
- Typer
- Rich
- structlog
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please read the Contributing Guide for details on our code of conduct and the process for submitting pull requests.
See CHANGELOG.md for a list of changes.