diff options
Diffstat (limited to 'lib/project/src/voiceforge/config.py')
| -rw-r--r-- | lib/project/src/voiceforge/config.py | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/lib/project/src/voiceforge/config.py b/lib/project/src/voiceforge/config.py new file mode 100644 index 0000000..9cbd59d --- /dev/null +++ b/lib/project/src/voiceforge/config.py @@ -0,0 +1,118 @@ +"""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" |
