Metadata-Version: 2.4
Name: overlayer
Version: 2.0.0
Summary: Visual overlay generator for sports data from FIT files
Project-URL: Homepage, https://github.com/finnetrolle/overlayer
Project-URL: Documentation, https://github.com/finnetrolle/overlayer#readme
Project-URL: Repository, https://github.com/finnetrolle/overlayer
Project-URL: Issues, https://github.com/finnetrolle/overlayer/issues
Project-URL: Changelog, https://github.com/finnetrolle/overlayer/blob/main/CHANGELOG.md
Author: Overlayer Team
License-Expression: MIT
License-File: LICENSE
Keywords: cycling,fit,garmin,overlay,sports,video
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.10
Requires-Dist: fitparse>=1.2.0
Requires-Dist: numpy>=1.24.0
Requires-Dist: opencv-python>=4.8.0
Requires-Dist: pillow>=10.0.0
Requires-Dist: pydantic-settings>=2.1.0
Requires-Dist: pydantic>=2.5.0
Requires-Dist: rich>=13.7.0
Requires-Dist: structlog>=24.1.0
Requires-Dist: typer>=0.9.0
Provides-Extra: dev
Requires-Dist: mypy>=1.8.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest>=7.4.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# Overlayer - Visual Overlay Generator for Sports Data

[![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub release](https://img.shields.io/github/v/release/finnetrolle/overlayer?include_prereleases)](https://github.com/finnetrolle/overlayer/releases)

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

## Features

- 🗺️ **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

## Installation

### Using pip

```bash
pip install overlayer
```

### From source (recommended for development)

```bash
# Clone the repository
git clone https://github.com/finnetrolle/overlayer.git
cd overlayer

# Install with uv (recommended)
pip install uv
uv sync

# Or with pip
pip install -e ".[dev]"
```

## Quick Start

### Command Line Interface

```bash
# 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
```

### As a Library

```python
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

Configuration is managed via JSON file or environment variables.

### Theme vs Module Variant

There are now two visual controls:

- `theme.variant` changes the shared color palette and drawing tokens.
- `<module>.variant` changes 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_minimal`
- `gauge.variant`: `cockpit_bar`, `analog_arc`, `ride_minimal`
- `map.variant`: `tactical_panel`, `clean_trace`, `ride_minimal`
- `stats.variant`: `telemetry_cards`, `compact_strip`, `ride_minimal`
- `heart_rate_chart.variant`, `power_chart.variant`: `bars`, `line`, `area`, `ride_minimal`

### Layout Tuning With `preview`

Use `preview` when you want to place modules on screen without generating a full frame sequence.

```bash
# 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 map
```

Fast workflow:

1. Edit module positions and sizes in `config.json`.
2. Run `overlayer preview ...`.
3. Open `preview.png`.
4. Repeat until the layout looks right.

Useful layout fields:

- `map.x`, `map.y`, `map.width`, `map.height`
- `gauge.panel_x`, `gauge.panel_y`, `gauge.panel_width`, `gauge.panel_height`
- `speed_display.x`, `speed_display.y`, `speed_display.width`, `speed_display.height`
- `time.x`, `time.y`, `time.width`, `time.height`
- `distance.x`, `distance.y`, `distance.width`, `distance.height`
- `stats.x`, `stats.y`, `stats.card_width`, `stats.card_height`, `stats.columns`, `stats.gap`
- `heart_rate_chart.x`, `heart_rate_chart.y`, `heart_rate_chart.width`, `heart_rate_chart.height`
- `power_chart.x`, `power_chart.y`, `power_chart.width`, `power_chart.height`

### JSON Configuration

Create a `config.json` file (see [`config.example.json`](config.example.json) for a complete example). If you want to start directly with the new minimal style, use [`config.ride-minimal.json`](config.ride-minimal.json).

```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
}
```

### Environment Variables

All configuration options can be set via environment variables with the `OVERLAYER_` prefix:

```bash
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=line
```

## Project Structure

```
overlayer/
├── 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
```

## Creating Custom Modules

Create new modules against the `v2` contract:

```python
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.

## Development

### Setup

```bash
# Install development dependencies
uv sync --all-extras

# Or with pip
pip install -e ".[dev]"
```

### Testing

```bash
# Run tests
pytest

# Run with coverage
pytest --cov=overlayer
```

### Type Checking

```bash
mypy src/overlayer
```

### Linting

```bash
ruff check src/overlayer
ruff format src/overlayer
```

## Requirements

- Python 3.10+
- OpenCV (opencv-python)
- NumPy
- fitparse
- Pydantic v2
- Typer
- Rich
- structlog

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Contributing

Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for a list of changes.

## Acknowledgments

- [fitparse](https://github.com/dtcooper/python-fitparse) - Python library for parsing FIT files
- [OpenCV](https://opencv.org/) - Computer vision library
- [Pydantic](https://docs.pydantic.dev/) - Data validation using Python type hints
