"""Flat, strict TOML configuration; CLI overrides are applied last.""" from dataclasses import asdict, dataclass, fields import math from pathlib import Path import tomllib @dataclass class Settings: profile: str = "natural" denoiser: str = "deepfilter" device: str = "auto" denoise_strength: float = 0.85 fft_reduction_db: float = 8.0 channel: str = "auto" highpass: bool = True highpass_hz: float = 70.0 lowpass: bool = False lowpass_hz: float = 16000.0 hum_hz: int = 0 leveling: bool = True level_target_db: float = -23.0 max_gain_db: float = 18.0 expansion: bool = False expansion_threshold_db: float = -50.0 expansion_ratio: float = 1.5 expansion_range_db: float = 12.0 eq: bool = True warmth_db: float = 0.5 mud_db: float = -1.5 presence_db: float = 1.0 compression: bool = True compressor_threshold_db: float = -21.0 compressor_ratio: float = 2.0 compressor_attack_ms: float = 15.0 compressor_release_ms: float = 150.0 deess: bool = True deess_intensity: float = 0.15 deess_amount: float = 0.4 normalize: bool = True target_lufs: float = -19.0 loudness_range: float = 9.0 limiter: bool = True true_peak_db: float = -1.5 sample_rate: int = 48000 mp3: bool = False mp3_bitrate: int = 192 PROFILES = { "natural": {}, "narrator": {"compressor_ratio": 1.6, "compressor_attack_ms": 25.0, "presence_db": 0.5, "warmth_db": 1.0, "loudness_range": 11.0}, "radio": {"compressor_ratio": 3.5, "compressor_threshold_db": -24.0, "compressor_attack_ms": 8.0, "compressor_release_ms": 100.0, "warmth_db": 2.0, "mud_db": -2.0, "presence_db": 2.0, "deess_intensity": 0.25, "loudness_range": 6.0}, "cleanup-only": {"leveling": False, "eq": False, "compression": False, "deess": False, "normalize": False, "limiter": False}, } RANGES = { "denoise_strength": (0, 1), "fft_reduction_db": (0.01, 30), "highpass_hz": (20, 300), "lowpass_hz": (4000, 22000), "level_target_db": (-40, -12), "max_gain_db": (0, 30), "expansion_threshold_db": (-90, -20), "expansion_ratio": (1, 4), "expansion_range_db": (0, 40), "warmth_db": (-12, 12), "mud_db": (-12, 12), "presence_db": (-12, 12), "compressor_threshold_db": (-50, -5), "compressor_ratio": (1, 10), "compressor_attack_ms": (0.1, 200), "compressor_release_ms": (10, 2000), "deess_intensity": (0, 1), "deess_amount": (0, 1), "target_lufs": (-30, -12), "loudness_range": (1, 20), "true_peak_db": (-9, -0.5), } def resolve(path: Path | None = None, profile: str | None = None, overrides: dict | None = None) -> Settings: values = tomllib.loads(path.read_text()) if path else {} chosen = (overrides or {}).get("profile", profile or values.get("profile", "natural")) if not isinstance(chosen, str) or chosen not in PROFILES: raise ValueError(f"Unknown profile {chosen!r}; choose {', '.join(PROFILES)}") merged = asdict(Settings()) | PROFILES[chosen] | values | {"profile": chosen} merged.update(overrides or {}) defaults = asdict(Settings()) unknown = merged.keys() - defaults.keys() if unknown: raise ValueError(f"Unknown setting(s): {', '.join(sorted(unknown))}") for key, value in merged.items(): expected = type(defaults[key]) if expected is float: if type(value) not in (int, float) or not math.isfinite(value): raise ValueError(f"{key} must be a finite number") elif type(value) is not expected: raise ValueError(f"{key} must be {expected.__name__}") if key in RANGES and not RANGES[key][0] <= value <= RANGES[key][1]: raise ValueError(f"{key} must be between {RANGES[key][0]} and {RANGES[key][1]}") for key, options in { "denoiser": ("deepfilter", "fft", "none"), "device": ("auto", "cpu", "cuda"), "channel": ("auto", "left", "right", "mix"), "hum_hz": (0, 50, 60), "sample_rate": (44100, 48000), "mp3_bitrate": (128, 160, 192, 224, 256, 320), }.items(): if merged[key] not in options: raise ValueError(f"{key} must be one of {options}") if merged["lowpass"] and merged["lowpass_hz"] >= merged["sample_rate"] / 2: raise ValueError("lowpass_hz must be below the output Nyquist frequency") if merged["normalize"] and not merged["limiter"]: raise ValueError("Normalization includes true-peak limiting; use --no-normalize to bypass it") return Settings(**merged) def toml(settings: Settings) -> str: lines = ["# VoiceForge configuration. CLI flags override these values."] for field in fields(settings): value = getattr(settings, field.name) text = str(value).lower() if isinstance(value, bool) else repr(value) lines.append(f"{field.name} = {text}") return "\n".join(lines) + "\n"