aboutsummaryrefslogtreecommitdiff

Producer

Local, offline speech cleanup and mono mastering for WAV recordings, launched with a single script: producer.sh. The only AI backend is DeepFilterNet3; FFmpeg provides filtering, dynamics, loudness normalization and encoding. There is no MossFormer backend. Audio processing is local; initial dependency and model setup requires internet access.

Defaults produce a 48 kHz, 24-bit mono WAV, using two-pass loudness normalization targeting -19 LUFS and -1.5 dBTP. MP3 is optional, at 192 kb/s by default. Originals are never overwritten, including with --overwrite.

Quick Start

./producer.sh process recording.wav

The first run bootstraps everything the program needs into lib/runtime/: a managed Python for the application (3.13), the application dependencies, a bundled FFmpeg, and — on first AI use — the separate pinned Python 3.11 DeepFilterNet3 environment and model weights. Every later run reuses that runtime, including fully offline once the needed assets are present. Nothing is installed outside this directory: no PATH changes, no binaries on your system, no sudo, and nothing written to your home directory.

Requirements for the first bootstrap: a glibc Linux with bash, and either an existing uv or a Python 3 with venv support (python3 -m venv). Download trust is PyPI (uv, wheels), Astral’s Python distribution, and — for AI — download.pytorch.org plus the pinned model host. There is no curl-to-shell.

./producer.sh doctor                       # check tools and driver, no AI downloads
./producer.sh setup --device cuda          # install and self-test the AI backend
./producer.sh process recording.wav --device cuda --mp3

Manage the NVIDIA driver yourself, using the appropriate packages for your GPU and kernel. Check nvidia-smi before CUDA setup. The launcher never installs drivers or a system CUDA toolkit. The pinned CUDA backend uses PyTorch’s CUDA 12.1 wheels and needs a compatible NVIDIA driver on Linux x86_64. For CPU use, replace cuda with cpu. The default auto selects based on availability; explicit cuda fails rather than silently using CPU.

Moving or renaming the directory

Move or rename the whole directory freely; nothing breaks. The next launch detects that the runtime moved, drops only the path-bound environments, and rebuilds them from the intact download caches — usually within seconds and without re-downloading. Models, caches, and recordings are preserved. ./producer.sh --rebuild process ... forces the same repair after you edit the source code under lib/project/.

Removing the program

Delete the directory. Recordings and their outputs always live wherever the inputs are (or where you point --output/--output-dir), never inside the runtime, so there is nothing else to clean up.

Everyday Use

# Master with the natural profile; outputs appear next to the input:
# recording.natural.wav, recording.natural.report.json
./producer.sh process recording.wav

# Other starting points; optional MP3 alongside WAV and report
./producer.sh process recording.wav --profile narrator --mp3
./producer.sh process recording.wav --profile radio
./producer.sh process recording.wav --profile cleanup-only

# Batch: every input keeps its outputs beside itself, so same-named files in
# different directories never collide
./producer.sh process take1.wav take2.wav --mp3

# Explicit destinations instead of the default
./producer.sh process recording.wav --output-dir delivery --mp3
./producer.sh process recording.wav -o delivery/episode.wav

# Stereo requires an explicit choice; final export is always mono
./producer.sh process stereo.wav --channel left
# Alternatives: --channel right, or --channel mix (equal-weight average)

# Explicit non-AI fallback: never selected automatically after an AI error
./producer.sh process recording.wav --denoiser fft --fft-reduction-db 6
./producer.sh process recording.wav --no-denoise

# Measurements as JSON on stdout, progress/logs on stderr
./producer.sh analyze recording.wav > analysis.json

Default outputs are written beside each input: <input-dir>/<stem>.<profile>.wav, plus <stem>.<profile>.report.json, and <stem>.<profile>.mp3 when requested. Previews default to <input-dir>/<stem>.<profile>.preview.wav. --output-dir collects all results in one directory instead; -o/--output sets one explicit WAV destination (single input) and cannot be combined with --output-dir. Existing outputs or sidecars cause an error unless --overwrite is supplied. Duplicate batch output names require separate runs with distinct --output paths. Processing accepts mono or stereo WAV inputs, not surround audio; it does not preserve stereo. Avoid mix for opposite-polarity channels, which can cancel the voice. The whole batch plan (all inputs, all outputs, all sidecars) is validated before any input is processed.

Profile Intended starting point
natural Mild EQ, 2:1 compression, light de-essing and normalization
narrator Gentler 1.6:1 compression, slower attack, wider loudness range
radio Stronger 3.5:1 compression, more warmth/presence, narrower range
cleanup-only Denoising and highpass; no leveling, EQ, compression, de-essing, normalization or limiter by default

These presets are engineering starting points, not tuned by listening to your recording or validated as universally good sounding. Listen before publishing.

Configuration

There is no config file at the root on purpose: configuration is only loaded from an explicit --config path. A complete annotated reference with natural-profile defaults lives at lib/project/examples/voice.toml.

./producer.sh process recording.wav --config lib/project/examples/voice.toml
./producer.sh process recording.wav --denoise-strength 0.65 --no-eq --hum-hz 50
./producer.sh process recording.wav --set 'denoise_strength=0.65' --set 'device="cpu"'

# Inspect or generate resolved settings without downloading AI
./producer.sh config --profile narrator
./producer.sh config --profile radio --json
./producer.sh config --profile natural > voice.toml
./producer.sh config --config voice.toml --no-compression
./producer.sh process --help

Settings have named flags: replace underscores with hyphens, such as true_peak_db -> --true-peak-db. Booleans support both forms, such as --eq and --no-eq. Repeated --set KEY=VALUE accepts TOML values (strings may also be unquoted). Unknown keys, incorrect types and out-of-range values are rejected. Select presets with --profile; --set profile=radio also selects and validates the profile before applying its defaults — except for preview, where profiles come from --profiles and a profile override via --set is rejected so file names always match the rendered configuration.

Precedence is defaults, selected profile, TOML values, named flags, then --set (last assignment wins). --profile selects ahead of a file’s profile, but other explicit file values still override the selected preset. A full natural config therefore masks narrator/radio settings; remove keys you want the preset to supply.

--no-normalize alone leaves the default limiter on. Use --no-normalize --no-limiter to disable both. Normalization requires limiting, so --no-limiter alone is invalid with normalizing profiles. Denoising can be bypassed with --no-denoise, --denoiser none, or --denoise-strength 0.

Previews

./producer.sh preview recording.wav --start 10 --duration 30 \
  --profiles natural narrator radio cleanup-only

Previews go beside the input by default, with reports and optional MP3s. Start must be nonnegative; duration must be greater than zero and at most 300 seconds. The default comparison includes natural, narrator and radio. Previews force normalization and limiting, even for cleanup-only and when --no-normalize is given, to compare at the configured loudness target. They are not a raw A/B comparison, and there is currently no raw-original preview export. Explicit config overrides can also make presets sound identical.

Limits and Reports

  • Leveling is one static recording-level gain correction, estimated from the cleaned audio and capped by max_gain_db. The estimate comes from a histogram of 100 ms RMS blocks; on pause-dominated recordings it falls back to the loud tail so sparse speech is not mistaken for silence, and if the estimate is still indistinguishable from the noise floor, leveling is skipped with a warning instead of applying a maximum gain. It is not slow automatic gain riding and does not independently repair quiet phrases.
  • Speech/noise estimates use that same histogram, not speech recognition. Noise floor is a low-confidence quiet-block heuristic and may be absent without sufficient room tone. It is not a certified noise measurement.
  • Denoising can affect timbre or introduce artifacts. Clipped peaks, severe room reverb and missing detail cannot be perfectly restored. There is no promise of perfect restoration or ACX compliance; check the actual delivery specification.
  • Normalization uses a measured first pass and a render pass, then verifies the exported WAV and decoded MP3. The final resample dithers explicitly at 24-bit depth. FFmpeg may use dynamic rather than linear normalization; the report records the mode. Loudness misses over 0.5 LU and limited WAV true-peak overshoots over 0.1 dB are warn-only, not automatic rejection or correction. MP3 overshoots also warn; use more headroom and rerun.
  • Reports include resolved settings, input/cleaned/master measurements, gain correction, loudness and warnings. Inspect warnings before publication; successful exit alone does not certify delivery targets.
  • AI working memory is bounded by 10-second chunks, with 2-second context and 50 ms overlap, rather than loading the whole recording. Chunked inference is not bit-identical to whole-recording inference. Measurement also uses bounded memory.
  • Float intermediates trade RAM for disk and multiple full-file passes. A mono 48 kHz float file costs about 0.69 GB per hour on the output filesystem; intermediates are deleted as soon as each stage has consumed them. Allow several GB of free space per hour on the output filesystem, separately from runtime storage. Runtime includes repeated disk I/O.
  • Files are staged beside the output and published only after processing and verification. Publication is atomic per file, and a directory lock prevents concurrent jobs writing to the same output directory from interleaving their WAV/report/MP3 sets. Publication across the three files of one job is not transactional: a failure between publications can leave a partial set, or mixed old/new outputs with --overwrite.

Progress and Troubleshooting

The CLI sends progress, diagnostics and completion messages to stderr. analyze and config --json emit JSON on stdout; config emits TOML there. Interactive terminals show stage progress and elapsed/remaining time; redirected logs get stage changes and periodic updates. Unknown totals show activity, not a meaningful percentage. Internal worker stdout carries prefixed JSON progress records, consumed by the parent; this is not a public CLI JSON-progress mode. Ctrl-C exits with code 130 and terminates the whole active process group — including grandchildren that outlive their parent — escalating from SIGTERM to SIGKILL after five seconds if needed. Ordinary failures exit 1. Temporary-file cleanup runs on handled cancellation/errors; SIGKILL or power loss can leave temporary directories. Already published files remain.

  • First bootstrap fails: the launcher needs uv, or python3 with venv support, plus network access to PyPI. Install the missing tool with your system package manager and rerun; the launcher never runs sudo itself.
  • CUDA unavailable: run ./producer.sh doctor, inspect nvidia-smi, the driver and CUDA_VISIBLE_DEVICES, then retry ./producer.sh setup --device cuda. Use --device cpu explicitly if needed. doctor probes tools/driver without AI downloads; setup actually self-tests Torch and DeepFilterNet.
  • Download/checksum or incomplete model failure: check network access and free space; never bypass the checksum. For an incomplete-model error, remove only the reported lib/runtime/models/df3-0.5.6 directory when no jobs are running, then retry setup.
  • FFmpeg missing/filter or MP3 encoder missing: the bundled FFmpeg is preferred; a system ffmpeg is used where the bundle cannot be installed. Run ./producer.sh doctor.
  • Changed source code: the runtime runs an installed copy of lib/project; run ./producer.sh --rebuild after editing it.
  • Musl/Alpine: AI requires Linux glibc >= 2.28; musl is explicitly unsupported. On musl, the launcher falls back to a system FFmpeg, and soundfile needs a system libsndfile with an unversioned symlink (for example apk add libsndfile-dev). A separate temporary glibc test confirmed DeepFilterNet CPU execution; CUDA was not tested. These are test boundaries, not a claim of end-to-end CUDA validation.
  • Silent/too-short audio or bad stereo mix: inspect the source with analyze, select the correct channel and use enough audible speech for loudness analysis. If cleanup sounds watery or thin, lower denoise strength and compare previews.

Layout

producer.sh            the only entry point; bootstrap + run
LICENSE                0BSD
README.md
lib/
  project/             application source, tests, example configuration
  runtime/             managed Python, dependencies, bundled FFmpeg, AI
                       environments, model weights, download caches (gitignored)
  archive/             previously generated listening samples and reports

lib/runtime/ is fully regenerable: deleting it only costs re-downloads on the next launch.

Development

On a glibc development host with Python >= 3.11, pip and FFmpeg:

python3 -m venv .venv
.venv/bin/python -m pip install -e 'lib/project[test]'
.venv/bin/python -m pytest -q lib/project/tests
./producer.sh --rebuild config --config lib/project/examples/voice.toml --json

Alternatively run the same bootstrap the launcher uses. Tests cover configuration, synthetic audio, FFmpeg processing, output safety, the launcher contract, and AI subprocess handling with controlled model/download stubs; they do not download/exercise the real AI backend or substitute for listening. Real CPU inference was verified separately.

For the musl test container only, where the declared bundled-FFmpeg wheel is unavailable, use system FFmpeg and explicitly install core/test dependencies:

python3 -m venv .venv
.venv/bin/python -m pip install 'hatchling==1.27.0' 'rich==14.1.0' \
  'numpy>=1.26.4,<3' 'soundfile==0.13.1' 'pytest>=8,<10'
.venv/bin/python -m pip install --no-deps -e 'lib/project'
.venv/bin/python -m pytest -q lib/project/tests

License

This application is licensed under 0BSD. Review upstream terms before redistributing dependencies, binaries or weights: DeepFilterNet code and models, PyTorch, Torchaudio, FFmpeg, imageio-ffmpeg, and uv. FFmpeg obligations depend on the build and enabled components. Upstream licenses do not assign a license to this application’s code.