nwe files

some files
This commit is contained in:
Thomas
2026-01-29 10:51:19 +01:00
parent abf2109171
commit 2b81d5843a
7 changed files with 485 additions and 0 deletions

117
DESIGN_RATIONALE.md Normal file
View File

@@ -0,0 +1,117 @@
# Design Rationale
This document explains *why* Auto Clip makes certain musical and engineering choices.
---
## Why “bars” instead of arbitrary seconds?
Electronic music (especially trance) is structured in **bars** and **phrases**:
- Most tracks are in **4/4**
- Changes happen on predictable boundaries (every 4/8/16/32 bars)
Cutting on bar boundaries reduces:
- awkward mid-kick edits
- off-grid transitions
- “why does this feel wrong?” moments
Thats why the CLI exposes:
- `--bars` (2 bars for rollcall, 4 bars for mini-mix feel)
- `--preroll-bars` (start a bar earlier so the listener hears the groove before the highlight)
---
## Why “pre-roll bars”?
Highlights often occur at an impact moment:
- a stab
- a fill
- a drop hit
If you cut *exactly* at the highlight, the listener misses the *lead-in groove*.
Pre-roll gives the ear context, so the transition feels like a DJ brought it in.
Practical defaults:
- Rollcall: `--bars 2 --preroll-bars 1`
- Mini-mix: `--bars 4 --preroll-bars 1`
---
## Why energy + onset for highlight detection?
In EDM, “interesting” moments correlate with:
- higher RMS energy (loudness/drive)
- strong transient activity (onset strength)
A simple weighted sum (with robust normalization) is:
- fast
- local-only
- works reasonably across many tracks
Its not perfect (pads/breakdowns can confuse it), but its a strong baseline.
---
## Why Camelot (harmonic mixing)?
DJ transitions feel smoother when keys are compatible.
The Camelot wheel provides a practical rule-of-thumb:
- Same number A<->B (relative major/minor)
- Same letter, number +/-1 (adjacent harmonies)
Auto Clip uses **best-effort** key detection and then maps to Camelot to:
- reduce harmonic clashes
- keep the teaser musically “coherent”
Caveats:
- Key detection can be unreliable on pad-heavy sections, noise, or breakdowns
- Thats why V3 calls it best-effort and V4 plans confidence-based fallback
---
## Why “downbeat-ish” snap instead of full ML downbeat detection?
True downbeat detection often needs:
- trained ML models
- more complex pipelines
- sometimes stems / better separation
Auto Clip stays local and lightweight.
So we approximate downbeat by:
- beat tracking grid
- onset accent scoring at bar starts (kick/transient emphasis)
This typically yields:
- better bar-aligned cuts than “nearest beat”
- without heavy dependencies
---
## Why 2-pass loudnorm?
When you cut from different tracks:
- perceived loudness can jump wildly
- the teaser feels amateur even if the edits are good
FFmpegs loudnorm supports 2-pass measurement + apply, which:
- improves consistency
- reduces clipping risk
- keeps the teaser “radio ready” (for a promo)
Thats why V3 uses 2-pass loudnorm per clip.
---
## Why this repo has V_1 / V_2 / V_3?
Keeping versions side-by-side has benefits:
- V_1: minimal baseline
- V_2: practical CLI + selection features
- V_3: trance/DJ quality logic
It also makes it easy for contributors to:
- understand evolution
- debug regressions
V4 aims to unify this into a single stable CLI while retaining clarity.

66
TEASER_COMPARISON.md Normal file
View File

@@ -0,0 +1,66 @@
# Teaser Comparison (Before / After)
This doc is a template you can fill in after generating teasers with different versions/settings.
The goal: make it obvious *what improved* and *why it sounds more DJ-like*.
---
## How to generate comparable teasers
Use the **same input folder** and keep teaser length constant.
### V2 (baseline)
```bash
cd V_2
python dj_teaser_v2.py --tracks-dir ./tracks --select all --teaser 60 --bars 2 --preroll-bars 0
# output: out/album_teaser.mp3
```
### V3 (DJ-focused)
```bash
cd V_3
python dj_teaser_v3.py --tracks-dir ./tracks --select all --teaser 60 --bars 2 --preroll-bars 1 --avoid-intro 30 --harmonic
# output: out/album_teaser.mp3
```
Optional: also compare `--bars 4` for a mini-mix feel.
---
## What to listen for (checklist)
### A) Grid / phrasing
- [ ] Cuts land on bar boundaries
- [ ] Kick is not chopped mid-hit
- [ ] Transitions feel “countable” (1-2-3-4)
### B) Loudness consistency
- [ ] No harsh jumps between clips
- [ ] No sudden distortion/clipping
### C) Harmonic smoothness
- [ ] Fewer “key clashes”
- [ ] Transitions feel less dissonant
### D) Teaser pacing
- [ ] Energy builds naturally
- [ ] No long dead intros
---
## Fill-in results table
| Test | Output file | Notes (what you hear) |
|------|-------------|------------------------|
| V2 bars=2 preroll=0 | `...` | ... |
| V3 bars=2 preroll=1 harmonic | `...` | ... |
| V3 bars=4 preroll=1 harmonic | `...` | ... |
---
## Suggested artifacts to include in the repo (optional)
- A short 15s sample MP3 from V2 and V3 (if you can legally share it)
- A screenshot/waveform image showing smoother transitions
- The `teaser_report.json` for each comparison run

25
TESTING.md Normal file
View File

@@ -0,0 +1,25 @@
# Testing
Auto Clip is audio-heavy, so tests are split into:
- **Sanity / unit tests** (fast, no audio required)
- Optional **integration tests** (needs real audio files; not included by default)
## Install dev deps
```bash
pip install -U pytest
```
## Run tests
From repo root:
```bash
pytest -q
```
## What the sanity tests validate
- ffmpeg is available in PATH (or the test is skipped with a clear message)
- CLI argument parsing doesn't crash
- selection parsing is correct (ranges, lists)
- report JSON schema expectations (keys exist)
- basic utilities remain importable
These tests are designed to catch “oops I broke the CLI” regressions quickly.

94
V4_ROADMAP.md Normal file
View File

@@ -0,0 +1,94 @@
# V4 Roadmap
V4 is about pushing the output closer to a **real DJ mini-mix** while keeping everything **local-first** and reproducible.
> Guiding principles:
> - Local-only (no cloud, no uploads)
> - Deterministic defaults (repeatable outputs)
> - “DJ control knobs” > opaque magic
> - Trance-friendly phrasing (bars/phrases matter)
---
## 1) Musical accuracy upgrades (core)
### A. Better downbeat / phrase alignment
- Add a stronger “1-of-bar” detector:
- Combine beat tracking + onset accents + low-frequency (kick) emphasis
- Prefer downbeats where kick energy spikes
- Add `--phrase-bars` (e.g. 8/16) to cut on larger musical phrases
- Add `--snap-strength` (soft vs hard snap)
### B. Key detection improvements
- Add `--key-window` to estimate key in the *selected region* (not whole track)
- Add confidence scoring + fallback:
- If confidence is low, disable harmonic ordering for that track
- Add “ignore key for breakdowns” option (`--key-harmonic-only`)
### C. Transition logic upgrades
- Add “mix-in/mix-out windows”:
- prefer stable groove segments for transitions
- Add optional “filter sweep style” transitions (FFmpeg filters) as an effect:
- `--fx filter_sweep` (subtle, optional)
---
## 2) DJ-friendly automation
### A. Auto tempo normalization (optional)
- Only for teasers (not for mastering)
- Add `--tempo-normalize`:
- time-stretch small BPM deltas (e.g. +/-2 BPM) for smoother blends
### B. Camelot-aware graph ordering
- Replace greedy chaining with a small graph search:
- minimize “harmonic distance” + tempo distance + maximize energy ramp
- Add `--order objective`:
- `harmonic`
- `tempo`
- `energy`
- `balanced` (default)
### C. Multi-style teaser modes
- `rollcall` (fast flips)
- `mini_mix` (longer phrases)
- `peak_build` (energy ramp to “drop” at the end)
- `social_15` (15s vertical-friendly teaser pacing)
---
## 3) Engineering / Repo upgrades
### A. Single stable CLI (autoclip)
- Publish one entrypoint:
- `autoclip build ...`
- `autoclip analyze ...`
- `autoclip report ...`
### B. Packaging
- Add `pyproject.toml` packaging metadata
- Add `pipx install .` support (optional)
- Add pinned optional dependency groups:
- `pip install .[dev]` -> pytest, ruff, black
### C. Better testing
- Add fast unit tests (no audio)
- Add optional integration test that uses a tiny bundled WAV sample (if license allows)
---
## 4) Nice-to-have (stretch)
- Spectral “drop detector” for trance peak moments
- Auto-generated tracklist timestamps + social captions via Ollama (optional)
- Export cue sheet (CUE) for the teaser
- Optional waveform PNG generation for README (matplotlib)
---
## Suggested V4 milestone plan
- **V4.0.0**: improved phrase/downbeat snap + balanced ordering + single CLI
- **V4.1.0**: key confidence + graph ordering
- **V4.2.0**: tempo normalize (optional) + effect transitions (optional)
- **V4.3.0**: stronger testing + packaging polish

98
autoclip.py Normal file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
Auto Clip - single CLI wrapper
Purpose:
- Provide one stable entrypoint for the repo
- Route commands to V_1 / V_2 / V_3 scripts
- Keep it simple and Git-friendly (no packaging required)
Usage:
python autoclip.py v3 --tracks-dir ./tracks --select all --teaser 60 --bars 2 --harmonic
python autoclip.py v2 --tracks-dir ./tracks --select auto --auto-n 8 --teaser 75 --bars 4
python autoclip.py list
Notes:
- This wrapper assumes your repo structure:
V_1/...
V_2/dj_teaser_v2.py
V_3/dj_teaser_v3.py
- It forwards all remaining arguments to the selected version script.
"""
import argparse
import subprocess
import sys
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parent
def existing_script(version: str) -> Path:
root = repo_root()
v = version.lower()
# Adjust these filenames if your scripts differ.
candidates = {
"v1": [root / "V_1" / "dj_teaser_v1.py", root / "V_1" / "dj_teaser.py"],
"v2": [root / "V_2" / "dj_teaser_v2.py", root / "V_2" / "dj_teaser.py"],
"v3": [root / "V_3" / "dj_teaser_v3.py", root / "V_3" / "dj_teaser.py"],
}
if v not in candidates:
raise SystemExit(f"Unknown version '{version}'. Use: v1 | v2 | v3")
for p in candidates[v]:
if p.exists():
return p
raise SystemExit(
f"Could not find a script for {version}.\n"
f"Looked for:\n- " + "\n- ".join(str(p) for p in candidates[v])
)
def list_versions():
root = repo_root()
print("Auto Clip versions available:")
for v in ["V_1", "V_2", "V_3"]:
p = root / v
if p.exists():
print(f" - {v}/")
print("\nTip: run `python autoclip.py v3 --help` (forwards to the version script).")
def main():
parser = argparse.ArgumentParser(
prog="autoclip",
description="Auto Clip - single CLI wrapper (routes to V_1 / V_2 / V_3).",
)
parser.add_argument("command", nargs="?", default="help", help="v1 | v2 | v3 | list | help")
parser.add_argument("args", nargs=argparse.REMAINDER, help="Arguments forwarded to the version script")
ns = parser.parse_args()
cmd = ns.command.lower()
if cmd in {"help", "-h", "--help"}:
parser.print_help()
print("\nExamples:")
print(" python autoclip.py list")
print(" python autoclip.py v3 --tracks-dir ./tracks --select all --teaser 60 --bars 2 --harmonic")
return
if cmd == "list":
list_versions()
return
if cmd in {"v1", "v2", "v3"}:
script = existing_script(cmd)
full = [sys.executable, str(script)] + ns.args
raise SystemExit(subprocess.call(full))
raise SystemExit(f"Unknown command '{ns.command}'. Use: v1 | v2 | v3 | list | help")
if __name__ == "__main__":
main()

7
testing/pyproject.toml Normal file
View File

@@ -0,0 +1,7 @@
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"
[tool.ruff]
line-length = 100
target-version = "py310"

78
testing/test_sanity.py Normal file
View File

@@ -0,0 +1,78 @@
import json
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
def test_ffmpeg_exists_or_skip():
if shutil.which("ffmpeg") is None:
pytest.skip("ffmpeg not found in PATH (required for rendering).")
p = subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
assert p.returncode == 0
def test_selection_parsing_logic():
# Minimal expectation for selection parsing behavior
def parse(selection: str, n: int):
s = selection.strip().lower()
if s in {"all", "auto"}:
return list(range(n))
out = []
for part in [p.strip() for p in s.split(",") if p.strip()]:
if "-" in part:
a, b = part.split("-", 1)
a_i = int(a) - 1
b_i = int(b) - 1
if a_i > b_i:
a_i, b_i = b_i, a_i
out.extend(list(range(a_i, b_i + 1)))
else:
out.append(int(part) - 1)
# unique + clamp
seen = set()
filtered = []
for i in out:
if 0 <= i < n and i not in seen:
seen.add(i)
filtered.append(i)
return filtered
assert parse("all", 5) == [0, 1, 2, 3, 4]
assert parse("auto", 3) == [0, 1, 2]
assert parse("1,3,5", 5) == [0, 2, 4]
assert parse("1-3", 10) == [0, 1, 2]
assert parse("3-1", 10) == [0, 1, 2]
assert parse("1-2,2-3", 10) == [0, 1, 2]
def test_example_report_schema_if_present():
# If the example JSON exists in repo root, validate basic schema.
root = Path(".").resolve()
ex = root / "example_teaser_report.json"
if not ex.exists():
pytest.skip("example_teaser_report.json not present in this environment.")
data = json.loads(ex.read_text(encoding="utf-8"))
assert "version" in data
assert "settings" in data
assert "tracks" in data
assert isinstance(data["tracks"], list)
if data["tracks"]:
t0 = data["tracks"][0]
assert "filename" in t0
if data.get("version") == "v3":
assert "camelot" in t0
def test_cli_wrapper_help_runs():
# Ensure autoclip.py exists and prints help without crashing.
root = Path(".").resolve()
cli = root / "autoclip.py"
if not cli.exists():
pytest.skip("autoclip.py not present in this environment.")
p = subprocess.run([sys.executable, str(cli), "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
assert p.returncode == 0
assert "Auto Clip" in (p.stdout + p.stderr)