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
sourceexists 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_framesarray. - Convert
beat_frames→beat_times(seconds) withlibrosa.frames_to_time(). - Run
librosa.onset.onset_detect(units="time")→onset_timesarray. - Group beat_times into bars of
beats_per_bar; first beat of each group = downbeat. Build abarsarray 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.jsonis valid JSON with all required top-level keys.bpmis a positive float;beatsis non-empty.downbeatslength equalsceil(len(beats) / beats_per_bar)(±1 for edge truncation).barslength equalslen(downbeats); each bar'sendequals the next bar'sstart.duration_smatches the source file's ffprobe duration within ±0.5s.manifest.jsonexists withstatus: "pass"andoutput_fileslistingbeat-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.
librosanot installed — install withpip3 install librosa; the script will print an actionable error message and exit with code 2 if librosa is missing.soundfile/audioreadbackend missing — librosa needs at least one audio backend. Install withpip3 install soundfile(recommended) orpip3 install audioread.- Very short or silent audio —
beat_trackmay 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 abpm < 20result as unreliable. - ffmpeg not on PATH — install via
brew install ffmpeg(macOS) orapt install ffmpeg(Linux).