diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 71286b2..4bb31c3 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -34,6 +34,7 @@ Focused subpackages, each with a clear dependency footprint: | Module | Purpose | Heavy deps | |---|---|---| | `mixing.audio` | `Audio`/`AudioSamples`, fades, crop, concat, overlay, `loop_audio`, `duck_audio`, `find_audio_offset`, segmentation | `pydub`, `numpy`, `scipy` | +| `mixing.audio.beats` | `beat_grid`/`BeatGrid` — beat/downbeat/onset analysis | `librosa` (extra `beats`, lazy-imported) | | `mixing.video` | `Video`/`VideoFrames`, crop/loop/speed, `replace_audio`, `normalize_audio`, `overlay_ambient_bed`, `ken_burns_*`, `concatenate_videos`, thumbnails, subtitles | `moviepy`, `opencv`, `pillow`, `looks` (vocabulary only — stdlib) | | `mixing.video.genai` | Google Vertex AI **Veo** generation | `google-genai` (extra `gen`) | | `mixing.transcript` | ElevenLabs **Scribe** STT (stdlib HTTP, cached), filler removal, SRT/prose | stdlib only | @@ -124,7 +125,8 @@ Use the helpers, don't reinvent them: `pip install mixing[]`: `audio` (pydub/ffmpeg-python/soundfile), `widget` (Jupyter audio widget), `gen` (Veo/`google-genai`), `llm` (`aix`, for -chapter titling + SRT translation), `clipboard` (`pyclip`), `dev`/`testing`. +chapter titling + SRT translation), `clipboard` (`pyclip`), `beats` (`librosa`, +for `mixing.audio.beats.beat_grid`), `dev`/`testing`. **ffmpeg must be on PATH** for most real work (`mixing.has_ffmpeg()` checks). ## Tests = guardrails diff --git a/.claude/skills/mixing-audio/SKILL.md b/.claude/skills/mixing-audio/SKILL.md index c5e320c..e0874b0 100644 --- a/.claude/skills/mixing-audio/SKILL.md +++ b/.claude/skills/mixing-audio/SKILL.md @@ -401,6 +401,27 @@ own `(AudioSegment, **kwargs) -> list[Segment]` callable as `strategy`. The `segment_by_silence/energy/self_similarity/speech_music` functions are exported too if you want to call one directly on a pydub `AudioSegment`. +## Beat / downbeat / onset analysis (`mixing[beats]` extra) + +```python +from mixing.audio import beat_grid, BeatGrid + +grid = beat_grid("song.mp3") # -> BeatGrid; installs librosa lazily on first call +grid.beat_times # np.ndarray of beat instants, seconds, ascending +grid.downbeat_times # best-effort; empty array for the default (only) "librosa" backend +grid.onset_env # onset-strength envelope, one value per STFT hop +grid.onset_hop_s # seconds between onset_env frames (hop_length / sample_rate) +grid.tempo_bpm # estimated global tempo +grid.to_dict() # JSON-round-trippable summary (arrays -> lists; onset_env omitted) +``` + +`backend="librosa"` is the only shipped backend (ISC, commercial-clean); a +stronger downbeat tracker can be added later behind the same `BeatGrid` shape. +Needs `pip install mixing[beats]` (`librosa`) — `import mixing.audio` stays +light without it; the import happens lazily inside `beat_grid` itself. First +customer is muvid's footage-scoring layer (computes one grid on the master +song, maps clips onto it via their known offset). + ## Common recipes ```python diff --git a/README.md b/README.md index d42d334..a9b6cd4 100644 --- a/README.md +++ b/README.md @@ -128,13 +128,14 @@ pip install mixing[widget] # interactive Jupyter audio widget pip install mixing[gen] # Google Vertex AI Veo generation pip install mixing[llm] # aix — chapter titling + SRT translation pip install mixing[clipboard] # get file paths from the clipboard +pip install mixing[beats] # librosa — beat/downbeat/onset analysis ``` ## What's inside | Subpackage | Highlights | |---|---| -| `mixing.audio` | `Audio`, `fade_in/out`, `crop_audio`, `concatenate_audio`, `overlay_audio`, `loop_audio`, `duck_audio`, `find_audio_offset`, `find_segments`/`extract_segments` | +| `mixing.audio` | `Audio`, `fade_in/out`, `crop_audio`, `concatenate_audio`, `overlay_audio`, `loop_audio`, `duck_audio`, `find_audio_offset`, `find_segments`/`extract_segments`, `beat_grid`/`BeatGrid` (extra `beats`) | | `mixing.video` | `Video`, `crop_video`, `loop_video`, `change_speed`, `replace_audio`, `normalize_audio`, `overlay_ambient_bed`, `ken_burns_video`/`ken_burns_film`, `concatenate_videos` (+ transitions: `crossfade_transition`, `fade_through_black`, …), `make_thumbnail`, `write_subtitles_in_video`, `SOCIAL_SIZES` | | `mixing.video.genai` | `generate_video` (Vertex AI Veo) | | `mixing.transcript` | `transcribe`, `remove_fillers`, `srt_for_media`, `words_to_srt`/`words_to_prose` | diff --git a/pyproject.toml b/pyproject.toml index 820243b..904e91c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,8 +61,9 @@ audio = [ "ffmpeg-python", "soundfile", ] -# Beat / downbeat / onset analysis (mixing.audio.beat_grid). librosa is ISC -# (commercial-clean) and lazy-imported, so `import mixing.audio` stays light. +# Beat / downbeat / onset analysis (mixing.audio.beats: beat_grid, BeatGrid). +# librosa is ISC (commercial-clean) and lazy-imported, so `import mixing.audio` +# stays light. beats = [ "librosa", ] @@ -146,7 +147,7 @@ install.macos = "brew install ffmpeg" install.windows = "choco install ffmpeg -y --no-progress" [tool.wads.ci] -project_name = "" +project_name = "mixing" [tool.wads.ci.commands] pre_test = []