| name | extract-beat-grid |
| description | detect beats, bars, downbeats, onsets, and BPM from an audio or video file and write a reusable beat-grid.json. Use when you need to snap edit cuts, captions, or SFX to a musical grid — feeds generate-edit-decision-list, render-edl, and review-video-pacing-rhythm. |
extract-beat-grid
Purpose
Analyse an audio or video file and produce a beat-grid.json that encodes the full rhythmic skeleton of the track: BPM, individual beat times, downbeats, bars, and onset transients. Downstream atoms consume this file to snap cut points, caption transitions, or SFX to the musical grid — replacing the previously-missing beat-sync-reel dependency that several skills referenced but no in-tree atom implemented.
If the source is a video file, the audio stream is extracted to a temporary WAV first (via ffmpeg); the atom is otherwise identical for audio-only inputs.
Inputs
| Field | Required | Notes |
|---|
source | yes | Path to an audio file (mp3, wav, m4a, aac, flac) or video file (mp4, mov, webm). |
output_dir | yes | Directory where beat-grid.json and manifest.json are written. Created if absent. |
beats_per_bar | no, default 4 | Time signature numerator — how many beats constitute one bar. |
CLI:
python3 skills/atoms/audio-editing/extract-beat-grid/scripts/extract_beats.py \
--source <path> --output-dir <dir> [--beats-per-bar 4]
Workflow
- Validate that
source exists and ffmpeg/ffprobe are on PATH.
- If the source is a video file (detected by extension or by probing), extract the audio stream to a temp WAV with ffmpeg (
-vn -ar 44100 -ac 1).
- Load audio with
librosa.load() (mono, native sample rate).
- Run
librosa.beat.beat_track() → tempo (BPM float), beat_frames array.
- Convert
beat_frames → beat_times (seconds) with librosa.frames_to_time().
- Run
librosa.onset.onset_detect(units="time") → onset_times array.
- Group beat_times into bars of
beats_per_bar; first beat of each group = downbeat. Build a bars array of {index, start, end} (end = start of next bar, or track duration for the last bar).
- Write
beat-grid.json (schema below).
- Write
manifest.json (standard skill manifest schema).
- Clean up temp WAV if one was created.
beat-grid.json schema
{
"schema_version": "1.0",
"source": "<absolute path>",
"duration_s": 120.4,
"bpm": 128.0,
"beat_interval_s": 0.469,
"beats": [0.46, 0.93, 1.40, ...],
"downbeats": [0.46, 2.32, 4.18, ...],
"bars": [
{"index": 0, "start": 0.46, "end": 2.32},
{"index": 1, "start": 2.32, "end": 4.18},
...
],
"onsets": [0.03, 0.46, 0.82, ...]
}
Output
<output_dir>/beat-grid.json — the rhythmic skeleton of the track.
<output_dir>/manifest.json — skill run metadata (standard manifest schema).
Quality Checks
beat-grid.json is valid JSON with all required top-level keys.
bpm is a positive float; beats is non-empty.
downbeats length equals ceil(len(beats) / beats_per_bar) (±1 for edge truncation).
bars length equals len(downbeats); each bar's end equals the next bar's start.
duration_s matches the source file's ffprobe duration within ±0.5s.
manifest.json exists with status: "pass" and output_files listing beat-grid.json.
Failure Modes
- Source not found — script exits with a clear error before loading anything.
- Video with no audio stream — ffmpeg extraction fails; the atom prints an error and exits non-zero. Supply an audio-only file or a video that has audio.
librosa not installed — install with pip3 install librosa; the script will print an actionable error message and exit with code 2 if librosa is missing.
soundfile / audioread backend missing — librosa needs at least one audio backend. Install with pip3 install soundfile (recommended) or pip3 install audioread.
- Very short or silent audio —
beat_track may return an empty beat array or a nonsensical BPM. The script records a warning in the manifest and still writes the (possibly sparse) JSON; callers should treat a bpm < 20 result as unreliable.
- ffmpeg not on PATH — install via
brew install ffmpeg (macOS) or apt install ffmpeg (Linux).