From 742093b18cd31e9d25c43f475b94c232fc28a5a7 Mon Sep 17 00:00:00 2001 From: historia Date: Fri, 11 Sep 2026 18:21:38 -0400 Subject: feat: config.toml added --- README.md | 31 ++++++++++++----------- config.toml | 55 +++++++++++++++++++++++++++++++++++++++++ lib/project/examples/voice.toml | 55 ----------------------------------------- lib/project/pyproject.toml | 2 +- lib/project/src/producer/cli.py | 8 ++++++ lib/project/tests/test_cli.py | 16 ++++++++++++ producer.sh | 31 +++++++++++++++++++++-- 7 files changed, 126 insertions(+), 72 deletions(-) create mode 100644 config.toml delete mode 100644 lib/project/examples/voice.toml diff --git a/README.md b/README.md index c405e75..63f5944 100644 --- a/README.md +++ b/README.md @@ -16,46 +16,49 @@ dependencies and models automatically. ```bash git clone https://git.historia.vg/git/producer ./producer.sh setup -./producer.sh process recording.wav +./producer.sh recording.wav ``` First run installs the runtime into `lib/runtime/` and may take a few minutes. +The `process` keyword is optional; `./producer.sh process recording.wav` is +equivalent. The master WAV is always **mono, 32-bit PCM**, at 48 kHz (default) or 44.1 kHz; stereo inputs must be downmixed to mono first. MP3 is encoded as constant bitrate at 192 kb/s by default. Loudness is normalized to **-19 -LUFS with a -1.5 dBTP true-peak ceiling**. A diagnostic report for each run -is written to a `logs/` directory next to the outputs, not beside the audio. +LUFS with a -1.5 dBTP true-peak ceiling**. ### Examples -```bash # Optional MP3 alongside the WAV -./producer.sh process recording.wav --mp3 +./producer.sh recording.wav --mp3 # Other sound profiles -./producer.sh process recording.wav --profile narrator -./producer.sh process recording.wav --profile radio +./producer.sh recording.wav --profile narrator +./producer.sh recording.wav --profile radio # Process several files at once -./producer.sh process take1.wav take2.wav --mp3 +./producer.sh take1.wav take2.wav --mp3 # Write outputs somewhere else -./producer.sh process recording.wav --output-dir delivery --mp3 +./producer.sh recording.wav --output-dir delivery --mp3 # Skip AI noise removal (FFT fallback or none at all) -./producer.sh process recording.wav --denoiser fft -./producer.sh process recording.wav --no-denoise +./producer.sh recording.wav --denoiser fft +./producer.sh recording.wav --no-denoise # LibriVox-style delivery file: 44.1 kHz, 128 kbps CBR, -19 LUFS, mono -./producer.sh process recording.wav \ +./producer.sh recording.wav \ --sample-rate 44100 --mp3 --mp3-bitrate 128 \ --target-lufs -19 --true-peak-db -1.5 ``` All of these settings are configurable; run `./producer.sh config` to see every -setting and its default, or `--help` on any command for the flags. A fully -documented example config lives at `lib/project/examples/voice.toml`. +setting and its default, or `--help` on any command for the flags. A +`config.toml` in the same directory as `producer.sh` is loaded automatically: +`./producer.sh config` then shows those resolved values, and an explicit +`--config FILE` or any CLI flag still overrides it. The shipped `config.toml` +documents every setting. ## Profiles diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..a574717 --- /dev/null +++ b/config.toml @@ -0,0 +1,55 @@ +# Producer configuration. This file is loaded automatically because it sits +# next to producer.sh. Flat TOML: no section headers. Named CLI flags override +# these values; --set KEY=VALUE overrides named flags. Use --profile, not +# --set profile=... All explicit values below override preset defaults, even +# with --profile radio. Remove settings you want a different profile to supply. + +profile = "narrator" # natural, narrator, radio, cleanup-only +denoiser = "deepfilter" # DeepFilterNet3 only; fft is explicit non-AI fallback; none +device = "auto" # auto, cpu, cuda; explicit cuda fails if unavailable +denoise_strength = 0.85 # 0..1; AI dry/wet blend; zero skips denoising and AI setup +fft_reduction_db = 8.0 # 0.01..30; FFT reduction is scaled by denoise_strength + +highpass = true +highpass_hz = 80.0 # 20..300 +lowpass = false +lowpass_hz = 16000.0 # 4000..22000; must be below output Nyquist when enabled +hum_hz = 0 # 0 (off), 50 or 60; notches fundamental and next two harmonics + +# One static gain adjustment for the whole recording, not automatic gain riding. +leveling = true +level_target_db = -23.0 # -40..-12; heuristic speech level in dBFS, not LUFS +max_gain_db = 18.0 # 0..30; caps both gain and attenuation + +expansion = false +expansion_threshold_db = -50.0 # -90..-20 +expansion_ratio = 1.5 # 1..4 +expansion_range_db = 12.0 # 0..40 + +eq = true +warmth_db = 0.5 # -12..12; 140 Hz +mud_db = -1.5 # -12..12; 300 Hz +presence_db = 1.0 # -12..12; 3500 Hz + +compression = true +compressor_threshold_db = -21.0 # -50..-5 +compressor_ratio = 2.0 # 1..10 +compressor_attack_ms = 15.0 # 0.1..200 +compressor_release_ms = 150.0 # 10..2000 +deess = true +deess_intensity = 0.15 # 0..1 +deess_amount = 0.4 # 0..1 + +# Two-pass normalization plus export verification; target misses warn only. +# Turning normalize off leaves limiter on unless you also disable limiter. +# normalize=true requires limiter=true. Previews force both on, even cleanup-only. +normalize = true +target_lufs = -19.0 # -30..-12; mono delivery target, not ACX certification +loudness_range = 9.0 # 1..20 LU +limiter = true +true_peak_db = -1.5 # -9..-0.5 dBTP; MP3 can overshoot, inspect report warnings + +# Final exports are always mono, 32-bit PCM WAV, with an optional MP3 sidecar. +sample_rate = 48000 # 44100 or 48000; intermediate processing is 48 kHz float +mp3 = false +mp3_bitrate = 192 # 128, 160, 192, 224, 256 or 320 kb/s diff --git a/lib/project/examples/voice.toml b/lib/project/examples/voice.toml deleted file mode 100644 index ba0fa5c..0000000 --- a/lib/project/examples/voice.toml +++ /dev/null @@ -1,55 +0,0 @@ -# Complete natural-profile defaults. Load with --config lib/project/examples/voice.toml. -# Flat TOML: no section headers. Named CLI flags override these values; -# --set KEY=VALUE overrides named flags. Use --profile, not --set profile=... -# All explicit values below override preset defaults, even with --profile radio. -# Remove settings you want a different profile to supply. - -profile = "natural" # natural, narrator, radio, cleanup-only -denoiser = "deepfilter" # DeepFilterNet3 only; fft is explicit non-AI fallback; none -device = "auto" # auto, cpu, cuda; explicit cuda fails if unavailable -denoise_strength = 0.85 # 0..1; AI dry/wet blend; zero skips denoising and AI setup -fft_reduction_db = 8.0 # 0.01..30; FFT reduction is scaled by denoise_strength - -highpass = true -highpass_hz = 70.0 # 20..300 -lowpass = false -lowpass_hz = 16000.0 # 4000..22000; must be below output Nyquist when enabled -hum_hz = 0 # 0 (off), 50 or 60; notches fundamental and next two harmonics - -# One static gain adjustment for the whole recording, not automatic gain riding. -leveling = true -level_target_db = -23.0 # -40..-12; heuristic speech level in dBFS, not LUFS -max_gain_db = 18.0 # 0..30; caps both gain and attenuation - -expansion = false -expansion_threshold_db = -50.0 # -90..-20 -expansion_ratio = 1.5 # 1..4 -expansion_range_db = 12.0 # 0..40 - -eq = true -warmth_db = 0.5 # -12..12; 140 Hz -mud_db = -1.5 # -12..12; 300 Hz -presence_db = 1.0 # -12..12; 3500 Hz - -compression = true -compressor_threshold_db = -21.0 # -50..-5 -compressor_ratio = 2.0 # 1..10 -compressor_attack_ms = 15.0 # 0.1..200 -compressor_release_ms = 150.0 # 10..2000 -deess = true -deess_intensity = 0.15 # 0..1 -deess_amount = 0.4 # 0..1 - -# Two-pass normalization plus export verification; target misses warn only. -# Turning normalize off leaves limiter on unless you also disable limiter. -# normalize=true requires limiter=true. Previews force both on, even cleanup-only. -normalize = true -target_lufs = -19.0 # -30..-12; mono delivery target, not ACX certification -loudness_range = 9.0 # 1..20 LU -limiter = true -true_peak_db = -1.5 # -9..-0.5 dBTP; MP3 can overshoot, inspect report warnings - -# Final exports are always mono, 32-bit PCM WAV, with an optional MP3 sidecar. -sample_rate = 48000 # 44100 or 48000; intermediate processing is 48 kHz float -mp3 = false -mp3_bitrate = 192 # 128, 160, 192, 224, 256 or 320 kb/s diff --git a/lib/project/pyproject.toml b/lib/project/pyproject.toml index 625ee23..6c42cc9 100644 --- a/lib/project/pyproject.toml +++ b/lib/project/pyproject.toml @@ -26,4 +26,4 @@ testpaths = ["tests"] packages = ["src/producer"] [tool.hatch.build.targets.sdist] -include = ["/src", "/tests", "/examples", "/pyproject.toml"] +include = ["/src", "/tests", "/pyproject.toml"] diff --git a/lib/project/src/producer/cli.py b/lib/project/src/producer/cli.py index 4788fb5..374455a 100644 --- a/lib/project/src/producer/cli.py +++ b/lib/project/src/producer/cli.py @@ -80,7 +80,15 @@ def require_input(source: Path) -> None: raise ValueError(f"Input must be an existing WAV file: {source}") +# The 'process' keyword is optional: any first argument that is not a known +# command (or a global option) is treated as the start of a process run. +COMMANDS = ("process", "preview", "config", "doctor", "setup") +GLOBAL_OPTIONS = ("-h", "--help", "--version") + + def main(): + if len(sys.argv) > 1 and sys.argv[1] not in COMMANDS and sys.argv[1] not in GLOBAL_OPTIONS: + sys.argv.insert(1, "process") app = parser() args = app.parse_args() console = Console(stderr=True) diff --git a/lib/project/tests/test_cli.py b/lib/project/tests/test_cli.py index e02aad1..05c8292 100644 --- a/lib/project/tests/test_cli.py +++ b/lib/project/tests/test_cli.py @@ -176,6 +176,22 @@ def test_default_outputs_are_written_beside_each_input(wav, tmp_path, ffmpeg, in assert captured.err.count("Written:") == 2 +def test_process_keyword_is_optional(wav, tmp_path, ffmpeg, invoke, capsys): + source = wav() + original = source.read_bytes() + invoke(source, "--no-denoise") + assert {path.name for path in tmp_path.iterdir()} == { + "input.wav", "input.natural.wav", "logs"} + assert source.read_bytes() == original + assert capsys.readouterr().err.count("Written:") == 1 + + +def test_leading_flag_still_selects_process(wav, tmp_path, ffmpeg, invoke, capsys): + source = wav() + invoke("--no-denoise", source) + assert (tmp_path / "input.natural.wav").exists() + + def test_output_and_output_dir_cannot_be_combined(wav, tmp_path, invoke, capsys): with pytest.raises(SystemExit) as error: invoke("process", wav(), "-o", tmp_path / "x.wav", "--output-dir", tmp_path / "outs") diff --git a/producer.sh b/producer.sh index c6f2759..f2dc6be 100755 --- a/producer.sh +++ b/producer.sh @@ -35,9 +35,11 @@ usage() { cat <<'USAGE' Producer - local speech cleanup and podcast mastering. -Usage: ./producer.sh COMMAND [options] +Usage: ./producer.sh COMMAND [options], or simply: + ./producer.sh RECORDING.wav... - process RECORDING.wav... Master one or more WAV files + RECORDING.wav... Master one or more WAV files ('process' is optional) + process RECORDING.wav... The same, with the explicit command keyword preview RECORDING.wav Render loudness-matched profile previews config Print resolved settings (TOML, or JSON with --json) doctor Check FFmpeg, data directory and CUDA driver @@ -72,6 +74,31 @@ if [[ ${#ARGS[@]} -eq 1 && ( "${ARGS[0]}" == "-h" || "${ARGS[0]}" == "--help" ) exit 0 fi +# A config.toml next to this script is loaded automatically for every +# command that accepts --config, unless one is given explicitly. Named CLI +# flags and --set still override values from the file. +HAVE_CONFIG=0 +for arg in ${ARGS[@]+"${ARGS[@]}"}; do + case "$arg" in --config|--config=*) HAVE_CONFIG=1 ;; esac +done +if [[ $HAVE_CONFIG -eq 0 && -f "$ROOT/config.toml" ]]; then + case "${ARGS[0]-}" in + # After the command word: the CLI treats a leading non-command + # argument as the implicit 'process' command and then as an input. + process|preview|config) + ARGS=("${ARGS[0]}" "--config=$ROOT/config.toml" "${ARGS[@]:1}") + ;; + # These commands take no --config (or no arguments at all). + doctor|setup|-h|--help|--version|"") + ;; + *) + # Bare inputs or options: the CLI inserts 'process' first, so the + # flag can go in front. + ARGS=("--config=$ROOT/config.toml" ${ARGS[@]+"${ARGS[@]}"}) + ;; + esac +fi + mkdir -p -- "$RUNTIME/cache/pip" "$RUNTIME/cache/uv" "$RUNTIME/uv" "$RUNTIME/tmp" \ || fail "Cannot create the runtime directory under $LIB" -- cgit v1.2.3