aboutsummaryrefslogtreecommitdiff
path: root/lib/tests
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-07 06:47:47 -0400
committerhistoria <historiavg@proton.me>2026-09-07 06:47:47 -0400
commit84dd2d068317998f6fb59400c534ef5be6b51b53 (patch)
tree025293e9d9229e02960374771ae522d9de2628ce /lib/tests
parent39b0f2bbed74f6487a41b82501ae3c6799e4b5c4 (diff)
downloadproducer-84dd2d068317998f6fb59400c534ef5be6b51b53.tar.gz
slop rewriteHEADmain
Diffstat (limited to 'lib/tests')
-rw-r--r--lib/tests/conftest.py70
-rw-r--r--lib/tests/test_chunking.py250
-rw-r--r--lib/tests/test_cli.py357
-rw-r--r--lib/tests/test_dsp.py130
-rw-r--r--lib/tests/test_engines.py515
-rw-r--r--lib/tests/test_lazy.py286
-rw-r--r--lib/tests/test_loudness.py30
-rw-r--r--lib/tests/test_meters.py65
-rw-r--r--lib/tests/test_pipeline.py114
-rw-r--r--lib/tests/test_ui.py169
-rw-r--r--lib/tests/test_updates.py221
11 files changed, 0 insertions, 2207 deletions
diff --git a/lib/tests/conftest.py b/lib/tests/conftest.py
deleted file mode 100644
index 885d22b..0000000
--- a/lib/tests/conftest.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-import numpy as np
-import pytest
-
-SRC = Path(__file__).resolve().parents[1] / "src"
-sys.path.insert(0, str(SRC))
-sys.path.insert(0, str(Path(__file__).resolve().parent))
-
-SR = 44100
-
-
-def speechish(
- dur: float,
- sr: int = SR,
- level_dbfs: float = -20.0,
- seed: int = 0,
-) -> np.ndarray:
- rng = np.random.default_rng(seed)
- n = int(sr * dur)
- t = np.arange(n) / sr
- f0 = 110.0 * (1.0 + 0.02 * np.sin(2 * np.pi * 0.9 * t))
- phase = 2 * np.pi * np.cumsum(f0) / sr
- x = np.zeros(n)
- for k in range(1, 9):
- x += (1.0 / k**1.3) * np.sin(k * phase + 0.3 * k)
- syll = 0.5 + 0.5 * np.sin(2 * np.pi * 3.0 * t + float(rng.uniform(0, 6)))
- pauses = (np.sin(2 * np.pi * 0.5 * t) > -0.6).astype(float)
- env = np.clip(syll, 0.02, 1.0) ** 0.6 * np.maximum(pauses, 0.05)
- x = x * env
- x /= np.max(np.abs(x)) + 1e-12
- return (x * (10 ** (level_dbfs / 20.0))).astype(np.float32)
-
-
-def sine(freq: float, dur: float, sr: int = SR, peak_dbfs: float = -20.0) -> np.ndarray:
- t = np.arange(int(sr * dur)) / sr
- return (10 ** (peak_dbfs / 20.0) * np.sin(2 * np.pi * freq * t)).astype(np.float32)
-
-
-def band_db(x: np.ndarray, sr: int, lo: float, hi: float) -> float:
- from scipy import signal
-
- sos = signal.butter(4, [lo, hi], btype="bandpass", fs=sr, output="sos")
- y = signal.sosfilt(sos, x.astype(np.float64))
- r = np.sqrt(np.mean(np.square(y)))
- if r <= 0:
- return -120.0
- return float(20 * np.log10(r))
-
-
-@pytest.fixture
-def sr() -> int:
- return SR
-
-
-@pytest.fixture
-def speech() -> np.ndarray:
- return speechish(6.0, level_dbfs=-20.0)
-
-
-@pytest.fixture
-def noisy_speech(sr, speech) -> np.ndarray:
- rng = np.random.default_rng(7)
- noise = rng.standard_normal(speech.size)
- noise *= (10 ** (-48.0 / 20.0)) / np.sqrt(np.mean(np.square(noise)))
- hum = 0.003 * np.sin(2 * np.pi * 50.0 * np.arange(speech.size) / sr)
- return (speech + noise + hum).astype(np.float32)
diff --git a/lib/tests/test_chunking.py b/lib/tests/test_chunking.py
deleted file mode 100644
index f00c04e..0000000
--- a/lib/tests/test_chunking.py
+++ /dev/null
@@ -1,250 +0,0 @@
-import numpy as np
-import pytest
-
-from producer.engines.base import blend
-from producer.engines.chunking import apply_chunked, plan_chunks, stitch
-
-
-def test_plan_chunks_basic():
- assert plan_chunks(0, 5, 2) == []
- assert plan_chunks(4, 10, 2) == [(0, 4)]
- assert plan_chunks(10, 5, 0) == [(0, 5), (5, 10)]
- assert plan_chunks(10, 5, 2) == [(0, 5), (3, 8), (6, 10)]
- assert plan_chunks(7, 5, 2) == [(0, 5), (3, 7)]
-
-
-def test_plan_chunks_last_span_covers_tail():
- spans = plan_chunks(11, 5, 4)
- assert spans[-1][1] == 11
- assert spans[-1][1] - spans[-1][0] >= 5
-
-
-def test_plan_chunks_rejects_overlap_ge_chunk():
- with pytest.raises(ValueError):
- plan_chunks(100, 5, 5)
- with pytest.raises(ValueError):
- plan_chunks(100, 5, 6)
-
-
-def test_stitch_identity_pieces_reconstruct_signal():
- rng = np.random.default_rng(0)
- x = rng.standard_normal(10_000).astype(np.float32)
- spans = plan_chunks(x.size, 2500, 300)
- y = stitch(spans, x.size, (x[a:b] for a, b in spans))
- assert y.dtype == np.float32
- assert y.shape == x.shape
- np.testing.assert_allclose(y, x, atol=1e-5)
-
-
-def test_stitch_single_span_passthrough():
- x = np.arange(100, dtype=np.float32)
- y = stitch([(0, 100)], 100, [x])
- np.testing.assert_array_equal(y, x)
-
-
-def test_stitch_crossfade_of_complementary_pieces():
- # piece A is silence, piece B is a full-scale ramp; the crossfade zone
- # must be a smooth blend, not a jump.
- spans = [(0, 10), (5, 15)]
- pieces = [np.zeros(10, dtype=np.float32), np.ones(10, dtype=np.float32)]
- y = stitch(spans, 15, pieces)
- mid = y[7] # 50% through the overlap
- assert 0.4 < mid < 0.6
- assert y[0] == 0.0
- assert y[14] == 1.0
-
-
-def test_stitch_pads_short_pieces():
- spans = [(0, 10), (5, 15)]
- pieces = [np.arange(10, dtype=np.float32), np.arange(8, dtype=np.float32)]
- y = stitch(spans, 15, pieces)
- assert y.shape == (15,)
- assert np.all(np.isfinite(y))
-
-
-def test_apply_chunked_identity_matches_whole_file():
- rng = np.random.default_rng(1)
- x = rng.standard_normal(50_000).astype(np.float32)
- sr = 8000
- calls = []
-
- def fn(chunk):
- calls.append(chunk.size)
- return chunk
-
- y = apply_chunked(x, sr, 7.0, 0.05, fn)
- assert calls == [50_000]
- np.testing.assert_array_equal(y, x)
-
- calls.clear()
- y = apply_chunked(x, sr, 0.5, 0.05, fn)
- assert len(calls) >= 10
- assert all(c <= 4000 for c in calls)
- np.testing.assert_allclose(y, x, atol=1e-5)
-
-
-def test_apply_chunked_zero_disables_chunking():
- x = np.zeros(10, dtype=np.float32)
- calls = []
-
- def fn(chunk):
- calls.append(chunk.size)
- return chunk
-
- apply_chunked(x, 8000, 0.0, 0.5, fn)
- assert calls == [10]
-
-
-def test_apply_chunked_context_reconstructs_identity():
- rng = np.random.default_rng(3)
- x = rng.standard_normal(30_000).astype(np.float32)
- y = apply_chunked(x, 8000, 1.5, 0.25, lambda c: c, context_s=0.5)
- np.testing.assert_allclose(y, x, atol=1e-5)
-
-
-def test_apply_chunked_context_feeds_padded_chunks():
- x = np.ones(5_000, dtype=np.float32)
- sizes = []
-
- def fn(chunk):
- sizes.append(chunk.size)
- return chunk
-
- apply_chunked(x, 1000, 2.0, 0.0, fn, context_s=0.5)
- # plan_chunks(5000, 2000, 0) -> (0,2000), (2000,4000), (4000,5000)
- assert sizes == [2500, 3000, 1500]
-
-
-def test_apply_chunked_context_trims_cold_start_artifacts():
- x = np.ones(6_000, dtype=np.float32)
-
- def fn(chunk):
- out = chunk.copy()
- out[0] = 0.0 # cold-start artifact at the start of every model call
- return out
-
- broken = apply_chunked(x, 1000, 2.0, 0.0, fn)
- assert int(np.sum(broken == 0.0)) > 1
-
- y = apply_chunked(x, 1000, 2.0, 0.0, fn, context_s=0.5)
- assert y[0] == 0.0 # only the true file start stays degraded
- assert np.all(y[1:] == 1.0)
-
-
-def test_apply_chunked_context_survives_oom_retry():
- x = np.ones(8_000, dtype=np.float32)
- attempts = []
-
- def fn(chunk):
- attempts.append(chunk.size)
- if chunk.size > 3000:
- raise MemoryError("simulated oom")
- out = chunk.copy()
- out[0] = 0.0
- return out
-
- y = apply_chunked(x, 1000, 6.0, 0.0, fn, min_chunk_s=1.0, context_s=0.5)
- assert max(attempts) > 3000
- assert y[0] == 0.0
- assert np.all(y[1:] == 1.0)
-
-
-def test_apply_chunked_retries_smaller_on_oom():
- x = np.random.default_rng(2).standard_normal(40_000).astype(np.float32)
- sr = 8000
- attempts = []
-
- def fn(chunk):
- attempts.append(chunk.size)
- if chunk.size > 3000:
- raise MemoryError("simulated oom")
- return chunk
-
- y = apply_chunked(x, sr, 5.0, 0.0, fn, min_chunk_s=0.25)
- assert attempts[0] == 40_000
- assert attempts[-1] == 2500
- assert max(a for a in attempts if a <= 3000) == 2500
- np.testing.assert_allclose(y, x, atol=1e-5)
-
-
-def test_apply_chunked_oom_gives_up_at_min_chunk():
- x = np.zeros(40_000, dtype=np.float32)
-
- def fn(chunk):
- raise MemoryError("always")
-
- with pytest.raises(MemoryError):
- apply_chunked(x, 8000, 1.0, 0.0, fn, min_chunk_s=0.5)
-
-
-def test_apply_chunked_progress_callback():
- x = np.random.default_rng(3).standard_normal(20_000).astype(np.float32)
- sr = 8000
- seen: list[tuple[int, int]] = []
- y = apply_chunked(x, sr, 0.5, 0.0, lambda c: c, on_progress=lambda d, t: seen.append((d, t)))
- total = len(plan_chunks(x.size, round(0.5 * sr), 0))
- assert seen[0] == (0, total)
- assert seen[-1] == (total, total)
- assert [d for d, _t in seen[1:]] == list(range(1, total + 1))
- np.testing.assert_allclose(y, x, atol=1e-6)
-
-
-def test_apply_chunked_progress_resets_after_oom_retry():
- x = np.random.default_rng(4).standard_normal(40_000).astype(np.float32)
- sr = 8000
- seen: list[tuple[int, int]] = []
-
- def fn(chunk):
- if chunk.size > 3000:
- raise MemoryError("simulated oom")
- return chunk
-
- y = apply_chunked(
- x, sr, 5.0, 0.0, fn, min_chunk_s=0.25, on_progress=lambda d, t: seen.append((d, t))
- )
- assert seen[0] == (0, 1) # first attempt is one whole-file chunk
- done, total = seen[-1]
- assert done == total and total > 2 # retried into smaller chunks
- np.testing.assert_allclose(y, x, atol=1e-5)
-
-
-def test_apply_chunked_progress_absent_when_disabled():
- x = np.zeros(10_000, dtype=np.float32)
- y = apply_chunked(x, 8000, 0.5, 0.0, lambda c: c, on_progress=None)
- assert y.shape == x.shape
-
-
-def test_apply_chunked_whole_file_mode_falls_back_on_oom():
- x = np.random.default_rng(5).standard_normal(10_000).astype(np.float32)
- sr = 1000
-
- def fn(chunk):
- if chunk.size > 1500:
- raise MemoryError("simulated oom")
- return chunk * 2
-
- y = apply_chunked(x, sr, 0.0, 0.0, fn, min_chunk_s=0.5)
- np.testing.assert_allclose(y, x * 2, atol=1e-6)
-
-
-def test_blend_float32_no_float64_temporaries():
- x = np.full(1000, 0.25, dtype=np.float32)
- y = np.full(1000, 0.75, dtype=np.float32)
- out = blend(x, y, 0.5)
- assert out.dtype == np.float32
- np.testing.assert_allclose(out, 0.5, atol=1e-7)
- assert blend(x, y, 1.0) is y
- np.testing.assert_allclose(blend(x, y, 0.0), x, atol=1e-7)
-
-
-def test_blend_realigns_resample_drift():
- # a 48k -> 16k -> 48k round trip can come back a sample or two long
- # (resample_poly emits ceil(n * up/down) per hop); blend must cope
- x = np.ones(5, dtype=np.float32)
- long_y = np.full(7, 0.5, dtype=np.float32)
- np.testing.assert_allclose(blend(x, long_y, 0.5), np.full(5, 0.75), atol=1e-7)
-
- short_y = np.full(4, 0.5, dtype=np.float32)
- out = blend(x, short_y, 0.5)
- assert out.shape == x.shape
- np.testing.assert_allclose(out, [0.75, 0.75, 0.75, 0.75, 0.5], atol=1e-7)
diff --git a/lib/tests/test_cli.py b/lib/tests/test_cli.py
deleted file mode 100644
index 76952ba..0000000
--- a/lib/tests/test_cli.py
+++ /dev/null
@@ -1,357 +0,0 @@
-import sys
-from types import SimpleNamespace
-
-import numpy as np
-import pytest
-import soundfile as sf
-from conftest import speechish
-
-from producer import io as pio
-from producer.cli import _apply_args, build_parser, process_one
-from producer.config import Options
-
-
-def _mk_wav(tmp_path, name="in.wav", stereo=False):
- x = speechish(3.0, level_dbfs=-30.0)
- if stereo:
- data = np.stack([x, x * 0.5], axis=1)
- sf.write(str(tmp_path / name), data, 44100, subtype="PCM_16")
- else:
- sf.write(str(tmp_path / name), x, 44100, subtype="PCM_16")
- return tmp_path / name
-
-
-def _opts(**kw):
- opts = Options()
- opts.denoise = "off"
- opts.enhance = "off"
- for k, v in kw.items():
- setattr(opts, k, v)
- return opts
-
-
-def test_decode_stereo_mixdown(tmp_path):
- p = _mk_wav(tmp_path, "st.wav", stereo=True)
- x, sr = pio.decode(p)
- assert sr == 44100
- assert x.dtype.name == "float32"
- assert x.ndim == 1
-
-
-def test_encode_wav_bitdepths(tmp_path):
- x = speechish(2.0, level_dbfs=-20.0)
- for depth in (16, 24, 32):
- out = tmp_path / f"o{depth}.wav"
- pio.encode(x, 44100, out, "wav", depth)
- y, sr = pio.decode(out)
- assert sr == 44100
- assert (
- abs(
- float(np.sqrt(np.mean(y.astype(np.float64) ** 2)))
- - float(np.sqrt(np.mean(x.astype(np.float64) ** 2)))
- )
- < 1e-3
- )
-
-
-def test_encode_flac(tmp_path):
- x = speechish(2.0, level_dbfs=-20.0)
- out = tmp_path / "o.flac"
- pio.encode(x, 44100, out, "flac", 24)
- y, sr = pio.decode(out)
- assert sr == 44100
- assert np.corrcoef(x, y)[0, 1] > 0.999
-
-
-def test_mp3_roundtrip(tmp_path):
- import pytest as _pt
-
- if not pio.ffmpeg_available():
- _pt.skip("ffmpeg missing")
- x = speechish(4.0, level_dbfs=-20.0)
- out = tmp_path / "o.mp3"
- pio.encode(x, 44100, out, "mp3", 16)
- y, sr = pio.decode(out)
- assert sr == 44100
- from producer import meters
-
- assert abs(meters.rms_db(x) - meters.rms_db(y)) < 0.7
-
-
-def test_decode_via_ffmpeg_fallback(tmp_path):
- import pytest as _pt
-
- if not pio.ffmpeg_available():
- _pt.skip("ffmpeg missing")
- x = speechish(4.0, level_dbfs=-20.0)
- out = tmp_path / "o.mp3"
- pio.encode(x, 44100, out, "mp3", 16)
- y, sr = pio.decode(out)
- assert sr == 44100
- assert y.size > 0
-
-
-def test_process_one_end_to_end(tmp_path, capsys):
- import json
-
- from producer import meters
-
- inp = _mk_wav(tmp_path, "e2e.wav")
- out = tmp_path / "e2e_processed.wav"
- rc = process_one(inp, _opts(report=True), single=True)
- assert rc == out
- assert out.exists()
- rep_path = out.with_name("e2e_processed.report.json")
- assert rep_path.exists()
- rep = json.loads(rep_path.read_text())
- y, sr = pio.decode(out)
- assert abs(meters.rms_db(y) + 20.0) < 0.6
- assert meters.true_peak_db(y, sr) <= -2.9
- assert rep["after"]["rms_db"] != 0
-
-
-def test_dry_run_listing(tmp_path, capsys):
- from producer.cli import main
-
- inp = _mk_wav(tmp_path, "dry.wav")
- rc = main([str(inp), "--dry-run", "--denoise", "off"])
- assert rc == 0
- out = capsys.readouterr().out
- assert "denoise" in out and "dsp" in out and "levelling" in out
-
-
-def test_arg_parsing_precedence():
- parser = build_parser()
- args = parser.parse_args(
- ["in.wav", "--profile", "podcast", "--warmth", "0.1", "--ceiling", "-2.0"]
- )
- opts = Options()
- _apply_args(opts, args)
- assert opts.profile == "podcast"
- assert opts.loudness_mode() == "lufs"
- assert opts.strengths["warmth"] == 0.1
- assert opts.ceiling() == -2.0
- assert opts.strengths["air"] is None
-
-
-def test_radio_profile_has_tuned_defaults():
- from producer import pipeline
-
- opts = Options(profile="radio")
- assert opts.eff("tape") > 0 and opts.eff("soothe") > 0
- assert opts.loudness_mode() == "lufs"
- stages = pipeline.build_stages(opts)
- assert [st.name for st in stages] == ["denoise", "enhance", "dsp", "levelling"]
- assert opts.denoise_strength is not None
-
-
-def test_tape_and_soothe_flags_override():
- args = build_parser().parse_args(
- ["in.wav", "--profile", "radio", "--tape", "0.4", "--soothe", "0.7"]
- )
- opts = Options()
- _apply_args(opts, args)
- assert opts.profile == "radio"
- assert opts.strengths["tape"] == 0.4
- assert opts.strengths["soothe"] == 0.7
-
-
-def test_default_output_suffix_processed(tmp_path):
- from producer.cli import _resolve_output
-
- inp = tmp_path / "song.wav"
- assert _resolve_output(inp, _opts(), single=True) == tmp_path / "song_processed.wav"
- odir = tmp_path / "out"
- opts = _opts(output=str(odir))
- assert _resolve_output(inp, opts, single=False) == odir / "song_processed.wav"
-
-
-def test_engine_chunk_flags():
- args = build_parser().parse_args(["in.wav", "--engine-chunk", "15", "--engine-overlap", "1.0"])
- opts = Options()
- _apply_args(opts, args)
- assert opts.engine_chunk_s == 15.0
- assert opts.engine_overlap_s == 1.0
-
-
-def test_default_options_whole_file_and_soft_denoise():
- opts = Options()
- assert opts.engine_chunk_s == 0.0
- assert opts.denoise_strength == 0.9
-
-
-def test_engine_chunk_validation():
- with pytest.raises(SystemExit):
- args = build_parser().parse_args(["in.wav", "--engine-chunk", "5", "--engine-overlap", "5"])
- _apply_args(Options(), args)
- with pytest.raises(SystemExit):
- args = build_parser().parse_args(["in.wav", "--engine-chunk", "-1"])
- _apply_args(Options(), args)
-
-
-def test_engine_chunk_config_override(tmp_path):
- from producer import config as cfgmod
-
- cfg = tmp_path / "config.toml"
- cfg.write_text("engine_chunk = 10.0\nengine_overlap = 1.0\n")
- opts = Options()
- cfgmod.apply_config(opts, cfgmod.load_config(cfg))
- assert opts.engine_chunk_s == 10.0
- assert opts.engine_overlap_s == 1.0
-
-
-def test_denoise_pf_flag_and_config_plumbing(tmp_path):
- from producer import config as cfgmod
-
- opts = Options()
- _apply_args(opts, build_parser().parse_args(["in.wav"]))
- assert opts.denoise_pf is False
-
- opts = Options()
- _apply_args(opts, build_parser().parse_args(["in.wav", "--denoise-pf"]))
- assert opts.denoise_pf is True
-
- cfg = tmp_path / "config.toml"
- cfg.write_text('[denoise]\nengine = "dfn3"\nstrength = 0.8\npf = true\n')
- opts = Options()
- cfgmod.apply_config(opts, cfgmod.load_config(cfg))
- assert opts.denoise_strength == 0.8
- assert opts.denoise_pf is True
-
-
-def test_spectral_denoise_choice(tmp_path):
- from producer import config as cfgmod
-
- opts = Options()
- _apply_args(opts, build_parser().parse_args(["in.wav", "--denoise", "spectral"]))
- assert opts.denoise == "spectral"
-
- cfg = tmp_path / "config.toml"
- cfg.write_text('[denoise]\nengine = "spectral"\nstrength = 0.8\n')
- opts = Options()
- cfgmod.apply_config(opts, cfgmod.load_config(cfg))
- assert opts.denoise == "spectral"
- assert opts.denoise_strength == 0.8
-
-
-def test_conflict_auto_renames_when_not_a_tty(tmp_path):
- inp = _mk_wav(tmp_path, "in.wav")
- first = process_one(inp, _opts(), single=True)
- assert first == tmp_path / "in_processed.wav"
- assert process_one(inp, _opts(), single=True) == tmp_path / "in_processed_1.wav"
- assert process_one(inp, _opts(), single=True) == tmp_path / "in_processed_2.wav"
- assert (tmp_path / "in_processed.wav").exists()
-
-
-def test_conflict_prompt_overwrite(tmp_path, monkeypatch):
- inp = _mk_wav(tmp_path, "in.wav")
- out = tmp_path / "in_processed.wav"
- process_one(inp, _opts(), single=True)
- monkeypatch.setattr(sys, "stdin", SimpleNamespace(isatty=lambda: True))
- monkeypatch.setattr("builtins.input", lambda _prompt: "o")
- assert process_one(inp, _opts(), single=True) == out
- assert out.exists()
-
-
-def test_conflict_prompt_rename(tmp_path, monkeypatch):
- inp = _mk_wav(tmp_path, "in.wav")
- out = tmp_path / "in_processed.wav"
- process_one(inp, _opts(), single=True)
- monkeypatch.setattr(sys, "stdin", SimpleNamespace(isatty=lambda: True))
- monkeypatch.setattr("builtins.input", lambda _prompt: "r")
- second = process_one(inp, _opts(), single=True)
- assert second == tmp_path / "in_processed_1.wav"
- assert second.exists()
- assert out.exists()
-
-
-def test_conflict_prompt_invalid_then_overwrite(tmp_path, monkeypatch):
- inp = _mk_wav(tmp_path, "in.wav")
- out = tmp_path / "in_processed.wav"
- process_one(inp, _opts(), single=True)
- monkeypatch.setattr(sys, "stdin", SimpleNamespace(isatty=lambda: True))
- answers = iter(["maybe", "O"])
- monkeypatch.setattr("builtins.input", lambda _prompt: next(answers))
- assert process_one(inp, _opts(), single=True) == out
-
-
-def test_conflict_prompt_cancel(tmp_path, monkeypatch, capsys):
- inp = _mk_wav(tmp_path, "in.wav")
- out = tmp_path / "in_processed.wav"
- process_one(inp, _opts(), single=True)
- before = out.read_bytes()
- monkeypatch.setattr(sys, "stdin", SimpleNamespace(isatty=lambda: True))
- monkeypatch.setattr("builtins.input", lambda _prompt: "c")
- assert process_one(inp, _opts(), single=True) is None
- assert out.read_bytes() == before
- assert "skipped" in capsys.readouterr().out
-
-
-def test_conflict_prompt_eof_cancels(tmp_path, monkeypatch):
- inp = _mk_wav(tmp_path, "in.wav")
- process_one(inp, _opts(), single=True)
- monkeypatch.setattr(sys, "stdin", SimpleNamespace(isatty=lambda: True))
-
- def _eof(_prompt):
- raise EOFError
-
- monkeypatch.setattr("builtins.input", _eof)
- assert process_one(inp, _opts(), single=True) is None
-
-
-def test_process_one_reports_stage_status(tmp_path, capsys):
- inp = _mk_wav(tmp_path, "status.wav")
- out_path = process_one(inp, _opts(), single=True)
- assert out_path is not None
- out = capsys.readouterr().out
- assert f"[producer] {inp} (" in out
- assert "dsp done in" in out
- assert "levelling done in" in out
- assert f"[producer] wrote {out_path}" in out
-
-
-def test_multi_file_run_gets_position_prefixes(tmp_path, capsys):
- from producer.cli import main
-
- a = _mk_wav(tmp_path, "a.wav")
- b = _mk_wav(tmp_path, "b.wav")
- rc = main([str(a), str(b), "--denoise", "off"])
- assert rc == 0
- out = capsys.readouterr().out
- assert "[1/2]" in out and "[2/2]" in out
- assert (tmp_path / "a_processed.wav").exists()
- assert (tmp_path / "b_processed.wav").exists()
-
-
-def test_output_file_rejected_for_multiple_inputs(tmp_path):
- from producer.cli import main
-
- a = _mk_wav(tmp_path, "a.wav")
- b = _mk_wav(tmp_path, "b.wav")
- with pytest.raises(SystemExit):
- main([str(a), str(b), "-o", str(tmp_path / "out.wav"), "--denoise", "off"])
-
-
-def test_output_dir_accepted_for_multiple_inputs(tmp_path):
- from producer.cli import main
-
- a = _mk_wav(tmp_path, "a.wav")
- b = _mk_wav(tmp_path, "b.wav")
- outdir = tmp_path / "masters"
- rc = main([str(a), str(b), "-o", str(outdir), "--denoise", "off"])
- assert rc == 0
- assert (outdir / "a_processed.wav").exists()
- assert (outdir / "b_processed.wav").exists()
-
-
-def test_batch_failure_continues_to_next_file(tmp_path, capsys):
- from producer.cli import main
-
- good = _mk_wav(tmp_path, "good.wav")
- missing = tmp_path / "missing.wav"
- rc = main([str(missing), str(good), "--denoise", "off"])
- assert rc == 1
- captured = capsys.readouterr()
- assert "[2/2]" in captured.out
- assert "[producer] ERROR" in captured.err
- assert (tmp_path / "good_processed.wav").exists()
diff --git a/lib/tests/test_dsp.py b/lib/tests/test_dsp.py
deleted file mode 100644
index b9508ab..0000000
--- a/lib/tests/test_dsp.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import numpy as np
-from conftest import band_db, sine, speechish
-
-from producer import dsp, meters
-
-
-def test_hpf_removes_rumble(sr):
- x = sine(40, 3.0, sr, -20.0) + sine(200, 3.0, sr, -20.0)
- y = dsp.hpf(x, sr, 80.0)
- assert band_db(x, sr, 35, 45) - band_db(y, sr, 35, 45) > 10.0
- assert abs(band_db(x, sr, 190, 210) - band_db(y, sr, 190, 210)) < 0.5
-
-
-def test_peak_eq_mud_cut(sr):
- x = sine(300, 3.0, sr, -20.0) + sine(1000, 3.0, sr, -20.0)
- y = dsp.peak_eq(x, sr, 300.0, -3.0, 1.0)
- d300 = band_db(x, sr, 280, 320) - band_db(y, sr, 280, 320)
- d1k = band_db(x, sr, 950, 1050) - band_db(y, sr, 950, 1050)
- assert 2.4 < d300 < 3.6
- assert abs(d1k) < 0.3
-
-
-def test_shelf(sr):
- x = sine(60, 3.0, sr, -20.0) + sine(3000, 3.0, sr, -20.0)
- y = dsp.shelf(x, sr, 150.0, 1.5, low=True)
- d60 = band_db(y, sr, 50, 70) - band_db(x, sr, 50, 70)
- assert 1.1 < d60 < 1.9
-
-
-def test_compressor_steady_sine(sr):
- x = sine(440, 4.0, sr, peak_dbfs=-10.0)
- y = dsp.compressor(x, sr, -20.0, 3.0, 15.0, 150.0, 6.0)
- in_rms = meters.rms_db(x)
- out_rms = meters.rms_db(y)
- expected_gr = (1.0 - 1.0 / 3.0) * (in_rms - (-20.0))
- assert abs((in_rms - out_rms) - expected_gr) < 0.4
-
-
-def test_compressor_quiet_signal_unaffected(sr):
- x = sine(440, 4.0, sr, peak_dbfs=-45.0)
- y = dsp.compressor(x, sr, -20.0, 3.0, 15.0, 150.0, 6.0)
- assert abs(meters.rms_db(x) - meters.rms_db(y)) < 0.1
-
-
-def test_deesser(sr):
- x = sine(1000, 4.0, sr, -20.0) + sine(6500, 4.0, sr, -30.0)
- y = dsp.deesser(x, sr, 5500.0, 8000.0, 8.0)
- d_sib = band_db(x, sr, 5800, 7200) - band_db(y, sr, 5800, 7200)
- d_mid = band_db(x, sr, 900, 1100) - band_db(y, sr, 900, 1100)
- assert 1.0 < d_sib < 3.0
- assert abs(d_mid) < 0.4
-
-
-def test_deesser_bypass(sr):
- x = sine(1000, 2.0, sr, -20.0)
- y = dsp.deesser(x, sr, 5500.0, 8000.0, 0.0)
- assert np.allclose(x, y)
-
-
-def test_expander_attenuates_pauses(sr):
- speech = speechish(10.0, sr, level_dbfs=-20.0)
- rng = np.random.default_rng(3)
- noise = rng.standard_normal(speech.size).astype(np.float64)
- noise *= (10 ** (-52.0 / 20.0)) / np.sqrt(np.mean(np.square(noise)))
- x = (speech + noise).astype(np.float32)
- y = dsp.expander(x, sr, max_drop_db=2.1)
- frame = int(0.02 * sr)
- nf = x.size // frame
- frms_x = np.sqrt(np.mean(np.square(x[: nf * frame].reshape(nf, frame)), axis=1))
- frms_y = np.sqrt(np.mean(np.square(y[: nf * frame].reshape(nf, frame)), axis=1))
- fdb_x = 20 * np.log10(frms_x + 1e-12)
- loud = fdb_x > np.percentile(fdb_x, 75)
- quiet = fdb_x < np.percentile(fdb_x, 15)
- stable_loud = np.convolve(loud.astype(int), np.ones(5, dtype=int), mode="same") == 5
- d_y = 20 * np.log10(frms_y + 1e-12)
- assert np.mean(fdb_x[stable_loud] - d_y[stable_loud]) < 0.6
- assert np.mean(fdb_x[quiet] - d_y[quiet]) > 1.5
-
-
-def test_limit_hits_ceiling(sr):
- x = speechish(6.0, sr, level_dbfs=-3.0)
- y = dsp.limit(x, sr, -3.0)
- assert meters.true_peak_db(y, sr) <= -3.0 + 0.1
- assert meters.rms_db(x) - meters.rms_db(y) < 1.0
-
-
-def test_limit_below_ceiling_transparent(sr):
- x = speechish(6.0, sr, level_dbfs=-20.0)
- y = dsp.limit(x, sr, -3.0)
- assert np.max(np.abs(x - y)) < 1e-6
-
-
-def test_resample_roundtrip(sr):
- x = speechish(4.0, sr)
- y = dsp.resample(dsp.resample(x, sr, 48000), 48000, sr)
- assert abs(y.size - x.size) <= 2
- assert abs(meters.rms_db(x) - meters.rms_db(y)) < 0.2
-
-
-def test_tape_bypass(sr):
- x = speechish(3.0, sr)
- assert np.array_equal(x, dsp.tape(x, sr, 0.0))
-
-
-def test_tape_adds_even_harmonics(sr):
- x = sine(220, 3.0, sr, -6.0)
- y = dsp.tape(x, sr, 1.0)
- assert np.all(np.isfinite(y))
- d_h2 = band_db(y, sr, 430, 460) - band_db(x, sr, 430, 460)
- d_h3 = band_db(y, sr, 655, 690) - band_db(x, sr, 655, 690)
- assert d_h2 > 1.0
- assert d_h3 > 1.0
- assert abs(meters.rms_db(x) - meters.rms_db(y)) < 2.0
-
-
-def test_soothe_bypass(sr):
- x = speechish(3.0, sr)
- assert np.array_equal(x, dsp.soothe(x, sr, 0.0))
-
-
-def test_soothe_reduces_resonant_bands(sr):
- x = sine(300, 4.0, sr, -6.0) + sine(3500, 4.0, sr, -6.0) + sine(1000, 4.0, sr, -30.0)
- y = dsp.soothe(x, sr, 1.0)
- d_low = band_db(x, sr, 260, 350) - band_db(y, sr, 260, 350)
- d_harsh = band_db(x, sr, 3200, 3800) - band_db(y, sr, 3200, 3800)
- assert 2.0 < d_low <= 3.6
- assert 2.0 < d_harsh <= 5.2
- # frequencies outside the bands stay untouched
- d_mid = band_db(x, sr, 900, 1200) - band_db(y, sr, 900, 1200)
- assert abs(d_mid) < 0.4
diff --git a/lib/tests/test_engines.py b/lib/tests/test_engines.py
deleted file mode 100644
index 9881859..0000000
--- a/lib/tests/test_engines.py
+++ /dev/null
@@ -1,515 +0,0 @@
-import os
-import sys
-import types
-import warnings
-
-import numpy as np
-import pytest
-
-AUDIO_META_MSG = (
- "`torchaudio.backend.common.AudioMetaData` has been moved to "
- "`torchaudio.AudioMetaData`. Please update the import path."
-)
-
-
-class _FakeTensor:
- def __init__(self, arr):
- self.arr = np.asarray(arr)
-
- def unsqueeze(self, axis):
- return _FakeTensor(self.arr[None, ...])
-
- def detach(self):
- return self
-
- def cpu(self):
- return self
-
- def numpy(self):
- return self.arr
-
-
-def _install_torch_stub(monkeypatch):
- fake = types.ModuleType("torch")
- fake.Tensor = _FakeTensor
- fake.from_numpy = lambda a: _FakeTensor(a)
- fake.cuda = types.SimpleNamespace(is_available=lambda: False)
- monkeypatch.setitem(sys.modules, "torch", fake)
-
-
-def _install_df_enhance_stub(monkeypatch, transform, calls, records=None):
- pkg = types.ModuleType("df")
-
- class _State:
- def sr(self):
- return 44100
-
- class _Model:
- def to(self, dev):
- return self
-
- def init_df(*_args, **kwargs):
- if records is not None:
- records["init_df"] = kwargs
- return _Model(), _State(), "fake"
-
- def enhance(_model, _state, audio, **kwargs):
- calls.append(audio.arr.shape[-1])
- if records is not None:
- records["enhance"] = kwargs
- return _FakeTensor(transform(audio.arr.copy()))
-
- pkg.enhance = types.ModuleType("df.enhance")
- pkg.enhance.init_df = init_df
- pkg.enhance.enhance = enhance
- for sub in ("df.io", "df.logger", "df.utils"):
- mod = types.ModuleType(sub)
- setattr(pkg, sub.split(".")[1], mod)
- monkeypatch.setitem(sys.modules, sub, mod)
- monkeypatch.setitem(sys.modules, "df", pkg)
- monkeypatch.setitem(sys.modules, "df.enhance", pkg.enhance)
-
-
-def test_dfn3_chunked_engine_stitches_full_length(monkeypatch, sr, noisy_speech):
- from producer.engines import denoise_dfn
-
- monkeypatch.setattr("producer.lazy.ensure", lambda *_a, **_k: None)
- monkeypatch.setattr("producer.lazy.ensure_torch", lambda: None)
- _install_torch_stub(monkeypatch)
- calls: list[int] = []
- _install_df_enhance_stub(monkeypatch, lambda a: a * 0.5 + 0.001, calls)
-
- x = noisy_speech[: sr * 4]
- y, eng, dev = denoise_dfn.denoise(x, sr, 1.0, "cpu", chunk_s=1.0, overlap_s=0.1)
- assert "dfn" in eng
- assert "cpu" in dev
- assert len(calls) > 2
- assert y.shape == x.shape
- np.testing.assert_allclose(y, x * 0.5 + 0.001, atol=1e-6)
-
-
-def test_dfn3_whole_file_mode_single_call(monkeypatch, sr, noisy_speech):
- from producer.engines import denoise_dfn
-
- monkeypatch.setattr("producer.lazy.ensure", lambda *_a, **_k: None)
- monkeypatch.setattr("producer.lazy.ensure_torch", lambda: None)
- _install_torch_stub(monkeypatch)
- calls: list[int] = []
- _install_df_enhance_stub(monkeypatch, lambda a: a * 0.5, calls)
-
- x = noisy_speech[: sr * 2]
- y, _eng, _dev = denoise_dfn.denoise(x, sr, 1.0, "cpu", chunk_s=0.0)
- assert calls == [x.size]
- np.testing.assert_allclose(y, x * 0.5, atol=1e-7)
-
-
-def test_dfn3_post_filter_opt_in_no_atten_lim(monkeypatch, sr, noisy_speech):
- from producer.engines import denoise_dfn
-
- monkeypatch.setattr("producer.lazy.ensure", lambda *_a, **_k: None)
- monkeypatch.setattr("producer.lazy.ensure_torch", lambda: None)
- _install_torch_stub(monkeypatch)
- calls: list[int] = []
- records: dict = {}
- _install_df_enhance_stub(monkeypatch, lambda a: a * 0.5, calls, records)
-
- x = noisy_speech[: sr * 2]
- denoise_dfn.denoise(x, sr, 1.0, "cpu", chunk_s=0.0)
- assert records["init_df"]["post_filter"] is False
- assert records["enhance"] == {} # stock df_enhance call, no atten-lim override
-
- calls.clear()
- records.clear()
- denoise_dfn.denoise(x, sr, 1.0, "cpu", chunk_s=0.0, post_filter=True)
- assert records["init_df"]["post_filter"] is True
-
-
-def test_dfn3_chunked_feeds_context_padding(monkeypatch, sr, noisy_speech):
- from producer.engines import denoise_dfn
-
- monkeypatch.setattr("producer.lazy.ensure", lambda *_a, **_k: None)
- monkeypatch.setattr("producer.lazy.ensure_torch", lambda: None)
- _install_torch_stub(monkeypatch)
- calls: list[int] = []
- _install_df_enhance_stub(monkeypatch, lambda a: a * 0.5, calls)
-
- x = noisy_speech[: sr * 10]
- y, _eng, _dev = denoise_dfn.denoise(x, sr, 1.0, "cpu", chunk_s=3.0, overlap_s=0.5)
- # chunks are widened with context, then trimmed back to the spans
- assert max(calls) > 3.0 * sr
- assert min(calls) >= 3.0 * sr
- np.testing.assert_allclose(y, x * 0.5, atol=1e-7)
-
-
-_Z_MODEL_REPO = "iic/speech_zipenhancer_ans_multiloss_16k_base"
-
-
-def _install_zipenhancer_stub(monkeypatch, calls):
- fake = types.ModuleType("zipenhancer")
- fake.MODEL_ZIPENHANCER = _Z_MODEL_REPO
-
- def denoise(chunk, sample_rate, model=_Z_MODEL_REPO, normalize=True, strength=1.0, **_kw):
- calls.append(
- {"n": chunk.size, "model": model, "normalize": normalize, "strength": strength}
- )
- scale = 0.1 if len(calls) % 2 == 1 else 1.0
- return (chunk * scale, 0.0, chunk.size / sample_rate)
-
- fake.denoise = denoise
- monkeypatch.setitem(sys.modules, "zipenhancer", fake)
-
-
-def test_zipenhancer_chunked_normalizes_once(monkeypatch, sr):
- from producer.engines import denoise_zip
-
- seq: list[str] = []
- monkeypatch.setattr(
- "producer.lazy.ensure",
- lambda pkgs, **_k: seq.append("ensure:" + ",".join(str(p) for p in pkgs)),
- )
- monkeypatch.setattr("producer.lazy.ensure_torch", lambda: seq.append("torch"))
- monkeypatch.setattr(
- "producer.lazy.ensure_import", lambda mod, **_k: seq.append("import:" + mod)
- )
- monkeypatch.setattr("producer.lazy.ensure_call", lambda fn, **_k: (seq.append("call"), fn())[1])
- calls: list[dict] = []
- _install_zipenhancer_stub(monkeypatch, calls)
-
- n = 4 * 44100
- t = np.arange(n) / sr
- x = (0.5 * np.sin(2 * np.pi * 160.0 * t)).astype(np.float32)
- y, eng, _dev = denoise_zip.denoise(x, sr, 1.0, "cpu", chunk_s=1.0, overlap_s=0.0)
- assert "zipenhancer" in eng
- # torch is pinned before package installs; the undeclared modelscope
- # import ships with the engine; import and call probes run after both
- assert seq == [
- "torch",
- "ensure:zipenhancer==0.3.2,modelscope",
- "import:zipenhancer",
- "call",
- "call",
- "call",
- "call",
- ]
- assert len(calls) == 4
- # the library API takes the full modelscope repo id; the short name
- # would be treated as a repo id and fail with modelscope E3021
- assert all(c["model"] == _Z_MODEL_REPO for c in calls)
- assert all(c["normalize"] is False for c in calls)
- assert y.shape == x.shape
- peak = float(np.max(np.abs(y)))
- assert abs(peak - 10 ** (-3.0 / 20.0)) < 0.01
- even_rms = float(np.sqrt(np.mean(y[: n // 4].astype(np.float64) ** 2)))
- odd_rms = float(np.sqrt(np.mean(y[n // 4 : n // 2].astype(np.float64) ** 2)))
- assert 8.0 < odd_rms / even_rms < 12.0
-
-
-def test_zipenhancer_resample_roundtrip_length_realigned(monkeypatch):
- # 120s @ 48k round-tripped through the 16k engine can come back a sample
- # or two long (resample_poly emits ceil(n * up/down) per hop); the blend
- # used to crash on the mismatch instead of realigning
- from producer.engines import denoise_zip
-
- monkeypatch.setattr("producer.lazy.ensure_torch", lambda: None)
- monkeypatch.setattr("producer.lazy.ensure", lambda *_a, **_k: None)
- calls: list[dict] = []
- _install_zipenhancer_stub(monkeypatch, calls)
-
- sr = 48000
- n = 100001 # 48k -> 16k -> 48k drifts +1 for this length
- x = (0.3 * np.sin(2 * np.pi * 220.0 * np.arange(n) / sr)).astype(np.float32)
- y, _eng, _dev = denoise_zip.denoise(x, sr, 0.5, "cpu")
- assert y.shape == x.shape
-
-
-def test_mossformer_clearvoice_probe(monkeypatch, sr, noisy_speech):
- from producer.engines import enhance_mossformer
-
- seq: list[str] = []
- monkeypatch.setattr("producer.lazy.ensure_torch", lambda: seq.append("torch"))
- monkeypatch.setattr(
- "producer.lazy.ensure",
- lambda pkgs, **_k: seq.append("ensure:" + ",".join(str(p) for p in pkgs)),
- )
- monkeypatch.setattr(
- "producer.lazy.ensure_import", lambda mod, **_k: seq.append("import:" + mod)
- )
-
- fake = types.ModuleType("clearvoice")
-
- class _FakeCV:
- def __init__(self, task=None, model_name=None):
- pass
-
- def __call__(self, chunk):
- return (chunk * 0.5, 48000)
-
- fake.ClearVoice = _FakeCV
- monkeypatch.setitem(sys.modules, "clearvoice", fake)
-
- x = noisy_speech[: sr * 4]
- y, eng, _dev = enhance_mossformer.enhance(x, 48000, 1.0, "cpu", chunk_s=2.0)
- assert "mossformer2" in eng
- assert seq == ["torch", "ensure:clearvoice==0.1.2", "import:clearvoice"]
- assert y.shape == x.shape
- np.testing.assert_allclose(y, x * 0.5, atol=1e-6)
-
-
-def _hissy_speech(sr, dur=12.0, noise_db=-42.0, seed=3):
- from conftest import speechish
-
- x = speechish(dur, sr, level_dbfs=-20.0, seed=seed)
- rng = np.random.default_rng(seed)
- noise = rng.standard_normal(x.size)
- noise *= (10 ** (noise_db / 20.0)) / np.sqrt(np.mean(np.square(noise)))
- return (x + noise).astype(np.float32)
-
-
-def test_spectral_reduces_steady_noise(sr):
- from producer import meters
- from producer.engines import denoise_spectral
-
- x = _hissy_speech(sr)
- y, eng, dev = denoise_spectral.denoise(x, sr, 1.0, "cpu")
- assert "spectral" in eng and dev == "cpu"
- assert y.shape == x.shape
- assert np.all(np.isfinite(y))
- assert meters.noise_floor_db(y, sr) < meters.noise_floor_db(x, sr) - 8.0
- assert np.corrcoef(x.astype(np.float64), y.astype(np.float64))[0, 1] > 0.8
-
-
-def test_spectral_speech_level_flat(sr):
- from producer.engines import denoise_spectral
-
- x = _hissy_speech(sr)
- y, _eng, _dev = denoise_spectral.denoise(x, sr, 1.0, "cpu")
- frame = int(0.03 * sr)
- nf = x.size // frame
-
- def frms_db(z):
- rms = np.sqrt(np.mean(z[: nf * frame].reshape(nf, frame).astype(np.float64) ** 2, axis=1))
- return 20.0 * np.log10(rms + 1e-12)
-
- xdb, ydb = frms_db(x), frms_db(y)
- speech = xdb > np.percentile(xdb, 10) + 12.0
- assert speech.sum() > 20
- # the deterministic engine must not pump the speech level (dfn3's failure mode)
- swing = np.abs(ydb[speech] - xdb[speech])
- assert np.percentile(swing, 95) < 2.0
-
-
-def test_spectral_strength_zero_is_identity(sr):
- from producer.engines import denoise_spectral
-
- x = _hissy_speech(sr)
- y, _eng, _dev = denoise_spectral.denoise(x, sr, 0.0, "cpu")
- np.testing.assert_array_equal(y, x)
-
-
-def test_spectral_sample_aligned(sr):
- from producer.engines import denoise_spectral
-
- x = _hissy_speech(sr)
- y, _eng, _dev = denoise_spectral.denoise(x, sr, 1.0, "cpu")
- lo, hi = int(sr * 2.0), int(sr * 9.0)
- corrs = {k: float(np.corrcoef(x[lo:hi], y[lo + k : hi + k])[0, 1]) for k in range(-3, 4)}
- assert max(corrs, key=corrs.get) == 0
- assert corrs[0] > 0.8
-
-
-def test_spectral_suppression_capped(sr):
- from producer import meters
- from producer.engines import denoise_spectral
-
- noise = (0.008 * np.random.default_rng(5).standard_normal(sr * 6)).astype(np.float32)
- y, _eng, _dev = denoise_spectral.denoise(noise, sr, 1.0, "cpu")
- drop = meters.noise_floor_db(noise, sr) - meters.noise_floor_db(y, sr)
- # bounded: deep enough to matter, never gated to digital silence
- assert 20.0 < drop < 40.0
-
-
-def test_spectral_chunked_matches_whole_file(sr):
- from producer.engines import denoise_spectral
-
- x = _hissy_speech(sr, dur=20.0)
- y1, _e, _d = denoise_spectral.denoise(x, sr, 1.0, "cpu", chunk_s=60.0, overlap_s=0.5)
- y2, _e, _d = denoise_spectral.denoise(x, sr, 1.0, "cpu", chunk_s=8.0, overlap_s=0.5)
- assert y1.shape == y2.shape == x.shape
- corr = np.corrcoef(y1.astype(np.float64), y2.astype(np.float64))[0, 1]
- assert corr > 0.99
-
-
-def test_spectral_profile_global_not_per_chunk(sr):
- """Tail hiss must get the same suppression with or without speech up front.
-
- The old per-chunk percentile leaked speech into the noise estimate and
- under-suppressed exactly where it matters (between sentences).
- """
- from conftest import speechish
-
- from producer import meters
- from producer.engines import denoise_spectral
-
- speech = speechish(8.0, sr, level_dbfs=-20.0, seed=11)
- rng = np.random.default_rng(9)
- noise = rng.standard_normal(sr * 16).astype(np.float32)
- noise *= (10 ** (-40.0 / 20.0)) / np.sqrt(np.mean(np.square(noise)))
- tail_lo, tail_hi = sr * 10, sr * 16
- with_speech = np.concatenate([speech + noise[: speech.size], noise[speech.size :]]).astype(
- np.float32
- )
- hiss_only = noise.copy()
- y1, _e, _d = denoise_spectral.denoise(with_speech, sr, 1.0, "cpu")
- y2, _e, _d = denoise_spectral.denoise(hiss_only, sr, 1.0, "cpu")
- drop1 = meters.noise_floor_db(with_speech[tail_lo:tail_hi], sr) - meters.noise_floor_db(
- y1[tail_lo:tail_hi], sr
- )
- drop2 = meters.noise_floor_db(hiss_only[tail_lo:tail_hi], sr) - meters.noise_floor_db(
- y2[tail_lo:tail_hi], sr
- )
- assert drop1 > 14.0
- assert drop1 > drop2 - 4.0
-
-
-def _stub_df_modules(calls: list[str]) -> tuple[types.ModuleType, list[types.ModuleType]]:
- pkg = types.ModuleType("df")
- mods = []
- for full in ("df.utils", "df.logger", "df.io"):
- mod = types.ModuleType(full)
-
- def probe(name: str):
- def fn(*_args):
- calls.append(name)
- return "deadbeef"
-
- return fn
-
- for fn_name in ("get_git_root", "get_commit_hash", "get_branch_name"):
- setattr(mod, fn_name, probe(f"{full}.{fn_name}"))
- setattr(pkg, full.split(".")[1], mod)
- mods.append(mod)
- return pkg, mods
-
-
-def test_dfn_shim_neutralizes_git_probes(monkeypatch):
- from producer.engines import denoise_dfn
-
- calls: list[str] = []
- pkg, mods = _stub_df_modules(calls)
- for name, mod in zip(("df", "df.utils", "df.logger", "df.io"), (pkg, *mods), strict=True):
- monkeypatch.setitem(sys.modules, name, mod)
- assert pkg.logger.get_commit_hash() == "deadbeef"
- calls.clear()
- denoise_dfn._shim_df_git()
- for mod in mods:
- for fn_name in ("get_git_root", "get_commit_hash", "get_branch_name"):
- assert mod.__dict__[fn_name]() is None
- assert calls == []
-
-
-def test_dfn_shim_silences_torchaudio_warning():
- from producer.engines import denoise_dfn
-
- code = "import warnings\nwarnings.warn(MESSAGE, UserWarning)"
- denoise_dfn._shim_torchaudio_backend()
- with warnings.catch_warnings(record=True) as caught:
- exec(compile(code, "df/io.py", "exec"), {"__name__": "df.io", "MESSAGE": AUDIO_META_MSG})
- assert caught == []
- with warnings.catch_warnings(record=True) as caught:
- exec(
- compile(code, "other/mod.py", "exec"),
- {"__name__": "other.mod", "MESSAGE": AUDIO_META_MSG},
- )
- assert len(caught) == 1
-
-
-def _metrics_floor(x, sr):
- from producer import meters
-
- return meters.noise_floor_db(x, sr)
-
-
-@pytest.mark.slow
-def test_dfn3_reduces_noise(sr, noisy_speech):
- pytest.importorskip("torch")
- pytest.importorskip("df")
- from producer.engines import denoise_dfn
-
- x = noisy_speech[: sr * 4]
- y, eng, _dev = denoise_dfn.denoise(x, sr, 1.0, "cpu")
- assert "dfn" in eng
- assert _metrics_floor(y, sr) < _metrics_floor(x, sr) - 5.0
- assert np.corrcoef(x, y.astype(np.float64))[0, 1] > 0.9
-
-
-@pytest.mark.slow
-def test_dfn3_speech_level_flat(sr, noisy_speech):
- """Guard against dfn3's reported failure mode: volume wobble in sentences."""
- pytest.importorskip("torch")
- pytest.importorskip("df")
- from producer.engines import denoise_dfn
-
- x = noisy_speech[: sr * 8]
- y, _eng, _dev = denoise_dfn.denoise(x, sr, 0.9, "cpu")
- frame = int(0.03 * sr)
- nf = x.size // frame
-
- def frms_db(z):
- rms = np.sqrt(np.mean(z[: nf * frame].reshape(nf, frame).astype(np.float64) ** 2, axis=1))
- return 20.0 * np.log10(rms + 1e-12)
-
- xdb, ydb = frms_db(x), frms_db(y)
- speech = xdb > np.percentile(xdb, 10) + 12.0
- swing = np.abs(ydb[speech] - xdb[speech])
- assert np.percentile(swing, 95) < 2.5
-
-
-@pytest.mark.slow
-def test_dfn3_chunked_matches_whole_file(sr, noisy_speech):
- pytest.importorskip("torch")
- pytest.importorskip("df")
- from producer.engines import denoise_dfn
-
- x = noisy_speech[: sr * 60]
- y_full, _, _ = denoise_dfn.denoise(x, sr, 1.0, "cpu", chunk_s=0.0)
- y_chunk, _, _ = denoise_dfn.denoise(x, sr, 1.0, "cpu", chunk_s=15.0, overlap_s=0.5)
- assert y_full.shape == y_chunk.shape == x.shape
- corr = np.corrcoef(y_full.astype(np.float64), y_chunk.astype(np.float64))[0, 1]
- assert corr > 0.99
- assert float(np.max(np.abs(y_full - y_chunk))) < 0.1
-
-
-@pytest.mark.slow
-def test_zipenhancer_reduces_noise(sr, noisy_speech):
- pytest.importorskip("torch")
- pytest.importorskip("zipenhancer")
- from producer.engines import denoise_zip
-
- x = noisy_speech[: sr * 4]
- y, eng, _ = denoise_zip.denoise(x, sr, 1.0, "cpu")
- assert "zipenhancer" in eng
- assert _metrics_floor(y, sr) < _metrics_floor(x, sr) - 5.0
-
-
-@pytest.mark.slow
-def test_mossformer2_enhances(sr, noisy_speech):
- pytest.importorskip("torch")
- pytest.importorskip("clearvoice")
- from producer.engines import enhance_mossformer
-
- x = noisy_speech[: sr * 4]
- y, eng, _ = enhance_mossformer.enhance(x, sr, 1.0, "cpu")
- assert "mossformer2" in eng
- assert np.all(np.isfinite(y))
-
-
-@pytest.mark.slow
-def test_resemble_enhance(sr, noisy_speech):
- if not os.environ.get("PRODUCER_TEST_RESEMBLE"):
- pytest.skip("set PRODUCER_TEST_RESEMBLE=1 to run the isolated-venv generative engine")
- from producer.engines import enhance_resemble
-
- x = noisy_speech[: sr * 4]
- y, eng, _ = enhance_resemble.enhance(x, sr, 1.0, "cpu")
- assert "resemble" in eng
- assert np.all(np.isfinite(y))
diff --git a/lib/tests/test_lazy.py b/lib/tests/test_lazy.py
deleted file mode 100644
index 5ab5b95..0000000
--- a/lib/tests/test_lazy.py
+++ /dev/null
@@ -1,286 +0,0 @@
-import types
-
-import pytest
-
-from producer import lazy
-
-PYPI_JSON = {
- "urls": [
- {
- "filename": "torch-2.7.1-cp311-cp311-manylinux_2_28_x86_64.whl",
- "url": "https://files.pythonhosted.org/packages/xx/torch-2.7.1-cp311-cp311-manylinux_2_28_x86_64.whl",
- "digests": {"sha256": "abc123"},
- "size": 766_668_798,
- },
- {
- "filename": "torch-2.7.1-cp311-cp311-win_amd64.whl",
- "url": "https://example.com/win.whl",
- "digests": {},
- "size": 1,
- },
- {
- "filename": "torch-2.7.1.tar.gz",
- "url": "https://example.com/src.tar.gz",
- "digests": {},
- "size": 1,
- },
- ]
-}
-
-_SHA = "e1a846516570851234567890abcdef1234567890abcdef1234567890abcdef12"
-GPU_HTML = (
- "<html><body>"
- '<a href="https://download-r2.pytorch.org/whl/cu126/torch-2.6.0%2Bcu126-cp311-cp311-manylinux_2_28_x86_64.whl#sha256=00'
- f'00">old</a><a href="https://download-r2.pytorch.org/whl/cu126/torch-2.7.1%2Bcu126-cp311-cp311-manylinux_2_28_x86_64.whl#sha256={_SHA}">new</a>'
- '<a href="https://download-r2.pytorch.org/whl/cu126/torch-2.7.1%2Bcu126-cp311-cp311-win_amd64.whl#sha256=1111">win</a>'
- '<a href="torch-2.7.1%2Bcu126-cp311-cp311-manylinux_2_28_x86_64.whl">relative-dup</a>'
- "</body></html>"
-)
-
-
-def test_choose_pypi_picks_linux_wheel():
- got = lazy._choose_pypi(PYPI_JSON)
- assert got is not None
- name, url, sha, size = got
- assert name.endswith("manylinux_2_28_x86_64.whl")
- assert "pythonhosted" in url
- assert sha == "abc123"
- assert size == 766_668_798
-
-
-def test_choose_gpu_matches_version_and_arch():
- got = lazy._choose_gpu(GPU_HTML, "torch", "2.7.1")
- assert got is not None
- name, url, sha, size = got
- assert name == "torch-2.7.1+cu126-cp311-cp311-manylinux_2_28_x86_64.whl"
- assert url == (
- "https://download-r2.pytorch.org/whl/cu126/"
- "torch-2.7.1%2Bcu126-cp311-cp311-manylinux_2_28_x86_64.whl"
- )
- assert sha == _SHA
- assert size is None
-
-
-def test_choose_gpu_no_match_returns_none():
- assert lazy._choose_gpu("<html></html>", "torch", "2.7.1") is None
-
-
-def test_resolve_torch_wheels_needs_both_packages(monkeypatch):
- def fake(pkg, ver):
- return None if pkg == "torchaudio" else ("t.whl", "u", None, 1)
-
- monkeypatch.setattr(lazy, "_pypi_wheel", fake)
- assert lazy._resolve_torch_wheels(gpu=False) == []
-
-
-def test_resolve_torch_wheels_returns_both(monkeypatch):
- wheels = [("torch.whl", "u1", "s1", 1), ("torchaudio.whl", "u2", None, None)]
- monkeypatch.setattr(lazy, "_pypi_wheel", lambda pkg, ver: wheels.pop(0))
- got = lazy._resolve_torch_wheels(gpu=False)
- assert [w[0] for w in got] == ["torch.whl", "torchaudio.whl"]
-
-
-def test_ensure_torch_downloads_wheels_then_installs(monkeypatch, tmp_path):
- wheels = [
- ("torch-2.7.1-cp311.whl", "https://x/torch.whl", "sha", 10),
- ("torchaudio-2.7.1-cp311.whl", "https://x/ta.whl", None, 5),
- ]
- monkeypatch.setattr(lazy, "has_module", lambda name: False)
- monkeypatch.setattr(lazy, "_resolve_torch_wheels", lambda gpu: wheels)
- monkeypatch.setattr(lazy, "find_uv", lambda: "uv")
- monkeypatch.setattr(lazy, "WHEEL_CACHE", tmp_path / "wheels")
- downloads: list[str] = []
- runs: list[tuple[list[str], str]] = []
-
- def fake_download(url, dest, label=None, expected_size=None, sha256=None, timeout=60.0):
- downloads.append(dest.name)
- return dest
-
- monkeypatch.setattr(lazy.ui, "download", fake_download)
- monkeypatch.setattr(lazy.ui, "run", lambda cmd, label, check=True: runs.append((cmd, label)))
- lazy.ensure_torch()
- assert downloads == ["torch-2.7.1-cp311.whl", "torchaudio-2.7.1-cp311.whl"]
- assert len(runs) == 1
- cmd = runs[0][0]
- assert cmd[0] == "uv" and cmd[1] == "pip" and cmd[2] == "install"
- assert str(tmp_path / "wheels" / "torch-2.7.1-cp311.whl") in cmd
- assert str(tmp_path / "wheels" / "torchaudio-2.7.1-cp311.whl") in cmd
-
-
-def test_ensure_torch_falls_back_to_uv_index(monkeypatch):
- monkeypatch.setattr(lazy, "has_module", lambda name: False)
- monkeypatch.setattr(lazy, "_resolve_torch_wheels", lambda gpu: [])
- monkeypatch.setattr(lazy, "find_uv", lambda: "uv")
- monkeypatch.setattr(lazy, "gpu_present", lambda: True)
- runs: list[list[str]] = []
- monkeypatch.setattr(lazy.ui, "run", lambda cmd, label, check=True: runs.append(cmd))
- lazy.ensure_torch()
- cmd = runs[0]
- assert "torch==2.7.1+cu126" in cmd
- assert "--index-url" in cmd
- assert lazy.TORCH_GPU_INDEX in cmd
-
-
-def test_ensure_torch_falls_back_to_pypi_cpu(monkeypatch):
- monkeypatch.setattr(lazy, "has_module", lambda name: False)
- monkeypatch.setattr(lazy, "_resolve_torch_wheels", lambda gpu: [])
- monkeypatch.setattr(lazy, "find_uv", lambda: "uv")
- monkeypatch.setattr(lazy, "gpu_present", lambda: False)
- runs: list[list[str]] = []
- monkeypatch.setattr(lazy.ui, "run", lambda cmd, label, check=True: runs.append(cmd))
- lazy.ensure_torch()
- cmd = runs[0]
- assert "torch==2.7.1" in cmd
- assert "--index-url" not in cmd
-
-
-def test_ensure_torch_noop_when_installed(monkeypatch):
- monkeypatch.setattr(lazy, "has_module", lambda name: True)
- runs: list = []
- monkeypatch.setattr(lazy.ui, "run", lambda cmd, label, check=True: runs.append(cmd))
- lazy.ensure_torch()
- assert runs == []
-
-
-def test_ensure_import_noop_when_importable(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
- lazy.ensure_import("numpy", purpose="test")
- assert installs == []
-
-
-def test_ensure_import_installs_missing_module(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
- state = {"round": 0}
-
- def fake_import(name):
- if name == "fakeengine":
- if state["round"] == 0:
- state["round"] = 1
- raise ModuleNotFoundError("No module named 'addict'", name="addict")
- return types.ModuleType("fakeengine")
- raise ModuleNotFoundError(f"No module named {name!r}", name=name)
-
- monkeypatch.setattr(lazy.importlib, "import_module", fake_import)
- lazy.ensure_import("fakeengine", purpose="test")
- assert installs == [["addict"]]
-
-
-def test_ensure_import_dotted_missing_reraises_without_install(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
-
- def fake_import(name):
- raise ModuleNotFoundError("No module named 'pkg.sub'", name="pkg.sub")
-
- monkeypatch.setattr(lazy.importlib, "import_module", fake_import)
- with pytest.raises(ModuleNotFoundError):
- lazy.ensure_import("whatever", purpose="test")
- assert installs == []
-
-
-def test_ensure_import_gives_up_after_rounds(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
-
- def fake_import(name):
- raise ModuleNotFoundError("No module named 'ghost'", name="ghost")
-
- monkeypatch.setattr(lazy.importlib, "import_module", fake_import)
- with pytest.raises(lazy.EngineUnavailable, match="ghost"):
- lazy.ensure_import("whatever", purpose="test")
- assert len(installs) == 4
- assert all(pkgs == ["ghost"] for pkgs in installs)
-
-
-def test_ensure_import_maps_pil_to_pillow(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
- state = {"round": 0}
-
- def fake_import(name):
- if name == "fakeengine":
- if state["round"] == 0:
- state["round"] = 1
- raise ModuleNotFoundError("No module named 'PIL'", name="PIL")
- return types.ModuleType("fakeengine")
- raise ModuleNotFoundError(f"No module named {name!r}", name=name)
-
- monkeypatch.setattr(lazy.importlib, "import_module", fake_import)
- lazy.ensure_import("fakeengine", purpose="test")
- assert installs == [["pillow"]]
-
-
-def test_ensure_call_passthrough_without_install(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
- assert lazy.ensure_call(lambda: 42, purpose="test") == 42
- assert installs == []
-
-
-def test_ensure_call_installs_missing_module(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
- state = {"round": 0}
-
- def fake_fn():
- if state["round"] == 0:
- state["round"] = 1
- raise ModuleNotFoundError("No module named 'addict'", name="addict")
- return "ok"
-
- assert lazy.ensure_call(fake_fn, purpose="test") == "ok"
- assert installs == [["addict"]]
-
-
-def test_ensure_call_dotted_missing_reraises_without_install(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
-
- def fake_fn():
- raise ModuleNotFoundError("No module named 'pkg.sub'", name="pkg.sub")
-
- with pytest.raises(ModuleNotFoundError):
- lazy.ensure_call(fake_fn, purpose="test")
- assert installs == []
-
-
-def test_ensure_call_unnamed_missing_reraises_without_install(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
-
- def fake_fn():
- raise ModuleNotFoundError("boom")
-
- with pytest.raises(ModuleNotFoundError):
- lazy.ensure_call(fake_fn, purpose="test")
- assert installs == []
-
-
-def test_ensure_call_maps_pil_to_pillow(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
- state = {"round": 0}
-
- def fake_fn():
- if state["round"] == 0:
- state["round"] = 1
- raise ModuleNotFoundError("No module named 'PIL'", name="PIL")
- return "ok"
-
- assert lazy.ensure_call(fake_fn, purpose="test") == "ok"
- assert installs == [["pillow"]]
-
-
-def test_ensure_call_gives_up_after_rounds(monkeypatch):
- installs: list[list[str]] = []
- monkeypatch.setattr(lazy, "ensure", lambda pkgs, **_k: installs.append(list(pkgs)))
-
- def fake_fn():
- raise ModuleNotFoundError("No module named 'ghost'", name="ghost")
-
- with pytest.raises(lazy.EngineUnavailable, match="ghost"):
- lazy.ensure_call(fake_fn, purpose="test")
- assert len(installs) == 8
- assert all(pkgs == ["ghost"] for pkgs in installs)
diff --git a/lib/tests/test_loudness.py b/lib/tests/test_loudness.py
deleted file mode 100644
index 5cd84d6..0000000
--- a/lib/tests/test_loudness.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import numpy as np
-from conftest import speechish
-
-from producer import loudness, meters
-
-
-def test_rms_normalize_hits_target(sr):
- x = speechish(8.0, sr, level_dbfs=-40.0)
- y = loudness.normalize(x, sr, "rms", -20.0, -3.0)
- assert abs(meters.rms_db(y) + 20.0) < 0.5
- assert meters.true_peak_db(y, sr) <= -2.9
-
-
-def test_lufs_normalize_hits_target(sr):
- x = speechish(8.0, sr, level_dbfs=-35.0)
- y = loudness.normalize(x, sr, "lufs", -16.0, -1.5)
- assert abs(meters.lufs(y, sr) + 16.0) < 0.6
- assert meters.true_peak_db(y, sr) <= -1.4
-
-
-def test_silence_passthrough():
- y = loudness.normalize(np.zeros(44100 * 2, dtype=np.float32), 44100, "rms", -20.0, -3.0)
- assert np.allclose(y, 0.0)
-
-
-def test_idempotent(sr):
- x = speechish(8.0, sr, level_dbfs=-35.0)
- y1 = loudness.normalize(x, sr, "rms", -20.0, -3.0)
- y2 = loudness.normalize(y1, sr, "rms", -20.0, -3.0)
- assert abs(meters.rms_db(y1) - meters.rms_db(y2)) < 0.6
diff --git a/lib/tests/test_meters.py b/lib/tests/test_meters.py
deleted file mode 100644
index b013ec9..0000000
--- a/lib/tests/test_meters.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import numpy as np
-from conftest import sine, speechish
-
-from producer import meters
-
-
-def test_rms_and_peak_of_sine(sr):
- x = sine(1000, 3.0, sr, peak_dbfs=-17.0)
- assert abs(meters.rms_db(x) + 20.0) < 0.1
- assert abs(meters.sample_peak_db(x) + 17.0) < 0.05
-
-
-def test_true_peak_bounds(sr):
- x = sine(1000, 3.0, sr, peak_dbfs=-17.0)
- tp = meters.true_peak_db(x, sr)
- sp = meters.sample_peak_db(x)
- assert sp - 0.01 <= tp <= sp + 0.6
-
-
-def test_true_peak_blockwise_matches_whole_file(sr):
- from scipy import signal
-
- rng = np.random.default_rng(3)
- n = sr * 75
- x = (
- 0.5 * np.sin(2 * np.pi * 997.0 * np.arange(n) / sr) + 0.02 * rng.standard_normal(n)
- ).astype(np.float32)
- half = 10 * 4
- h = signal.firwin(2 * half + 1, 0.25, window=("kaiser", 8.0)).astype(np.float32)
- ref = meters.sample_peak_db(signal.resample_poly(x, 4, 1, window=h))
- assert abs(meters.true_peak_db(x, sr) - ref) < 0.01
-
-
-def test_lufs_of_sine(sr):
- x = sine(1000, 3.0, sr, peak_dbfs=-17.0)
- assert abs(meters.lufs(x, sr) + 20.05) < 0.2
-
-
-def test_lufs_silence():
- assert meters.lufs(np.zeros(48000, dtype=np.float32), 48000) == meters.SILENCE_DB
-
-
-def test_noise_floor_below_speech(sr):
- x = speechish(8.0, sr, level_dbfs=-20.0)
- floor = meters.noise_floor_db(x, sr)
- assert floor < meters.rms_db(x) - 4.0
-
-
-def test_speech_level_db_ignores_leading_silence(sr):
- x = speechish(6.0, sr, level_dbfs=-20.0)
- y = np.concatenate([np.zeros(sr * 4, dtype=np.float32), x])
- assert abs(meters.speech_level_db(x, sr) - meters.speech_level_db(y, sr)) < 1.0
- assert meters.speech_level_db(x, sr) > meters.rms_db(x)
-
-
-def test_all_meters_keys(sr):
- d = meters.all_meters(speechish(3.0, sr), sr)
- assert set(d) == {
- "rms_db",
- "sample_peak_db",
- "true_peak_db",
- "lufs",
- "noise_floor_db",
- "duration_s",
- }
diff --git a/lib/tests/test_pipeline.py b/lib/tests/test_pipeline.py
deleted file mode 100644
index b3f91f1..0000000
--- a/lib/tests/test_pipeline.py
+++ /dev/null
@@ -1,114 +0,0 @@
-import numpy as np
-from conftest import band_db, sine, speechish
-
-from producer import meters, pipeline
-from producer.config import Options
-
-
-def test_stage_order_and_names():
- opts = Options()
- opts.denoise = "off"
- opts.enhance = "off"
- res = pipeline.run_pipeline(np.zeros(44100, dtype=np.float32), 44100, opts)
- names = [s.name for s in res.stages]
- assert names == ["denoise", "enhance", "dsp", "levelling"]
- by_name = {s.name: s for s in res.stages}
- assert by_name["denoise"].enabled is False
- assert by_name["enhance"].enabled is False
- assert by_name["dsp"].enabled is True
- assert by_name["levelling"].enabled is True
-
-
-def test_denoise_stage_detail_shows_pf():
- opts = Options()
- stages = pipeline.build_stages(opts)
- assert "pf=off" in stages[0].detail
- assert "leveling" not in stages[0].detail
- opts.denoise_pf = True
- stages = pipeline.build_stages(opts)
- assert "pf=on" in stages[0].detail
-
-
-def test_full_chain_profile_bounds(sr):
- opts = Options()
- opts.denoise = "off"
- opts.enhance = "off"
- x = speechish(8.0, sr, level_dbfs=-35.0)
- res = pipeline.run_pipeline(x, sr, opts)
- assert abs(meters.rms_db(res.audio) + 20.0) < 0.6
- assert meters.true_peak_db(res.audio, sr) <= -2.9
- assert res.timings.get("levelling", 0) >= 0
-
-
-def test_passthrough_when_disabled(sr):
- opts = Options()
- opts.denoise = "off"
- opts.enhance = "off"
- opts.dsp = False
- opts.levelling = False
- x = speechish(4.0, sr, level_dbfs=-20.0)
- res = pipeline.run_pipeline(x, sr, opts)
- assert np.allclose(res.audio, x)
-
-
-def test_knob_zero_disables_eq(sr):
- base = Options()
- base.denoise = "off"
- base.enhance = "off"
- base.levelling = False
- base.strengths["warmth"] = 0.0
- warm = Options()
- warm.denoise = "off"
- warm.enhance = "off"
- warm.levelling = False
- x = sine(60, 4.0, sr, -20.0) + sine(3000, 4.0, sr, -20.0)
- y_flat = pipeline.run_pipeline(x, sr, base).audio
- y_warm = pipeline.run_pipeline(x, sr, warm).audio
- d_flat = band_db(y_flat, sr, 50, 70) - band_db(x, sr, 50, 70)
- d_warm = band_db(y_warm, sr, 50, 70) - band_db(x, sr, 50, 70)
- assert d_warm - d_flat > 0.8
-
-
-def test_podcast_profile_bounds(sr):
- opts = Options()
- opts.profile = "podcast"
- opts.denoise = "off"
- opts.enhance = "off"
- x = speechish(8.0, sr, level_dbfs=-35.0)
- res = pipeline.run_pipeline(x, sr, opts)
- assert abs(meters.lufs(res.audio, sr) + 16.0) < 0.8
- assert meters.true_peak_db(res.audio, sr) <= -1.4
-
-
-def test_dsp_pregain_makes_chain_level_invariant(sr):
- """The voice chain must treat a quiet and a hot take identically.
-
- Denoised files often arrive far below the chain's design level; without
- the pre-gain the absolute comp thresholds would idle (or slam) depending
- on input level alone.
- """
- hot = Options()
- hot.denoise = "off"
- hot.enhance = "off"
- hot.levelling = False
- quiet = Options()
- quiet.denoise = "off"
- quiet.enhance = "off"
- quiet.levelling = False
- x_hot = speechish(6.0, sr, level_dbfs=-14.0)
- x_quiet = speechish(6.0, sr, level_dbfs=-38.0)
- y_hot = pipeline.run_pipeline(x_hot, sr, hot).audio
- y_quiet = pipeline.run_pipeline(x_quiet, sr, quiet).audio
- diff = abs(meters.rms_db(y_hot) - meters.rms_db(y_quiet))
- assert diff < 1.0
-
-
-def test_dsp_pregain_note_recorded(sr):
- opts = Options()
- opts.denoise = "off"
- opts.enhance = "off"
- notes: list[str] = []
- stages = pipeline.build_stages(opts, notes)
- dsp_stage = next(s for s in stages if s.name == "dsp")
- dsp_stage.fn(speechish(4.0, sr, level_dbfs=-30.0), sr)
- assert any("pregain" in n for n in notes)
diff --git a/lib/tests/test_ui.py b/lib/tests/test_ui.py
deleted file mode 100644
index 156b3e8..0000000
--- a/lib/tests/test_ui.py
+++ /dev/null
@@ -1,169 +0,0 @@
-import hashlib
-import subprocess
-import sys
-
-import pytest
-
-from producer import ui
-
-
-class _FakeResp:
- def __init__(self, payload: bytes, length: str | None = None):
- self._payload = payload
- self.headers = {"Content-Length": length} if length else {}
-
- def read(self, n=-1):
- if not self._payload:
- return b""
- out, self._payload = self._payload[:n], self._payload[n:]
- return out
-
- def __enter__(self):
- return self
-
- def __exit__(self, *_a):
- return False
-
-
-def test_fmt_bytes():
- assert ui.fmt_bytes(512) == "512 B"
- assert ui.fmt_bytes(2048) == "2.0 KB"
- assert ui.fmt_bytes(8 << 20) == "8.0 MB"
- assert ui.fmt_bytes(None) == "?"
-
-
-def test_fmt_secs():
- assert ui.fmt_secs(0) == "0:00"
- assert ui.fmt_secs(59) == "0:59"
- assert ui.fmt_secs(61) == "1:01"
- assert ui.fmt_secs(3700) == "1:01:40"
- assert ui.fmt_secs(None) == "?"
-
-
-def test_bar_fills():
- assert ui._bar(0.0) == "[" + "-" * 22 + "]"
- assert ui._bar(1.0) == "[" + "#" * 22 + "]"
- half = ui._bar(0.5)
- assert half.count("#") == 11 and half.count("-") == 11
-
-
-def test_download_writes_file(tmp_path, monkeypatch, capsys):
- payload = b"x" * (2 << 20)
- sha = hashlib.sha256(payload).hexdigest()
- monkeypatch.setattr(
- "urllib.request.urlopen", lambda req, timeout=None: _FakeResp(payload, str(len(payload)))
- )
- dest = tmp_path / "big.bin"
- out = ui.download("https://example.com/big.bin", dest, sha256=sha)
- assert out == dest
- assert dest.read_bytes() == payload
- assert "done" in capsys.readouterr().out
-
-
-def test_download_checksum_mismatch(tmp_path, monkeypatch):
- monkeypatch.setattr("urllib.request.urlopen", lambda req, timeout=None: _FakeResp(b"abc"))
- dest = tmp_path / "f.bin"
- with pytest.raises(RuntimeError):
- ui.download("https://example.com/f.bin", dest, sha256="0" * 64)
- assert not dest.exists()
- assert not dest.with_name(dest.name + ".part").exists()
-
-
-def test_download_cached_skips(tmp_path, monkeypatch, capsys):
- dest = tmp_path / "cached.bin"
- dest.write_bytes(b"hello")
-
- def boom(*_a, **_k):
- raise AssertionError("should not download")
-
- monkeypatch.setattr("urllib.request.urlopen", boom)
- ui.download("https://example.com/cached.bin", dest)
- assert dest.read_bytes() == b"hello"
- assert "cached" in capsys.readouterr().out
-
-
-def test_download_size_mismatch_redownloads(tmp_path, monkeypatch):
- dest = tmp_path / "short.bin"
- dest.write_bytes(b"too short")
- monkeypatch.setattr("urllib.request.urlopen", lambda req, timeout=None: _FakeResp(b"abcd"))
- ui.download("https://example.com/short.bin", dest, expected_size=4)
- assert dest.read_bytes() == b"abcd"
-
-
-def test_download_milestones_non_tty(tmp_path, monkeypatch, capsys):
- payload = b"y" * (8 << 20)
- monkeypatch.setattr(
- "urllib.request.urlopen", lambda req, timeout=None: _FakeResp(payload, str(len(payload)))
- )
- ui.download("https://example.com/m.bin", tmp_path / "m.bin")
- out = capsys.readouterr().out
- assert "8.0 MB/8.0 MB" in out
- assert "done" in out
-
-
-def test_run_piped_forwards_output(monkeypatch, capsys):
- monkeypatch.setattr(ui, "is_tty", lambda: False)
- rc = ui.run([sys.executable, "-c", "print('hello-uv-output')"], "test run")
- assert rc == 0
- assert "hello-uv-output" in capsys.readouterr().out
-
-
-def test_run_piped_raises_on_failure(monkeypatch):
- monkeypatch.setattr(ui, "is_tty", lambda: False)
- with pytest.raises(subprocess.CalledProcessError):
- ui.run([sys.executable, "-c", "raise SystemExit(3)"], "test run")
-
-
-def test_status_non_tty_milestones_and_done(capsys):
- s = ui.Status(prefix="[producer] in.wav — ")
- s.stage("denoise")
- for i in range(1, 11):
- s.tick(i, 10)
- s.stage_done("denoise", 2.5)
- s.stage("dsp")
- s.stage_done("dsp", 0.3)
- s.finish()
- out = capsys.readouterr().out
- assert "denoise 10/10 chunks (100%)" in out
- assert "denoise done in 2.5s (10/10 chunks)" in out
- assert "dsp done in 0.3s" in out
-
-
-def test_status_tty_live_line(monkeypatch, capsys):
- monkeypatch.setattr(ui, "is_tty", lambda: True)
- s = ui.Status(prefix="p — ")
- s.stage("denoise")
- s.tick(10, 10) # completion redraws immediately (bypasses the 0.1 s throttle)
- out = capsys.readouterr().out
- assert "denoise" in out and "10/10" in out
- s.finish()
-
-
-def test_progress_milestones_non_tty(capsys):
- p = ui.Progress("downloading x", total=1000)
- for i in (100, 300, 1000):
- p.update(i)
- p.close()
- out = capsys.readouterr().out
- assert "1000 B/1000 B" in out
- assert "done (1000 B" in out
-
-
-def test_progress_tty_bar(monkeypatch, capsys):
- monkeypatch.setattr(ui, "is_tty", lambda: True)
- p = ui.Progress("downloading torch", total=100)
- p.update(50)
- p.close()
- out = capsys.readouterr().out
- assert "50 B/100 B (50%)" in out
-
-
-def test_log_clears_live_line(monkeypatch, capsys):
- monkeypatch.setattr(ui, "is_tty", lambda: True)
- p = ui.Progress("downloading", total=100)
- p.update(50)
- ui.log("[producer] some message")
- out = capsys.readouterr().out
- assert "some message" in out
- assert out.index("some message") > out.index("downloading")
- p.close()
diff --git a/lib/tests/test_updates.py b/lib/tests/test_updates.py
deleted file mode 100644
index 7ad1727..0000000
--- a/lib/tests/test_updates.py
+++ /dev/null
@@ -1,221 +0,0 @@
-import subprocess
-import sys
-from types import SimpleNamespace
-
-from producer import lazy, updates
-
-
-def test_parse_would_install():
- out = "\n".join(
- [
- "Using Python 3.11.16 environment at: /x",
- "Resolved 2 packages in 1ms",
- "Would install 2 packages",
- " + scipy==1.14.1",
- " - numpy==1.26.4",
- " + numpy==2.2.6",
- " ~ cffi==2.1.1",
- "",
- ]
- )
- assert updates._parse_would_install(out) == {
- "scipy": "1.14.1",
- "numpy": "2.2.6",
- "cffi": "2.1.1",
- }
-
-
-def test_vkey_ordering():
- assert updates._vkey("1.26.4") < updates._vkey("2.2.6")
- assert updates._vkey("0.5.6") < updates._vkey("0.5.7")
- assert updates._vkey("2.7.1") < updates._vkey("2.7.1+cu126")
- assert updates._vkey("1.2.10") > updates._vkey("1.2.9")
- assert not updates._vkey("0.5.6") > updates._vkey("0.5.6")
-
-
-def test_core_names(tmp_path, monkeypatch):
- reqs = tmp_path / "requirements-core.txt"
- reqs.write_text("numpy==1.26.4\nscipy>=1.11,<1.15\n\n# comment\nsoundfile>=0.12,<0.13\n")
- monkeypatch.setattr(updates, "CORE_FILE", reqs)
- assert updates._core_names() == ["numpy", "scipy", "soundfile"]
-
-
-def test_probe_filters_to_keep(monkeypatch):
- calls = []
-
- def fake_run(cmd, capture_output, text, timeout):
- calls.append(cmd)
- return SimpleNamespace(
- returncode=0,
- stdout="Resolved 2 packages\n + scipy==1.14.1\n + numpy==2.2.6\n",
- )
-
- monkeypatch.setattr(lazy, "find_uv", lambda: "uv")
- monkeypatch.setattr(updates.subprocess, "run", fake_run)
- got = updates._probe(["-U", "-r", "reqs.txt"], None, {"scipy"})
- assert got == {"scipy": "1.14.1"}
- assert calls[0][1:4] == ["pip", "install", "--dry-run"]
-
-
-def test_probe_failure_returns_empty(monkeypatch):
- def boom(cmd, capture_output, text, timeout):
- raise subprocess.TimeoutExpired(cmd, timeout)
-
- monkeypatch.setattr(lazy, "find_uv", lambda: "uv")
- monkeypatch.setattr(updates.subprocess, "run", boom)
- assert updates._probe(["torch"], None, {"torch"}) == {}
-
-
-def test_collect_full_matrix(monkeypatch, tmp_path):
- installed = {
- "numpy": "1.26.4",
- "scipy": "1.13.1",
- "soundfile": "0.12.1",
- "pyloudnorm": "0.2.0",
- "torch": "2.7.1+cu126",
- "torchaudio": "2.7.1+cu126",
- "deepfilternet": "0.5.6",
- "zipenhancer": None,
- "clearvoice": None,
- }
-
- def fake_probe(args, python, keep):
- if "torch" in args and "torchaudio" in args and "--index-url" in args:
- return {"torch": "2.9.0+cu126", "torchaudio": "2.9.0+cu126"}
- if "deepfilternet" in args:
- return {"deepfilternet": "0.5.7", "torch": "2.14.0"}
- if "zipenhancer" in args:
- return {}
- return {"scipy": "1.14.1"}
-
- monkeypatch.setattr(updates, "_installed", lambda d: installed.get(d))
- monkeypatch.setattr(updates, "_probe", fake_probe)
- monkeypatch.setattr(lazy, "gpu_present", lambda: True)
- monkeypatch.setattr(lazy, "find_uv", lambda: "uv")
- monkeypatch.setattr(updates, "RESEMBLE_PY", tmp_path / "missing" / "python")
-
- got = updates.collect()
- labels = [u.label for u in got]
- assert labels == ["scipy", "torch + torchaudio", "deepfilternet"]
-
- scipy = got[0]
- assert (scipy.old, scipy.new) == ("1.13.1", "1.14.1")
- assert "-U" in scipy.cmd and str(updates.CORE_FILE) in scipy.cmd
-
- torch_u = got[1]
- assert (torch_u.old, torch_u.new) == ("2.7.1+cu126", "2.9.0+cu126")
- assert "CUDA" in torch_u.note
- assert "torch==2.9.0+cu126" in torch_u.cmd
- assert "torchaudio==2.9.0+cu126" in torch_u.cmd
- assert lazy.TORCH_GPU_INDEX in torch_u.cmd
-
- dfn = got[2]
- assert (dfn.old, dfn.new) == ("0.5.6", "0.5.7")
- assert "torch==2.7.1" in dfn.cmd
- assert "deepfilternet==0.5.7" in dfn.cmd
- assert "torch==2.14.0" not in dfn.cmd
-
-
-def test_collect_skips_uninstalled_engines_and_missing_torch(monkeypatch, tmp_path):
- monkeypatch.setattr(updates, "_installed", lambda d: None)
- monkeypatch.setattr(lazy, "find_uv", lambda: "uv")
- monkeypatch.setattr(updates, "RESEMBLE_PY", tmp_path / "missing" / "python")
-
- probed = []
- monkeypatch.setattr(updates, "_probe", lambda args, python, keep: probed.append(args) or {})
- assert updates.collect() == []
- assert len(probed) == 1
- assert "deepfilternet" not in probed[0] and "torch" not in probed[0]
-
-
-def test_collect_resemble_isolated_venv(monkeypatch, tmp_path):
- py = tmp_path / "resemble" / "bin" / "python"
- py.parent.mkdir(parents=True)
- py.write_text("")
- monkeypatch.setattr(updates, "_installed", lambda d: "2.7.1+cu126" if d == "torch" else None)
- monkeypatch.setattr(updates, "_installed_in", lambda p, d: "0.0.1")
- monkeypatch.setattr(lazy, "find_uv", lambda: "uv")
- monkeypatch.setattr(updates, "RESEMBLE_PY", py)
-
- def fake_probe(args, python, keep):
- if "resemble-enhance" in args:
- assert python == str(py)
- return {"resemble-enhance": "0.0.2"}
- return {}
-
- monkeypatch.setattr(updates, "_probe", fake_probe)
- got = updates.collect()
- assert len(got) == 1
- u = got[0]
- assert u.label == "resemble-enhance"
- assert "resemble-enhance==0.0.2" in u.cmd
- assert str(py) in u.cmd
-
-
-def test_confirm(monkeypatch):
- answers = iter(["", "no", "y", "yes"])
- monkeypatch.setattr("builtins.input", lambda prompt: next(answers))
- assert not updates._confirm("install updates?")
- assert not updates._confirm("install updates?")
- assert updates._confirm("install updates?")
- assert updates._confirm("install updates?")
-
-
-def test_check_and_prompt_non_tty_notices_only(monkeypatch, capsys):
- monkeypatch.setattr(
- updates,
- "collect",
- lambda: [updates.Update("scipy", "1.13.1", "1.14.1", "", ["uv"])],
- )
- monkeypatch.setattr(sys, "stdin", SimpleNamespace(isatty=lambda: False))
- assert updates.check_and_prompt() is False
- out = capsys.readouterr().out
- assert "updates available" in out
- assert "./producer update" in out
-
-
-def test_check_and_prompt_assume_yes_applies(monkeypatch, capsys):
- u = updates.Update("scipy", "1.13.1", "1.14.1", "", ["uv", "pip", "install", "scipy"])
- monkeypatch.setattr(updates, "collect", lambda: [u])
- runs = []
- monkeypatch.setattr(updates.ui, "run", lambda cmd, label, check=True: runs.append(cmd))
- assert updates.check_and_prompt(assume_yes=True) is True
- assert runs == [["uv", "pip", "install", "scipy"]]
- assert "updates installed" in capsys.readouterr().out
-
-
-def test_check_and_prompt_declined(monkeypatch, capsys):
- u = updates.Update("scipy", "1.13.1", "1.14.1", "", ["uv"])
- monkeypatch.setattr(updates, "collect", lambda: [u])
- monkeypatch.setattr(sys, "stdin", SimpleNamespace(isatty=lambda: True))
- monkeypatch.setattr(updates, "_confirm", lambda prompt: False)
- runs = []
- monkeypatch.setattr(updates.ui, "run", lambda cmd, label, check=True: runs.append(cmd))
- assert updates.check_and_prompt() is False
- assert runs == []
- assert "skipped updates" in capsys.readouterr().out
-
-
-def test_check_and_prompt_up_to_date(monkeypatch, capsys):
- monkeypatch.setattr(updates, "collect", lambda: [])
- assert updates.check_and_prompt(force=True) is False
- assert "up to date" in capsys.readouterr().out
-
-
-def test_update_command_requires_yes_when_not_interactive(monkeypatch):
- monkeypatch.setattr(sys, "stdin", SimpleNamespace(isatty=lambda: False))
- monkeypatch.setattr(
- updates,
- "collect",
- lambda: [updates.Update("scipy", "1.13.1", "1.14.1", "", ["uv"])],
- )
- try:
- updates.run_update_command([])
- except SystemExit as e:
- assert "--yes" in str(e)
- else:
- raise AssertionError("expected SystemExit")
- runs = []
- monkeypatch.setattr(updates.ui, "run", lambda cmd, label, check=True: runs.append(cmd))
- assert updates.run_update_command(["--yes"]) == 0
- assert len(runs) == 1