From 4c5c4276c1a30ccf0ad872f45e6d0d81238aad40 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 2 Jul 2026 14:51:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20core/waveform=20=E2=80=94=20peak=20redu?= =?UTF-8?q?ction=20+=20ffmpeg=20region=20decode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/waveform.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_utils.py | 26 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 core/waveform.py diff --git a/core/waveform.py b/core/waveform.py new file mode 100644 index 0000000..0dd6c55 --- /dev/null +++ b/core/waveform.py @@ -0,0 +1,35 @@ +import subprocess + +import numpy as np + +from core.paths import _bin + + +def peaks(samples, buckets: int = 128) -> list[float]: + """Reduce a 1-D sample array to *buckets* normalized peak magnitudes (0..1).""" + if samples is None or len(samples) == 0: + return [0.0] * buckets + a = np.abs(np.asarray(samples, dtype="float32")) + idx = np.linspace(0, len(a), buckets + 1).astype(int) + out = [float(a[idx[i]:idx[i + 1]].max()) if idx[i + 1] > idx[i] else 0.0 + for i in range(buckets)] + m = max(out) or 1.0 + return [v / m for v in out] + + +def load_region_samples(path: str, start: float, duration: float, + sr: int = 8000) -> np.ndarray: + """Decode a [start, start+duration] mono slice via ffmpeg for waveform + preview (low sample rate). Returns a float32 numpy array, empty on failure.""" + cmd = [ + _bin("ffmpeg"), "-ss", str(start), "-i", path, "-t", str(duration), + "-vn", "-ac", "1", "-ar", str(sr), "-f", "f32le", + "-loglevel", "error", "pipe:1", + ] + try: + proc = subprocess.run(cmd, capture_output=True, timeout=60) + except Exception: + return np.zeros(0, dtype="float32") + if proc.returncode != 0 or not proc.stdout: + return np.zeros(0, dtype="float32") + return np.frombuffer(proc.stdout, dtype="float32") diff --git a/tests/test_utils.py b/tests/test_utils.py index 4c833ce..2529d05 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -540,3 +540,29 @@ def test_audio_extract_ltx2_duration(): duration=frames / fps) assert "-t" in cmd assert cmd[cmd.index("-t") + 1] == str(frames / fps) + + +# --- waveform peak reduction + region decode --- + +def test_peaks_downsamples_to_bucket_count(): + from core.waveform import peaks + import numpy as np + samples = np.sin(np.linspace(0, 100, 10000)).astype("float32") + p = peaks(samples, buckets=64) + assert len(p) == 64 + assert all(0.0 <= v <= 1.0 for v in p) + + +def test_peaks_empty_returns_zeros(): + from core.waveform import peaks + assert peaks(None, buckets=16) == [0.0] * 16 + import numpy as np + assert peaks(np.zeros(0, dtype="float32"), buckets=8) == [0.0] * 8 + + +def test_load_region_samples_bad_path_returns_empty(): + from core.waveform import load_region_samples + import numpy as np + out = load_region_samples("/no/such/file.mp4", 0.0, 1.0) + assert isinstance(out, np.ndarray) + assert out.size == 0