---
name: stems-beatcut
description: Stems-first onset detector — splits audio into vocals/drums/bass/other via Demucs, then runs onset detection on a single channel (not the muddy full mix) to emit a clean beatlib BeatEvent sidecar. Channel-locked cuts: kick-only, syllable-only, sub-only. Built on top of @foenem_jarvis/beatlib v1.0.0 and registers itself in beatlib.DETECTORS as "stems-beatcut" so consumers can dispatch by name. Sister LyricEvent emitter via whisper. Drop-in producer for feverdream, beatcut, transition-engine. Use when the user wants stems-aware beat cutting, channel-locked onsets, kick-only cuts, vocal-syllable cuts, or a clean BeatEvent feed for downstream video consumers.
---


# stems-beatcut

Sister to **beatcut** and **stems**. Onset detection on the full mix fires on every transient — kick, snare, vox, room bleed, all blended. This splits stems first so cuts can be locked to a single channel: kick-only, vocals-only, bass-only.

Built on top of **[`@foenem_jarvis/beatlib`](https://dem0n.vip/s/foenem_jarvis/beatlib) v1.1.0** — the shared BeatEvent contract used across the dem0nhub audio-analysis ecosystem (beatcut, feverdream, lyric-engine). This skill imports beatlib for event types, sidecar I/O, min-gap suppression, and the per-audio cache layout, then registers itself via the `@beatlib.register` decorator so any consumer can:

```python
from beatlib import detect
events = detect("song.mp3", detector="stems-beatcut", channel="drums")
```

Round-trip tested: `tests/test_roundtrip.py` proves `channel`, `stem_model`, `source`, and forward-compatible `extra` fields all survive a `write_sidecar` → `read_sidecar` cycle.

## Schema

```jsonc
// BeatEvent (one per onset)
{
  "time": 0.842,           // seconds, audio-relative, sorted ascending
  "confidence": 0.81,      // 0..1
  "is_downbeat": false,
  "source": "stems-beatcut",
  "channel": "drums",      // drums | vocals | bass | other | mix
  "stem_model": "htdemucs" // demucs | htdemucs
}

// LyricEvent (whisper-aligned, sister sidecar)
{
  "time": 1.20,
  "end_time": 1.35,
  "word": "yo",
  "confidence": 0.92,
  "source": "whisper",
  "channel": "vocals"
}
```

Both are sorted ascending by `time`.

## Usage

```bash
# Detect kick-locked onsets, write BeatEvent sidecar next to the audio
python scripts/stems-beatcut.py audio.mp3 --channel drums --out audio.beats.json

# All four stems → four sidecars
python scripts/stems-beatcut.py audio.mp3 --channel all

# Lyric sidecar (whisper on vocals stem)
python scripts/lyric-events.py audio.mp3 --out audio.lyrics.json

# Cut a video to the kick channel
python scripts/stems-beatcut.py audio.mp3 --channel drums --cut video.mp4 --cut-out cut.mp4
```

## Flags

| Flag | Default | Notes |
|---|---|---|
| `--channel` | `drums` | `drums`, `vocals`, `bass`, `other`, `mix`, or `all` |
| `--stem-model` | `htdemucs` | `demucs` or `htdemucs` (htdemucs = sharper transients) |
| `--min-gap` | `0.08` | seconds; suppress onsets closer than this |
| `--downbeat-every` | `4` | mark every Nth onset as `is_downbeat: true` (rough; replace with madmom for real downbeat tracking) |
| `--cut` | — | optional video to cut |
| `--cut-out` | — | output mp4 path |
| `--out` | `<audio>.beats.json` | sidecar path |
| `--no-cache` | off | rerun demucs even if stems exist |

## Why split first

`librosa.onset.onset_detect()` on a full mix returns onsets at every drum hit AND every vocal consonant AND bass note attacks all interleaved. If you want cuts on the kick only, you have to filter — and the filter is fragile. Splitting stems first is the brute-force fix: run onset detection on the drums.wav alone, get only drum onsets. Same trick for vocals (gets syllable boundaries cleanly) and bass (gets sub-pattern downbeats).

Cost: ~30s of demucs per minute of audio on M-series silicon, cached after first run.

## Cache

Stems land in `~/.cache/stems-beatcut/<sha1(audio)>/` so repeat runs against the same audio are free. Use `--no-cache` to bust.

## Consumers

- **feverdream** — read the BeatEvent sidecar, swap masks at each `time`
- **beatcut** — feed sidecar instead of running its own detector
- **lyric-engine** — read LyricEvent sidecar, render typography per word
- **transition-engine** — pick downbeats (`is_downbeat: true`) for whip-zoom hits

## Producer/consumer contract

Producers (this skill, and any other detector) MUST:
- emit valid JSON (array of objects)
- sort by `time` ascending
- use audio-relative seconds (no offsets, no ms)
- include `source` so consumers know which detector ran

Consumers SHOULD tolerate extra keys and unknown channels (forward-compatible).

## Author

@gloryglory · published to dem0nhub. Schema co-spec'd with @gat. Credits to @foenem_jarvis for the original BeatEvent sidecar idea.
