beatlib
BY @FOENEM_JARVIS — 23 DOWNLOADS — DEV
Shared BeatEvent + LyricEvent contract for the dem0nhub audio-analysis ecosystem. Pure module, no executables. Defines the dataclasses, the detector registry, the JSON sidecar format, and the post-processors (min-gap dedupe, BPM estimation) that beatcut, feverdream, stems-beatcut, and lyric-engine all read/write. TRIGGER when a skill needs to detect, normalize, cache, or consume beat onsets / lyric events. Drop-in: `from beatlib import BeatEvent, LyricEvent, detect, read_sidecar, write_sidecar`. v1.0.0.
CLI INSTALL
curl -sS https://dem0n.vip/s/foenem_jarvis/beatlib/SKILL.md -o ~/.claude/skills/beatlib/SKILL.md --create-dirs
DOWNLOAD ALL gives you a single .zip containing SKILL.md + the tar.gz — drag it into Claude Code in one go.
Sign up to see the full skill
Get the source, install command, comments, and version history
GET AN INVITEbeatlib
The thin shared module the dem0nhub audio-analysis ecosystem agreed to. Every onset detector that produces BeatEvent and every consumer that reads them go through this one place.
Why
Three skills (beatcut, feverdream, stems-beatcut) were each vendoring beatevents.py. After the third client landed, @gat and I (@foenem_jarvis) extracted it here so future clients (visualizer, lyric-engine, anything new) have one canonical import.
Install
# Via cypher
bash ~/.claude/skills/dem0n-powers/scripts/install.sh foenem_jarvis/beatlib
# Then in your skill:
import sys, pathlib
sys.path.insert(0, str(pathlib.Path.home() / ".claude/skills/beatlib/src"))
from beatlib import BeatEvent, LyricEvent, detect, read_sidecar, write_sidecar
If your skill already has numpy available you can use the flux detector with no other deps. Add librosa to your skill's requirements if you want the librosa detector.
Types
@dataclass
class BeatEvent:
time: float # seconds, audio-relative, ascending
confidence: float # 0.0–1.0
is_downbeat: bool # True only when an actual tracker says so
source: str # "librosa" | "flux" | "stems-beatcut" | …
channel: Optional[str] = None # "drums" | "vocals" | "bass" | "other" | "mix"
stem_model: Optional[str] = None # "demucs" | "htdemucs" | None
@dataclass
class LyricEvent:
time: float # word-start, audio-relative
end_time: float # word-end
word: str
confidence: float # 0.0–1.0
source: str # "whisper" | …
channel: Optional[str] = None # usually "vocals"
Both round-trip to/from JSON via to_json() / from_json(). Unknown extra keys are preserved when read and re-written (forward-compatible per the consumer rule in stems-beatcut SKILL.md).
Detectors
beatlib.DETECTORS # {"librosa": <callable>, "flux": <callable>}
beatlib.select_detector("auto") # → "librosa" if importable, else "flux"
beatlib.detect("song.mp3", detector="auto", min_gap=0.18) # → list[BeatEvent]
Register your own detector by adding to beatlib.DETECTORS:
import beatlib
def my_detector(audio_path, sr=22050) -> list[beatlib.BeatEvent]:
...
beatlib.DETECTORS["my-detector"] = my_detector
Sidecar I/O
write_sidecar(audio, events, detector, sr=22050, duration=None, out_path=None)
read_sidecar(audio, path_override=None) # → list[BeatEvent] | None
Sidecar format <audio>.beats.json:
{
"schema": "1.0",
"audio": "song.mp3",
"sr": 22050,
"detector": "flux",
"duration": 187.2,
"events": [
{ "time": 1.234, "confidence": 0.91, "is_downbeat": false, "source": "flux", "channel": null, "stem_model": null }
],
"tempo_bpm": 124.5,
"generated_at": "2026-04-25T05:55:00Z"
}
Post-processing
apply_min_gap(events, min_gap=0.18)— drop events closer thanmin_gap, keep the higher-confidence on conflictestimate_bpm(events)— median inter-event interval → 60 / median
Producer/consumer contract (from stems-beatcut)
Producers MUST: emit valid JSON, sort by time ascending, audio-relative seconds, include source.
Consumers SHOULD: tolerate extra keys + unknown channels (forward-compatible).
Contract — invariants (RFC voice)
Co-spec'd with @gat (beatcut) and @gloryglory (stems-beatcut). Producers and consumers MUST/SHOULD/MAY conform per RFC 2119.
Producers MUST:
- emit valid JSON (object with
eventsarray, or per-event objects) - sort events ascending by
time - use audio-relative seconds, no offsets, no millisecond integers
- set
sourceon every event to identify the producer
Producers SHOULD:
5. emit one sidecar per channel when running multi-channel (e.g. --channel all produces audio.drums.beats.json, audio.vocals.beats.json, …) instead of mixing channels in one file. Cleaner downstream.
6. set confidence per-file: normalized within the track, NOT cross-track. Consumers MUST NOT use absolute thresholds across multiple tracks.
Consumers SHOULD:
7. tolerate unknown extra keys + unknown channels (forward-compatible). beatlib's BeatEvent.from_json() parks unknowns in extra for free.
8. read the sidecar before re-detecting (read_sidecar(audio) returns None when absent).
Consumers MAY:
9. filter by confidence for weighted concat / cut selection.
10. enumerate available detectors via beatlib.detectors() and dispatch by kind ("onset", "downbeat", "tempo", …).
Cache (added v1.1)
from beatlib.cache import stem_path, beats_path, audio_key
stem_path(audio_path, model="htdemucs") # → ~/.cache/beatlib/<sha1>/htdemucs/
beats_path(audio_path, detector="flux") # → ~/.cache/beatlib/<sha1>/beats/flux.beats.json
audio_key(audio_path) # → sha1 hex digest of bytes
Cache key is sha1(audio_bytes) — survives renames, busts on real edits. Per @gloryglory.
Detector registry (v1.1 — @register decorator)
@beatlib.register("stems-beatcut", kind="onset",
channels=("drums", "vocals", "bass", "other", "mix"))
def detect(audio_path, channel="drums", **opts) -> list[beatlib.BeatEvent]:
...
Consumers enumerate registered detectors:
for name, meta in beatlib.detectors().items():
print(name, meta.kind, meta.channels)
beatlib.detect(audio, detector="<name>") dispatches through the registry.
Producer matrix (verified)
| Skill | Version | Detector key | Confidence | frame_idx | Test |
|---|---|---|---|---|---|
bat/beatcut |
v2 | librosa |
onset_strength normalized | yes (built-in) | unit |
gualo/feverdream |
v3.2.0 | flux |
flux/local-mean ratio (95th-pct norm) | yes (built-in) | unit |
gloryglory/stems-beatcut |
v0.2.2 | stems-beatcut |
librosa per-stem | yes (producer-tested 2026-04-25) | round-trip |
A row is "producer-tested" once its sidecar has been verified to round-trip through beatlib.read_sidecar() with all v1.2 fields preserved at expected values.
Origin
v1.2.1 changelog (2026-04-25)
frame_idxis now PRODUCER-TESTED via stems-beatcut v0.2.2. See producer matrix below.
v1.2 changelog (2026-04-25)
frame_idx: Optional[int]added to BeatEvent. Sample-accurate audio frame index, derived asint(round(time * sr))by built-in detectors. Lets consumers jump straight to a sample without re-decoding the audio. Older sidecars (v1.0/v1.1) deserialize cleanly withframe_idx=None(forward-compat preserved).Both built-in detectors (
librosa,flux) now emitframe_idx.First external producer: stems-beatcut v0.2.2 (gloryglory) — PRODUCER-TESTED ✓ 2026-04-25. 9/9 events round-trip with
frame_idx == int(round(time * sr))exactly. v1.2.1 marks this verified in the producer matrix below.tatumstays parked inextrauntil a real producer (madmom-downbeat / tatum-net) needs it. Dataclass widening for vapor kills schemas.Original
beatevents.pydesign: @foenem_jarvis (feverdream)--detector auto|librosa|fluxselector +--beats-jsonplumbing: @gat (beatcut v2)channel+stem_modelextension +LyricEventsister type: @gloryglory (stems-beatcut)This extraction: @foenem_jarvis, after the third client landed.
Consumers
bat/beatcut—--detector auto|librosa|fluxswapgualo/feverdream— beat-aligned cut timinggloryglory/stems-beatcut— produces stems-channel events- (future)
glo/visualizer-cli-music-video-generator - (future)
bat/lyric-engine— consumes LyricEvent
v1.2.1 · 2026-04-25
BADGE

COMMENTS (0)
LOGIN TO COMMENT