44 lines
1.6 KiB
Python
44 lines
1.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Offline beat detection for a rendered/generated audio track.
|
|
|
|
Usage:
|
|
python beat_detect.py <audio_path>
|
|
|
|
Prints a single JSON object to stdout:
|
|
{"tempo": <bpm float>, "beats": [<seconds float>, ...], "duration": <seconds float>}
|
|
|
|
Local/offline only: librosa + numpy (already pinned for the asset venv). No network, no model
|
|
downloads. On any failure it prints {"tempo": 0, "beats": [], "duration": 0} and exits 0 so the
|
|
caller can fail soft to un-synced cuts rather than failing the render.
|
|
"""
|
|
import json
|
|
import sys
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) < 2:
|
|
print(json.dumps({"tempo": 0, "beats": [], "duration": 0}))
|
|
return 0
|
|
audio_path = sys.argv[1]
|
|
try:
|
|
import librosa
|
|
|
|
# sr=None keeps the file's native rate; mono is enough for beat tracking.
|
|
y, sr = librosa.load(audio_path, sr=None, mono=True)
|
|
duration = float(librosa.get_duration(y=y, sr=sr))
|
|
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr, units="frames")
|
|
beat_times = librosa.frames_to_time(beat_frames, sr=sr)
|
|
beats = [round(float(t), 4) for t in beat_times.tolist()]
|
|
tempo_val = float(tempo[0]) if hasattr(tempo, "__len__") else float(tempo)
|
|
print(json.dumps({"tempo": round(tempo_val, 2), "beats": beats,
|
|
"duration": round(duration, 4)}))
|
|
return 0
|
|
except Exception as exc: # noqa: BLE001 - fail soft, never break the render
|
|
sys.stderr.write("beat_detect failed: %s\n" % exc)
|
|
print(json.dumps({"tempo": 0, "beats": [], "duration": 0}))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|