Vibecode Auphonic
track this build5 phases, 15 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Auphonic, normalize loudness, reduce noise, and batch-process user-owned audio locally. The hard boundary is proprietary adaptive audio processing, cloud queues, and broad format delivery, plus audio infrastructure, distribution, and production polish.
You are building a lean indie version of Auphonic. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Auphonic · indie build A command line and small interface over ffmpeg that measures each file, normalises it to a podcast loudness target with a true peak ceiling, reduces steady noise, evens out speakers recorded at different levels, and exports production ready files with a report showing the measured figures before and after. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg with the loudnorm and afftdn filters | loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one | | Runtime | Python 3.12 driving ffmpeg as a subprocess | the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation | | Measurement | Two pass loudnorm: measure, then apply with linear correction | single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain | | Transcription | whisper.cpp locally, optional | chapter marks and a transcript without sending anyone's recording to a service | | Storage | Original files untouched, outputs written alongside with a JSON report | you must be able to rerun with different settings, which means never overwriting the source | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **ffmpeg with the loudnorm and afftdn filters** · free - Why: It is the entire processing engine. A build without these filters cannot do any of this. - Get it: brew install ffmpeg, your package manager, or a static build from ffmpeg.org. Confirm the filters are present rather than assuming. - Verify: ffmpeg -filters lists loudnorm and afftdn - [ ] **Python 3.12 or newer** · free - Why: Runs the orchestration, measurement parsing and batch logic. - Get it: python.org or your package manager, then a virtual environment in the project directory. - Verify: python3 --version prints 3.12 or higher - [ ] **Three real recordings with different problems** · free - Why: A clean studio file proves nothing. You need one with steady hiss, one with two speakers at clearly different levels, and one already close to target. - Get it: Use your own recordings, or record them deliberately. Keep them as fixtures so a settings change can be compared against the same material. - Verify: All three are saved as fixtures with their initial measurements recorded - [ ] **Headphones or monitors you know well** · free if you already have a pair - Why: Denoising decisions are made by ear, and laptop speakers hide exactly the artefacts you are trying to avoid. - Get it: Any headphones you have listened to enough to know how they sound. Familiarity matters more than price here. - [ ] **whisper.cpp with a model, optional** (optional) · free, but transcription takes real time on your own machine - Why: Phase 5 only, for transcripts and chapter assistance. Everything else works without it. - Get it: Build whisper.cpp and download a model. Model sizes vary from tens of megabytes to several gigabytes, and the larger ones are much slower. - Verify: whisper-cli transcribes a short 16 kHz mono WAV ## Quick start ```sh ffmpeg -i input.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Adaptive per-recording processing decisions. - Cloud queues and processing that continues when your machine sleeps. - Delivery integrations to podcast hosts. - Licensed music and automatic ducking around it. - proprietary adaptive audio processing, cloud queues, and broad format delivery - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support If one of those is essential to you, that is the reason to keep paying for Auphonic, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Auphonic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local batch processor that levels, denoises and exports spoken word audio to a real loudness target to replace Auphonic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Processing: ffmpeg with the loudnorm and afftdn filters · loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one - Runtime: Python 3.12 driving ffmpeg as a subprocess · the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation - Measurement: Two pass loudnorm: measure, then apply with linear correction · single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain - Transcription: whisper.cpp locally, optional · chapter marks and a transcript without sending anyone's recording to a service - Storage: Original files untouched, outputs written alongside with a JSON report · you must be able to rerun with different settings, which means never overwriting the source ### Data model (create this before Phase 1) - jobs(id, created_at, preset_id, state) - files(id, job_id, source_path, output_path, duration_ms, channels, sample_rate) - measurements(file_id, stage, input_i, input_tp, input_lra, input_thresh, target_offset) · stage is before or after - presets(id, name, target_lufs, true_peak_db, denoise_strength, output_format, bitrate) - The measurement rows are the product. A processed file without its before and after figures is a file you cannot defend. ### Phase 1 · Measure before you change anything Build: Run loudnorm in analysis mode and parse its JSON output: integrated loudness, true peak, loudness range and threshold. Store the measurement. Do not modify any audio in this phase. Measuring correctly is what makes every later claim checkable. Trap: Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. Done when: Analysing a file prints integrated loudness in LUFS, true peak in dBTP and loudness range in LU. Analysing a known reference tone produces the expected figure. The source file's bytes are unchanged after analysis, verified by checksum. Do not build yet: No normalisation, no denoising, no batch. One correct measurement. ### Phase 2 · Normalising to a target that is right for speech Build: Apply loudnorm in a second pass using the measured values, with linear correction so a fixed gain is applied rather than dynamic compression. Default to a podcast target of -16 LUFS with a true peak ceiling of -1.5 dBTP, and make both configurable because different destinations want different numbers. Trap: Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. Trap: A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. Done when: A processed file measures within a small tolerance of the configured target, verified by re-running the analysis from Phase 1 on the output. Its loudness range is close to the source's, showing the dynamics were not compressed. A file that was already at target is not audibly altered. Do not build yet: No noise reduction and no batch yet. ### Phase 3 · Noise reduction that does not damage the voice Build: Apply spectral denoising at a configurable strength, defaulting to gentle. Add a high pass to remove rumble below the speech range. Always keep the untouched original and make an A and B comparison easy, because denoising is where people ruin recordings. Trap: Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. Done when: A recording with steady hiss measures a lower noise floor after processing, taken from a silent passage. The speech remains free of the watery artefact at the default strength, confirmed by listening to a passage with sibilance. The original file is still present and unmodified. Do not build yet: No batch and no transcription yet. ### Phase 4 · Multiple speakers and multiple files Build: Process a directory as a job. Normalise each file to the same target so speakers recorded at different levels match. For multitrack recordings of one conversation, measure and correct each track before mixing, not after. Trap: Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. Done when: A job of five files with different source levels produces five outputs measuring within a small tolerance of each other. A multitrack conversation processed per track and then mixed has both speakers at comparable loudness. A failed file does not stop the batch and appears in the report with its error. Do not build yet: No transcription or chapters yet. ### Phase 5 · Transcript, chapters and the report Build: Optionally transcribe locally with whisper.cpp. Generate chapter marks from long silences or from a supplied outline. Write a report showing every file's before and after measurements, the preset used and any warnings. Trap: whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. Done when: A job report lists every file with its before and after integrated loudness and true peak. A transcript is produced for a test recording and its timestamps line up with the audio. Chapter marks land at real boundaries rather than mid sentence. Skipping transcription does not slow the audio export. Do not build yet: Distribution and presets sharing come next. ### Phase 6 · Presets, watch folders and running it for others Build: Named presets committed as files. A watch folder that processes new arrivals automatically. Determinism checks so the same input and preset always produce the same measured output. If others submit files, add authentication and a size limit. Trap: A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing. Done when: Dropping a file into the watch folder produces processed output and a report without intervention. A file still being copied is not processed until the copy completes. Running the same input and preset twice produces outputs with identical measurements. Presets are files under version control. Do not build yet: Nothing after this. ### Out of scope (and why) - The adaptive processing behind the hosted service, which makes per-recording decisions this pipeline makes once per preset. - Cloud queues and the ability to submit an hour of audio and close your laptop. Your processing happens on your machine and takes your machine's time. - Broad delivery integrations to hosting platforms, which are each an API with its own quirks. - Licensed music libraries and the ducking behaviour built around them. ### README must contain - The loudness target and true peak ceiling, and why podcast targets differ from streaming ones - That normalisation is two pass with linear correction, and what single pass would do to the sound - That denoising is destructive and defaults to gentle deliberately - That multitrack input must be corrected per track before mixing - That originals are never modified, and where outputs and reports are written ===== AGENTS.md ===== # Agent instructions · Auphonic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg with the loudnorm and afftdn filters, Python 3.12 driving ffmpeg as a subprocess, Two pass loudnorm: measure, then apply with linear correction, whisper.cpp locally, optional, Original files untouched, outputs written alongside with a JSON report. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. ===== BUILD_PLAN.md ===== # Build plan · Auphonic A command line and small interface over ffmpeg that measures each file, normalises it to a podcast loudness target with a true peak ceiling, reduces steady noise, evens out speakers recorded at different levels, and exports production ready files with a report showing the measured figures before and after. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Measure before you change anything You can state the loudness of a file in the same units everyone else uses. ### Steps 1. Run loudnorm in analysis mode and parse the JSON The filter prints a JSON block on the error stream in print_format=json mode. Parse it rather than scraping the human readable output, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i input.wav -af loudnorm=print_format=json -f null - ``` 2. Store the measurement One row per file per stage. You will compare before and after, and that comparison is the report. Files: `src/db.py` 3. Verify against a known signal Generate a tone at a known level and confirm the measured figure matches. This calibrates your trust in everything downstream. ```sh ffmpeg -f lavfi -i "sine=frequency=1000:duration=10" -af volume=-20dB tone.wav ``` ### Done when - [ ] Analysis prints integrated loudness, true peak and loudness range - [ ] A generated reference tone measures at its expected level - [ ] The source file checksum is unchanged after analysis - [ ] Parsing uses the JSON output rather than the human readable text ### Watch out - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. ## Phase 2 · Normalising to a target that is right for speech Every output lands on the same loudness target without being squashed to get there. ### Steps 1. Feed measured values into the second pass Pass measured_I, measured_TP, measured_LRA and measured_thresh back to loudnorm with linear=true. Without them, the second pass measures again and compresses. Files: `src/normalise.py` ```sh ffmpeg -i in.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-24.1:measured_TP=-3.2:measured_LRA=9.4:measured_thresh=-34.9:linear=true -ar 48000 out.wav ``` 2. Make the target a preset value Podcast, broadcast and streaming destinations differ. Store the target with the preset and record it in the report rather than hard coding one number. Files: `src/presets.py` 3. Verify by re-measuring the output Run Phase 1's analysis on the result. This is a closed loop and it is the whole quality argument. Files: `src/verify.py` ### Done when - [ ] The output measures within tolerance of the configured target - [ ] The output's loudness range is close to the source's rather than much smaller - [ ] A file already at target passes through without audible change - [ ] The applied target is recorded in the report ### Watch out - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. ## Phase 3 · Noise reduction that does not damage the voice Steady background noise is reduced, and the speech still sounds like the person. ### Steps 1. Add a high pass filter Rumble below the speech range carries no information and consumes loudness budget. A high pass is cheap and safe, unlike denoising. Files: `src/clean.py` 2. Apply spectral denoising conservatively The afftdn filter at a light default. Expose the strength but do not raise the default because a test file improved. ```sh ffmpeg -i in.wav -af "highpass=f=80,afftdn=nr=12:nf=-30" out.wav ``` 3. Make A and B comparison one command A command that plays the same passage from source and output back to back. Denoising decisions must be made by ear, not by a number. Files: `src/compare.py` ### Done when - [ ] The noise floor in a silent passage is measurably lower after processing - [ ] Sibilant passages show no watery artefact at the default strength - [ ] The original file is unmodified - [ ] The comparison command plays both versions of the same passage ### Watch out - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. ## Phase 4 · Multiple speakers and multiple files A batch of recordings from different microphones ends up sounding like one conversation. ### Steps 1. Process a directory as a job Enumerate audio files, measure all, then process all. Two passes over the batch, matching the two pass design. Files: `src/batch.py` 2. Handle per-track correction before mixing For multitrack input, normalise each track to the target, then mix with a documented gain reduction to avoid summing above the ceiling. Files: `src/mix.py` 3. Keep failures visible and non-fatal One unreadable file must not abort a batch of forty. Record the error against the file and continue. Files: `src/batch.py` ### Done when - [ ] Five files at different source levels come out within tolerance of each other - [ ] A multitrack conversation has both speakers at comparable loudness - [ ] A corrupt file is reported and does not stop the batch - [ ] The mixed output does not exceed the true peak ceiling ### Watch out - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. ## Phase 5 · Transcript, chapters and the report Each job produces the files and the evidence that they were processed correctly. ### Steps 1. Convert before transcribing Downmix to mono 16 kHz 16 bit WAV for whisper.cpp specifically, from the source rather than the export. ```sh ffmpeg -i out.wav -ar 16000 -ac 1 -c:a pcm_s16le for-whisper.wav ``` 2. Detect chapter boundaries Long silences using ffmpeg's silence detection, or a supplied outline with timestamps. Do not guess from the transcript alone. Files: `src/chapters.py` ```sh ffmpeg -i in.wav -af silencedetect=n=-35dB:d=2 -f null - ``` 3. Write the report JSON and a readable summary. Before and after figures per file, the preset, and every warning. This is what makes the job auditable. Files: `src/report.py` ### Done when - [ ] The report shows before and after figures for every file - [ ] Transcript timestamps line up with the audio - [ ] Chapter marks fall at real boundaries - [ ] Skipping transcription does not slow the audio export ### Watch out - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. ## Not in this build - Adaptive per-recording processing decisions. - Cloud queues and processing that continues when your machine sleeps. - Delivery integrations to podcast hosts. - Licensed music and automatic ducking around it. ## After v1, if you want it - Speaker diarisation to label who is talking in the transcript - A loudness history chart across episodes, which catches drift over a season - Automatic removal of long silences, with the threshold exposed ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23. TARGET_LUFS=-16 # Required. True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format. TRUE_PEAK_DB=-1.5 # Required. Target loudness range in LU. Speech usually sits lower than music. LOUDNESS_RANGE=11 # Required. Noise reduction strength. Low by default; raising it introduces artefacts around consonants. DENOISE_NR=12 # Required. Where processed files and reports are written. Never the source directory. OUTPUT_DIR=./out # Optional. Optional. Leave unset to skip transcription entirely. WHISPER_MODEL_PATH=./models/ggml-base.en.bin
You are building a lean indie version of Auphonic. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Auphonic · indie build A command line and small interface over ffmpeg that measures each file, normalises it to a podcast loudness target with a true peak ceiling, reduces steady noise, evens out speakers recorded at different levels, and exports production ready files with a report showing the measured figures before and after. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg with the loudnorm and afftdn filters | loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one | | Runtime | Python 3.12 driving ffmpeg as a subprocess | the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation | | Measurement | Two pass loudnorm: measure, then apply with linear correction | single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain | | Transcription | whisper.cpp locally, optional | chapter marks and a transcript without sending anyone's recording to a service | | Storage | Original files untouched, outputs written alongside with a JSON report | you must be able to rerun with different settings, which means never overwriting the source | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **ffmpeg with the loudnorm and afftdn filters** · free - Why: It is the entire processing engine. A build without these filters cannot do any of this. - Get it: brew install ffmpeg, your package manager, or a static build from ffmpeg.org. Confirm the filters are present rather than assuming. - Verify: ffmpeg -filters lists loudnorm and afftdn - [ ] **Python 3.12 or newer** · free - Why: Runs the orchestration, measurement parsing and batch logic. - Get it: python.org or your package manager, then a virtual environment in the project directory. - Verify: python3 --version prints 3.12 or higher - [ ] **Three real recordings with different problems** · free - Why: A clean studio file proves nothing. You need one with steady hiss, one with two speakers at clearly different levels, and one already close to target. - Get it: Use your own recordings, or record them deliberately. Keep them as fixtures so a settings change can be compared against the same material. - Verify: All three are saved as fixtures with their initial measurements recorded - [ ] **Headphones or monitors you know well** · free if you already have a pair - Why: Denoising decisions are made by ear, and laptop speakers hide exactly the artefacts you are trying to avoid. - Get it: Any headphones you have listened to enough to know how they sound. Familiarity matters more than price here. - [ ] **whisper.cpp with a model, optional** (optional) · free, but transcription takes real time on your own machine - Why: Phase 5 only, for transcripts and chapter assistance. Everything else works without it. - Get it: Build whisper.cpp and download a model. Model sizes vary from tens of megabytes to several gigabytes, and the larger ones are much slower. - Verify: whisper-cli transcribes a short 16 kHz mono WAV ## Quick start ```sh ffmpeg -i input.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Adaptive per-recording processing decisions. - Cloud queues and processing that continues when your machine sleeps. - Delivery integrations to podcast hosts. - Licensed music and automatic ducking around it. - proprietary adaptive audio processing, cloud queues, and broad format delivery - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support If one of those is essential to you, that is the reason to keep paying for Auphonic, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Auphonic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local batch processor that levels, denoises and exports spoken word audio to a real loudness target to replace Auphonic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Processing: ffmpeg with the loudnorm and afftdn filters · loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one - Runtime: Python 3.12 driving ffmpeg as a subprocess · the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation - Measurement: Two pass loudnorm: measure, then apply with linear correction · single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain - Transcription: whisper.cpp locally, optional · chapter marks and a transcript without sending anyone's recording to a service - Storage: Original files untouched, outputs written alongside with a JSON report · you must be able to rerun with different settings, which means never overwriting the source ### Data model (create this before Phase 1) - jobs(id, created_at, preset_id, state) - files(id, job_id, source_path, output_path, duration_ms, channels, sample_rate) - measurements(file_id, stage, input_i, input_tp, input_lra, input_thresh, target_offset) · stage is before or after - presets(id, name, target_lufs, true_peak_db, denoise_strength, output_format, bitrate) - The measurement rows are the product. A processed file without its before and after figures is a file you cannot defend. ### Phase 1 · Measure before you change anything Build: Run loudnorm in analysis mode and parse its JSON output: integrated loudness, true peak, loudness range and threshold. Store the measurement. Do not modify any audio in this phase. Measuring correctly is what makes every later claim checkable. Trap: Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. Done when: Analysing a file prints integrated loudness in LUFS, true peak in dBTP and loudness range in LU. Analysing a known reference tone produces the expected figure. The source file's bytes are unchanged after analysis, verified by checksum. Do not build yet: No normalisation, no denoising, no batch. One correct measurement. ### Phase 2 · Normalising to a target that is right for speech Build: Apply loudnorm in a second pass using the measured values, with linear correction so a fixed gain is applied rather than dynamic compression. Default to a podcast target of -16 LUFS with a true peak ceiling of -1.5 dBTP, and make both configurable because different destinations want different numbers. Trap: Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. Trap: A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. Done when: A processed file measures within a small tolerance of the configured target, verified by re-running the analysis from Phase 1 on the output. Its loudness range is close to the source's, showing the dynamics were not compressed. A file that was already at target is not audibly altered. Do not build yet: No noise reduction and no batch yet. ### Phase 3 · Noise reduction that does not damage the voice Build: Apply spectral denoising at a configurable strength, defaulting to gentle. Add a high pass to remove rumble below the speech range. Always keep the untouched original and make an A and B comparison easy, because denoising is where people ruin recordings. Trap: Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. Done when: A recording with steady hiss measures a lower noise floor after processing, taken from a silent passage. The speech remains free of the watery artefact at the default strength, confirmed by listening to a passage with sibilance. The original file is still present and unmodified. Do not build yet: No batch and no transcription yet. ### Phase 4 · Multiple speakers and multiple files Build: Process a directory as a job. Normalise each file to the same target so speakers recorded at different levels match. For multitrack recordings of one conversation, measure and correct each track before mixing, not after. Trap: Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. Done when: A job of five files with different source levels produces five outputs measuring within a small tolerance of each other. A multitrack conversation processed per track and then mixed has both speakers at comparable loudness. A failed file does not stop the batch and appears in the report with its error. Do not build yet: No transcription or chapters yet. ### Phase 5 · Transcript, chapters and the report Build: Optionally transcribe locally with whisper.cpp. Generate chapter marks from long silences or from a supplied outline. Write a report showing every file's before and after measurements, the preset used and any warnings. Trap: whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. Done when: A job report lists every file with its before and after integrated loudness and true peak. A transcript is produced for a test recording and its timestamps line up with the audio. Chapter marks land at real boundaries rather than mid sentence. Skipping transcription does not slow the audio export. Do not build yet: Distribution and presets sharing come next. ### Phase 6 · Presets, watch folders and running it for others Build: Named presets committed as files. A watch folder that processes new arrivals automatically. Determinism checks so the same input and preset always produce the same measured output. If others submit files, add authentication and a size limit. Trap: A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing. Done when: Dropping a file into the watch folder produces processed output and a report without intervention. A file still being copied is not processed until the copy completes. Running the same input and preset twice produces outputs with identical measurements. Presets are files under version control. Do not build yet: Nothing after this. ### Out of scope (and why) - The adaptive processing behind the hosted service, which makes per-recording decisions this pipeline makes once per preset. - Cloud queues and the ability to submit an hour of audio and close your laptop. Your processing happens on your machine and takes your machine's time. - Broad delivery integrations to hosting platforms, which are each an API with its own quirks. - Licensed music libraries and the ducking behaviour built around them. ### README must contain - The loudness target and true peak ceiling, and why podcast targets differ from streaming ones - That normalisation is two pass with linear correction, and what single pass would do to the sound - That denoising is destructive and defaults to gentle deliberately - That multitrack input must be corrected per track before mixing - That originals are never modified, and where outputs and reports are written ===== AGENTS.md ===== # Agent instructions · Auphonic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg with the loudnorm and afftdn filters, Python 3.12 driving ffmpeg as a subprocess, Two pass loudnorm: measure, then apply with linear correction, whisper.cpp locally, optional, Original files untouched, outputs written alongside with a JSON report. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. ===== BUILD_PLAN.md ===== # Build plan · Auphonic A command line and small interface over ffmpeg that measures each file, normalises it to a podcast loudness target with a true peak ceiling, reduces steady noise, evens out speakers recorded at different levels, and exports production ready files with a report showing the measured figures before and after. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Measure before you change anything You can state the loudness of a file in the same units everyone else uses. ### Steps 1. Run loudnorm in analysis mode and parse the JSON The filter prints a JSON block on the error stream in print_format=json mode. Parse it rather than scraping the human readable output, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i input.wav -af loudnorm=print_format=json -f null - ``` 2. Store the measurement One row per file per stage. You will compare before and after, and that comparison is the report. Files: `src/db.py` 3. Verify against a known signal Generate a tone at a known level and confirm the measured figure matches. This calibrates your trust in everything downstream. ```sh ffmpeg -f lavfi -i "sine=frequency=1000:duration=10" -af volume=-20dB tone.wav ``` ### Done when - [ ] Analysis prints integrated loudness, true peak and loudness range - [ ] A generated reference tone measures at its expected level - [ ] The source file checksum is unchanged after analysis - [ ] Parsing uses the JSON output rather than the human readable text ### Watch out - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. ## Phase 2 · Normalising to a target that is right for speech Every output lands on the same loudness target without being squashed to get there. ### Steps 1. Feed measured values into the second pass Pass measured_I, measured_TP, measured_LRA and measured_thresh back to loudnorm with linear=true. Without them, the second pass measures again and compresses. Files: `src/normalise.py` ```sh ffmpeg -i in.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-24.1:measured_TP=-3.2:measured_LRA=9.4:measured_thresh=-34.9:linear=true -ar 48000 out.wav ``` 2. Make the target a preset value Podcast, broadcast and streaming destinations differ. Store the target with the preset and record it in the report rather than hard coding one number. Files: `src/presets.py` 3. Verify by re-measuring the output Run Phase 1's analysis on the result. This is a closed loop and it is the whole quality argument. Files: `src/verify.py` ### Done when - [ ] The output measures within tolerance of the configured target - [ ] The output's loudness range is close to the source's rather than much smaller - [ ] A file already at target passes through without audible change - [ ] The applied target is recorded in the report ### Watch out - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. ## Phase 3 · Noise reduction that does not damage the voice Steady background noise is reduced, and the speech still sounds like the person. ### Steps 1. Add a high pass filter Rumble below the speech range carries no information and consumes loudness budget. A high pass is cheap and safe, unlike denoising. Files: `src/clean.py` 2. Apply spectral denoising conservatively The afftdn filter at a light default. Expose the strength but do not raise the default because a test file improved. ```sh ffmpeg -i in.wav -af "highpass=f=80,afftdn=nr=12:nf=-30" out.wav ``` 3. Make A and B comparison one command A command that plays the same passage from source and output back to back. Denoising decisions must be made by ear, not by a number. Files: `src/compare.py` ### Done when - [ ] The noise floor in a silent passage is measurably lower after processing - [ ] Sibilant passages show no watery artefact at the default strength - [ ] The original file is unmodified - [ ] The comparison command plays both versions of the same passage ### Watch out - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. ## Phase 4 · Multiple speakers and multiple files A batch of recordings from different microphones ends up sounding like one conversation. ### Steps 1. Process a directory as a job Enumerate audio files, measure all, then process all. Two passes over the batch, matching the two pass design. Files: `src/batch.py` 2. Handle per-track correction before mixing For multitrack input, normalise each track to the target, then mix with a documented gain reduction to avoid summing above the ceiling. Files: `src/mix.py` 3. Keep failures visible and non-fatal One unreadable file must not abort a batch of forty. Record the error against the file and continue. Files: `src/batch.py` ### Done when - [ ] Five files at different source levels come out within tolerance of each other - [ ] A multitrack conversation has both speakers at comparable loudness - [ ] A corrupt file is reported and does not stop the batch - [ ] The mixed output does not exceed the true peak ceiling ### Watch out - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. ## Phase 5 · Transcript, chapters and the report Each job produces the files and the evidence that they were processed correctly. ### Steps 1. Convert before transcribing Downmix to mono 16 kHz 16 bit WAV for whisper.cpp specifically, from the source rather than the export. ```sh ffmpeg -i out.wav -ar 16000 -ac 1 -c:a pcm_s16le for-whisper.wav ``` 2. Detect chapter boundaries Long silences using ffmpeg's silence detection, or a supplied outline with timestamps. Do not guess from the transcript alone. Files: `src/chapters.py` ```sh ffmpeg -i in.wav -af silencedetect=n=-35dB:d=2 -f null - ``` 3. Write the report JSON and a readable summary. Before and after figures per file, the preset, and every warning. This is what makes the job auditable. Files: `src/report.py` ### Done when - [ ] The report shows before and after figures for every file - [ ] Transcript timestamps line up with the audio - [ ] Chapter marks fall at real boundaries - [ ] Skipping transcription does not slow the audio export ### Watch out - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. ## Not in this build - Adaptive per-recording processing decisions. - Cloud queues and processing that continues when your machine sleeps. - Delivery integrations to podcast hosts. - Licensed music and automatic ducking around it. ## After v1, if you want it - Speaker diarisation to label who is talking in the transcript - A loudness history chart across episodes, which catches drift over a season - Automatic removal of long silences, with the threshold exposed ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23. TARGET_LUFS=-16 # Required. True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format. TRUE_PEAK_DB=-1.5 # Required. Target loudness range in LU. Speech usually sits lower than music. LOUDNESS_RANGE=11 # Required. Noise reduction strength. Low by default; raising it introduces artefacts around consonants. DENOISE_NR=12 # Required. Where processed files and reports are written. Never the source directory. OUTPUT_DIR=./out # Optional. Optional. Leave unset to skip transcription entirely. WHISPER_MODEL_PATH=./models/ggml-base.en.bin
You are building a production product version of Auphonic. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Auphonic · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Auphonic, normalize loudness, reduce noise, and batch-process user-owned audio locally. The hard boundary is proprietary adaptive audio processing, cloud queues, and broad format delivery, plus audio infrastructure, distribution, and production polish. ## Product outcome A processor that produces files at a stated loudness target with headroom against clipping, leaves originals untouched, reports the measured figures before and after, and produces the same result from the same input every time. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - ffmpeg - local audio files - optional transcription API key - sufficient disk space ## Explicit non-goals for v1 - Adaptive per-recording processing decisions. - Cloud queues and processing that continues when your machine sleeps. - Delivery integrations to podcast hosts. - Licensed music and automatic ducking around it. - proprietary adaptive audio processing, cloud queues, and broad format delivery - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support ## Success criteria - Outputs measure within tolerance of the configured target - Loudness range is preserved rather than compressed - The determinism test passes on repeated runs - Source files are confirmed unmodified by checksum - The default denoise strength has been checked by ear on the sibilant fixture ===== BRIEF.md ===== # Build brief · Auphonic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local batch processor that levels, denoises and exports spoken word audio to a real loudness target to replace Auphonic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Processing: ffmpeg with the loudnorm and afftdn filters · loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one - Runtime: Python 3.12 driving ffmpeg as a subprocess · the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation - Measurement: Two pass loudnorm: measure, then apply with linear correction · single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain - Transcription: whisper.cpp locally, optional · chapter marks and a transcript without sending anyone's recording to a service - Storage: Original files untouched, outputs written alongside with a JSON report · you must be able to rerun with different settings, which means never overwriting the source ### Data model (create this before Phase 1) - jobs(id, created_at, preset_id, state) - files(id, job_id, source_path, output_path, duration_ms, channels, sample_rate) - measurements(file_id, stage, input_i, input_tp, input_lra, input_thresh, target_offset) · stage is before or after - presets(id, name, target_lufs, true_peak_db, denoise_strength, output_format, bitrate) - The measurement rows are the product. A processed file without its before and after figures is a file you cannot defend. ### Phase 1 · Measure before you change anything Build: Run loudnorm in analysis mode and parse its JSON output: integrated loudness, true peak, loudness range and threshold. Store the measurement. Do not modify any audio in this phase. Measuring correctly is what makes every later claim checkable. Trap: Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. Done when: Analysing a file prints integrated loudness in LUFS, true peak in dBTP and loudness range in LU. Analysing a known reference tone produces the expected figure. The source file's bytes are unchanged after analysis, verified by checksum. Do not build yet: No normalisation, no denoising, no batch. One correct measurement. ### Phase 2 · Normalising to a target that is right for speech Build: Apply loudnorm in a second pass using the measured values, with linear correction so a fixed gain is applied rather than dynamic compression. Default to a podcast target of -16 LUFS with a true peak ceiling of -1.5 dBTP, and make both configurable because different destinations want different numbers. Trap: Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. Trap: A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. Done when: A processed file measures within a small tolerance of the configured target, verified by re-running the analysis from Phase 1 on the output. Its loudness range is close to the source's, showing the dynamics were not compressed. A file that was already at target is not audibly altered. Do not build yet: No noise reduction and no batch yet. ### Phase 3 · Noise reduction that does not damage the voice Build: Apply spectral denoising at a configurable strength, defaulting to gentle. Add a high pass to remove rumble below the speech range. Always keep the untouched original and make an A and B comparison easy, because denoising is where people ruin recordings. Trap: Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. Done when: A recording with steady hiss measures a lower noise floor after processing, taken from a silent passage. The speech remains free of the watery artefact at the default strength, confirmed by listening to a passage with sibilance. The original file is still present and unmodified. Do not build yet: No batch and no transcription yet. ### Phase 4 · Multiple speakers and multiple files Build: Process a directory as a job. Normalise each file to the same target so speakers recorded at different levels match. For multitrack recordings of one conversation, measure and correct each track before mixing, not after. Trap: Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. Done when: A job of five files with different source levels produces five outputs measuring within a small tolerance of each other. A multitrack conversation processed per track and then mixed has both speakers at comparable loudness. A failed file does not stop the batch and appears in the report with its error. Do not build yet: No transcription or chapters yet. ### Phase 5 · Transcript, chapters and the report Build: Optionally transcribe locally with whisper.cpp. Generate chapter marks from long silences or from a supplied outline. Write a report showing every file's before and after measurements, the preset used and any warnings. Trap: whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. Done when: A job report lists every file with its before and after integrated loudness and true peak. A transcript is produced for a test recording and its timestamps line up with the audio. Chapter marks land at real boundaries rather than mid sentence. Skipping transcription does not slow the audio export. Do not build yet: Distribution and presets sharing come next. ### Phase 6 · Presets, watch folders and running it for others Build: Named presets committed as files. A watch folder that processes new arrivals automatically. Determinism checks so the same input and preset always produce the same measured output. If others submit files, add authentication and a size limit. Trap: A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing. Done when: Dropping a file into the watch folder produces processed output and a report without intervention. A file still being copied is not processed until the copy completes. Running the same input and preset twice produces outputs with identical measurements. Presets are files under version control. Do not build yet: Nothing after this. ### Out of scope (and why) - The adaptive processing behind the hosted service, which makes per-recording decisions this pipeline makes once per preset. - Cloud queues and the ability to submit an hour of audio and close your laptop. Your processing happens on your machine and takes your machine's time. - Broad delivery integrations to hosting platforms, which are each an API with its own quirks. - Licensed music libraries and the ducking behaviour built around them. ### README must contain - The loudness target and true peak ceiling, and why podcast targets differ from streaming ones - That normalisation is two pass with linear correction, and what single pass would do to the sound - That denoising is destructive and defaults to gentle deliberately - That multitrack input must be corrected per track before mixing - That originals are never modified, and where outputs and reports are written ===== ARCHITECTURE.md ===== # Architecture · Auphonic ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg with the loudnorm and afftdn filters | loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one | | Runtime | Python 3.12 driving ffmpeg as a subprocess | the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation | | Measurement | Two pass loudnorm: measure, then apply with linear correction | single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain | | Transcription | whisper.cpp locally, optional | chapter marks and a transcript without sending anyone's recording to a service | | Storage | Original files untouched, outputs written alongside with a JSON report | you must be able to rerun with different settings, which means never overwriting the source | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Measure | Loudness analysis and parsing. | ffmpeg's loudnorm implements the standard. Any replacement must produce the same units, because the report is the product. | | Normalise | Two pass gain correction. | The two pass with linear correction is the quality decision. A swap that reverts to single pass is a downgrade regardless of what else it improves. | | Clean | High pass and denoising. | Any denoiser fits behind the same interface. Keep the conservative default whatever the implementation. | | Batch | Jobs, per-file failure isolation and reporting. | Straightforward orchestration. The rule that one bad file does not stop a batch is the part that matters. | | Transcribe | Optional local transcription and chapters. | Fully optional and off the critical path. A hosted transcription service would work, at the cost of sending recordings elsewhere. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TARGET_LUFS` · required · Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23. - `TRUE_PEAK_DB` · required · True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format. - `LOUDNESS_RANGE` · required · Target loudness range in LU. Speech usually sits lower than music. - `DENOISE_NR` · required · Noise reduction strength. Low by default; raising it introduces artefacts around consonants. - `OUTPUT_DIR` · required · Where processed files and reports are written. Never the source directory. - `WHISPER_MODEL_PATH` · optional · Optional. Leave unset to skip transcription entirely. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Auphonic product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg with the loudnorm and afftdn filters, Python 3.12 driving ffmpeg as a subprocess, Two pass loudnorm: measure, then apply with linear correction, whisper.cpp locally, optional, Original files untouched, outputs written alongside with a JSON report. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. - A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing. ===== MILESTONES.md ===== # Delivery milestones · Auphonic Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Measure before you change anything You can state the loudness of a file in the same units everyone else uses. ### Steps 1. Run loudnorm in analysis mode and parse the JSON The filter prints a JSON block on the error stream in print_format=json mode. Parse it rather than scraping the human readable output, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i input.wav -af loudnorm=print_format=json -f null - ``` 2. Store the measurement One row per file per stage. You will compare before and after, and that comparison is the report. Files: `src/db.py` 3. Verify against a known signal Generate a tone at a known level and confirm the measured figure matches. This calibrates your trust in everything downstream. ```sh ffmpeg -f lavfi -i "sine=frequency=1000:duration=10" -af volume=-20dB tone.wav ``` ### Done when - [ ] Analysis prints integrated loudness, true peak and loudness range - [ ] A generated reference tone measures at its expected level - [ ] The source file checksum is unchanged after analysis - [ ] Parsing uses the JSON output rather than the human readable text ### Watch out - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. ## M2 · Normalising to a target that is right for speech Every output lands on the same loudness target without being squashed to get there. ### Steps 1. Feed measured values into the second pass Pass measured_I, measured_TP, measured_LRA and measured_thresh back to loudnorm with linear=true. Without them, the second pass measures again and compresses. Files: `src/normalise.py` ```sh ffmpeg -i in.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-24.1:measured_TP=-3.2:measured_LRA=9.4:measured_thresh=-34.9:linear=true -ar 48000 out.wav ``` 2. Make the target a preset value Podcast, broadcast and streaming destinations differ. Store the target with the preset and record it in the report rather than hard coding one number. Files: `src/presets.py` 3. Verify by re-measuring the output Run Phase 1's analysis on the result. This is a closed loop and it is the whole quality argument. Files: `src/verify.py` ### Done when - [ ] The output measures within tolerance of the configured target - [ ] The output's loudness range is close to the source's rather than much smaller - [ ] A file already at target passes through without audible change - [ ] The applied target is recorded in the report ### Watch out - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. ## M3 · Noise reduction that does not damage the voice Steady background noise is reduced, and the speech still sounds like the person. ### Steps 1. Add a high pass filter Rumble below the speech range carries no information and consumes loudness budget. A high pass is cheap and safe, unlike denoising. Files: `src/clean.py` 2. Apply spectral denoising conservatively The afftdn filter at a light default. Expose the strength but do not raise the default because a test file improved. ```sh ffmpeg -i in.wav -af "highpass=f=80,afftdn=nr=12:nf=-30" out.wav ``` 3. Make A and B comparison one command A command that plays the same passage from source and output back to back. Denoising decisions must be made by ear, not by a number. Files: `src/compare.py` ### Done when - [ ] The noise floor in a silent passage is measurably lower after processing - [ ] Sibilant passages show no watery artefact at the default strength - [ ] The original file is unmodified - [ ] The comparison command plays both versions of the same passage ### Watch out - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. ## M4 · Multiple speakers and multiple files A batch of recordings from different microphones ends up sounding like one conversation. ### Steps 1. Process a directory as a job Enumerate audio files, measure all, then process all. Two passes over the batch, matching the two pass design. Files: `src/batch.py` 2. Handle per-track correction before mixing For multitrack input, normalise each track to the target, then mix with a documented gain reduction to avoid summing above the ceiling. Files: `src/mix.py` 3. Keep failures visible and non-fatal One unreadable file must not abort a batch of forty. Record the error against the file and continue. Files: `src/batch.py` ### Done when - [ ] Five files at different source levels come out within tolerance of each other - [ ] A multitrack conversation has both speakers at comparable loudness - [ ] A corrupt file is reported and does not stop the batch - [ ] The mixed output does not exceed the true peak ceiling ### Watch out - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. ## M5 · Transcript, chapters and the report Each job produces the files and the evidence that they were processed correctly. ### Steps 1. Convert before transcribing Downmix to mono 16 kHz 16 bit WAV for whisper.cpp specifically, from the source rather than the export. ```sh ffmpeg -i out.wav -ar 16000 -ac 1 -c:a pcm_s16le for-whisper.wav ``` 2. Detect chapter boundaries Long silences using ffmpeg's silence detection, or a supplied outline with timestamps. Do not guess from the transcript alone. Files: `src/chapters.py` ```sh ffmpeg -i in.wav -af silencedetect=n=-35dB:d=2 -f null - ``` 3. Write the report JSON and a readable summary. Before and after figures per file, the preset, and every warning. This is what makes the job auditable. Files: `src/report.py` ### Done when - [ ] The report shows before and after figures for every file - [ ] Transcript timestamps line up with the audio - [ ] Chapter marks fall at real boundaries - [ ] Skipping transcription does not slow the audio export ### Watch out - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. ## M6 · Presets, watch folders and running it for others (production only) It runs unattended on a schedule and produces the same result every time. ### Steps 1. Make presets files Version controlled rather than stored in a database, so a change is reviewable and a past job's preset is recoverable. Files: `presets/podcast.json` 2. Add a watch folder with a settle check Wait until the file size has been stable for a few seconds before touching it. Files: `src/watch.py` 3. Assert determinism Process the same input twice and compare measurements. A difference means something in the pipeline is not being fed the measured values properly. Files: `tests/test_determinism.py` ### Done when - [ ] A dropped file is processed automatically with a report - [ ] A file still being copied is not processed early - [ ] The same input and preset produce identical measurements twice - [ ] Presets are version controlled files ### Watch out - A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing. ===== OPERATIONS.md ===== # Operations · Auphonic ## Backup Source recordings are the irreplaceable data and belong in an ordinary backup. Outputs and reports are regenerable from sources and presets, which is why presets are version controlled. ## Restore Restore the sources, check out the presets at the right revision, and rerun. Determinism is what makes this a real restore rather than an approximation. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For unattended operation, watch the job failure rate and the spread of output measurements. Outputs drifting from target usually means the second pass is not receiving measured values. ## Incident checklist A batch processed with the wrong preset is recoverable because sources are untouched: correct the preset and rerun. This is the reason for the never-overwrite rule and it will justify itself at least once. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Outputs measure within tolerance of the configured target - [ ] Loudness range is preserved rather than compressed - [ ] The determinism test passes on repeated runs - [ ] Source files are confirmed unmodified by checksum - [ ] The default denoise strength has been checked by ear on the sibilant fixture ## Launch constraint Do not market omitted Auphonic capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23. TARGET_LUFS=-16 # Required. True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format. TRUE_PEAK_DB=-1.5 # Required. Target loudness range in LU. Speech usually sits lower than music. LOUDNESS_RANGE=11 # Required. Noise reduction strength. Low by default; raising it introduces artefacts around consonants. DENOISE_NR=12 # Required. Where processed files and reports are written. Never the source directory. OUTPUT_DIR=./out # Optional. Optional. Leave unset to skip transcription entirely. WHISPER_MODEL_PATH=./models/ggml-base.en.bin
# Auphonic · indie build A command line and small interface over ffmpeg that measures each file, normalises it to a podcast loudness target with a true peak ceiling, reduces steady noise, evens out speakers recorded at different levels, and exports production ready files with a report showing the measured figures before and after. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg with the loudnorm and afftdn filters | loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one | | Runtime | Python 3.12 driving ffmpeg as a subprocess | the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation | | Measurement | Two pass loudnorm: measure, then apply with linear correction | single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain | | Transcription | whisper.cpp locally, optional | chapter marks and a transcript without sending anyone's recording to a service | | Storage | Original files untouched, outputs written alongside with a JSON report | you must be able to rerun with different settings, which means never overwriting the source | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **ffmpeg with the loudnorm and afftdn filters** · free - Why: It is the entire processing engine. A build without these filters cannot do any of this. - Get it: brew install ffmpeg, your package manager, or a static build from ffmpeg.org. Confirm the filters are present rather than assuming. - Verify: ffmpeg -filters lists loudnorm and afftdn - [ ] **Python 3.12 or newer** · free - Why: Runs the orchestration, measurement parsing and batch logic. - Get it: python.org or your package manager, then a virtual environment in the project directory. - Verify: python3 --version prints 3.12 or higher - [ ] **Three real recordings with different problems** · free - Why: A clean studio file proves nothing. You need one with steady hiss, one with two speakers at clearly different levels, and one already close to target. - Get it: Use your own recordings, or record them deliberately. Keep them as fixtures so a settings change can be compared against the same material. - Verify: All three are saved as fixtures with their initial measurements recorded - [ ] **Headphones or monitors you know well** · free if you already have a pair - Why: Denoising decisions are made by ear, and laptop speakers hide exactly the artefacts you are trying to avoid. - Get it: Any headphones you have listened to enough to know how they sound. Familiarity matters more than price here. - [ ] **whisper.cpp with a model, optional** (optional) · free, but transcription takes real time on your own machine - Why: Phase 5 only, for transcripts and chapter assistance. Everything else works without it. - Get it: Build whisper.cpp and download a model. Model sizes vary from tens of megabytes to several gigabytes, and the larger ones are much slower. - Verify: whisper-cli transcribes a short 16 kHz mono WAV ## Quick start ```sh ffmpeg -i input.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Adaptive per-recording processing decisions. - Cloud queues and processing that continues when your machine sleeps. - Delivery integrations to podcast hosts. - Licensed music and automatic ducking around it. - proprietary adaptive audio processing, cloud queues, and broad format delivery - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support If one of those is essential to you, that is the reason to keep paying for Auphonic, and the README should say so rather than pretend.
# Build brief · Auphonic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local batch processor that levels, denoises and exports spoken word audio to a real loudness target to replace Auphonic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Processing: ffmpeg with the loudnorm and afftdn filters · loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one - Runtime: Python 3.12 driving ffmpeg as a subprocess · the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation - Measurement: Two pass loudnorm: measure, then apply with linear correction · single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain - Transcription: whisper.cpp locally, optional · chapter marks and a transcript without sending anyone's recording to a service - Storage: Original files untouched, outputs written alongside with a JSON report · you must be able to rerun with different settings, which means never overwriting the source ### Data model (create this before Phase 1) - jobs(id, created_at, preset_id, state) - files(id, job_id, source_path, output_path, duration_ms, channels, sample_rate) - measurements(file_id, stage, input_i, input_tp, input_lra, input_thresh, target_offset) · stage is before or after - presets(id, name, target_lufs, true_peak_db, denoise_strength, output_format, bitrate) - The measurement rows are the product. A processed file without its before and after figures is a file you cannot defend. ### Phase 1 · Measure before you change anything Build: Run loudnorm in analysis mode and parse its JSON output: integrated loudness, true peak, loudness range and threshold. Store the measurement. Do not modify any audio in this phase. Measuring correctly is what makes every later claim checkable. Trap: Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. Done when: Analysing a file prints integrated loudness in LUFS, true peak in dBTP and loudness range in LU. Analysing a known reference tone produces the expected figure. The source file's bytes are unchanged after analysis, verified by checksum. Do not build yet: No normalisation, no denoising, no batch. One correct measurement. ### Phase 2 · Normalising to a target that is right for speech Build: Apply loudnorm in a second pass using the measured values, with linear correction so a fixed gain is applied rather than dynamic compression. Default to a podcast target of -16 LUFS with a true peak ceiling of -1.5 dBTP, and make both configurable because different destinations want different numbers. Trap: Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. Trap: A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. Done when: A processed file measures within a small tolerance of the configured target, verified by re-running the analysis from Phase 1 on the output. Its loudness range is close to the source's, showing the dynamics were not compressed. A file that was already at target is not audibly altered. Do not build yet: No noise reduction and no batch yet. ### Phase 3 · Noise reduction that does not damage the voice Build: Apply spectral denoising at a configurable strength, defaulting to gentle. Add a high pass to remove rumble below the speech range. Always keep the untouched original and make an A and B comparison easy, because denoising is where people ruin recordings. Trap: Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. Done when: A recording with steady hiss measures a lower noise floor after processing, taken from a silent passage. The speech remains free of the watery artefact at the default strength, confirmed by listening to a passage with sibilance. The original file is still present and unmodified. Do not build yet: No batch and no transcription yet. ### Phase 4 · Multiple speakers and multiple files Build: Process a directory as a job. Normalise each file to the same target so speakers recorded at different levels match. For multitrack recordings of one conversation, measure and correct each track before mixing, not after. Trap: Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. Done when: A job of five files with different source levels produces five outputs measuring within a small tolerance of each other. A multitrack conversation processed per track and then mixed has both speakers at comparable loudness. A failed file does not stop the batch and appears in the report with its error. Do not build yet: No transcription or chapters yet. ### Phase 5 · Transcript, chapters and the report Build: Optionally transcribe locally with whisper.cpp. Generate chapter marks from long silences or from a supplied outline. Write a report showing every file's before and after measurements, the preset used and any warnings. Trap: whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. Done when: A job report lists every file with its before and after integrated loudness and true peak. A transcript is produced for a test recording and its timestamps line up with the audio. Chapter marks land at real boundaries rather than mid sentence. Skipping transcription does not slow the audio export. Do not build yet: Distribution and presets sharing come next. ### Phase 6 · Presets, watch folders and running it for others Build: Named presets committed as files. A watch folder that processes new arrivals automatically. Determinism checks so the same input and preset always produce the same measured output. If others submit files, add authentication and a size limit. Trap: A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing. Done when: Dropping a file into the watch folder produces processed output and a report without intervention. A file still being copied is not processed until the copy completes. Running the same input and preset twice produces outputs with identical measurements. Presets are files under version control. Do not build yet: Nothing after this. ### Out of scope (and why) - The adaptive processing behind the hosted service, which makes per-recording decisions this pipeline makes once per preset. - Cloud queues and the ability to submit an hour of audio and close your laptop. Your processing happens on your machine and takes your machine's time. - Broad delivery integrations to hosting platforms, which are each an API with its own quirks. - Licensed music libraries and the ducking behaviour built around them. ### README must contain - The loudness target and true peak ceiling, and why podcast targets differ from streaming ones - That normalisation is two pass with linear correction, and what single pass would do to the sound - That denoising is destructive and defaults to gentle deliberately - That multitrack input must be corrected per track before mixing - That originals are never modified, and where outputs and reports are written
# Agent instructions · Auphonic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg with the loudnorm and afftdn filters, Python 3.12 driving ffmpeg as a subprocess, Two pass loudnorm: measure, then apply with linear correction, whisper.cpp locally, optional, Original files untouched, outputs written alongside with a JSON report. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported.
# Build plan · Auphonic A command line and small interface over ffmpeg that measures each file, normalises it to a podcast loudness target with a true peak ceiling, reduces steady noise, evens out speakers recorded at different levels, and exports production ready files with a report showing the measured figures before and after. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Measure before you change anything You can state the loudness of a file in the same units everyone else uses. ### Steps 1. Run loudnorm in analysis mode and parse the JSON The filter prints a JSON block on the error stream in print_format=json mode. Parse it rather than scraping the human readable output, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i input.wav -af loudnorm=print_format=json -f null - ``` 2. Store the measurement One row per file per stage. You will compare before and after, and that comparison is the report. Files: `src/db.py` 3. Verify against a known signal Generate a tone at a known level and confirm the measured figure matches. This calibrates your trust in everything downstream. ```sh ffmpeg -f lavfi -i "sine=frequency=1000:duration=10" -af volume=-20dB tone.wav ``` ### Done when - [ ] Analysis prints integrated loudness, true peak and loudness range - [ ] A generated reference tone measures at its expected level - [ ] The source file checksum is unchanged after analysis - [ ] Parsing uses the JSON output rather than the human readable text ### Watch out - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. ## Phase 2 · Normalising to a target that is right for speech Every output lands on the same loudness target without being squashed to get there. ### Steps 1. Feed measured values into the second pass Pass measured_I, measured_TP, measured_LRA and measured_thresh back to loudnorm with linear=true. Without them, the second pass measures again and compresses. Files: `src/normalise.py` ```sh ffmpeg -i in.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-24.1:measured_TP=-3.2:measured_LRA=9.4:measured_thresh=-34.9:linear=true -ar 48000 out.wav ``` 2. Make the target a preset value Podcast, broadcast and streaming destinations differ. Store the target with the preset and record it in the report rather than hard coding one number. Files: `src/presets.py` 3. Verify by re-measuring the output Run Phase 1's analysis on the result. This is a closed loop and it is the whole quality argument. Files: `src/verify.py` ### Done when - [ ] The output measures within tolerance of the configured target - [ ] The output's loudness range is close to the source's rather than much smaller - [ ] A file already at target passes through without audible change - [ ] The applied target is recorded in the report ### Watch out - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. ## Phase 3 · Noise reduction that does not damage the voice Steady background noise is reduced, and the speech still sounds like the person. ### Steps 1. Add a high pass filter Rumble below the speech range carries no information and consumes loudness budget. A high pass is cheap and safe, unlike denoising. Files: `src/clean.py` 2. Apply spectral denoising conservatively The afftdn filter at a light default. Expose the strength but do not raise the default because a test file improved. ```sh ffmpeg -i in.wav -af "highpass=f=80,afftdn=nr=12:nf=-30" out.wav ``` 3. Make A and B comparison one command A command that plays the same passage from source and output back to back. Denoising decisions must be made by ear, not by a number. Files: `src/compare.py` ### Done when - [ ] The noise floor in a silent passage is measurably lower after processing - [ ] Sibilant passages show no watery artefact at the default strength - [ ] The original file is unmodified - [ ] The comparison command plays both versions of the same passage ### Watch out - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. ## Phase 4 · Multiple speakers and multiple files A batch of recordings from different microphones ends up sounding like one conversation. ### Steps 1. Process a directory as a job Enumerate audio files, measure all, then process all. Two passes over the batch, matching the two pass design. Files: `src/batch.py` 2. Handle per-track correction before mixing For multitrack input, normalise each track to the target, then mix with a documented gain reduction to avoid summing above the ceiling. Files: `src/mix.py` 3. Keep failures visible and non-fatal One unreadable file must not abort a batch of forty. Record the error against the file and continue. Files: `src/batch.py` ### Done when - [ ] Five files at different source levels come out within tolerance of each other - [ ] A multitrack conversation has both speakers at comparable loudness - [ ] A corrupt file is reported and does not stop the batch - [ ] The mixed output does not exceed the true peak ceiling ### Watch out - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. ## Phase 5 · Transcript, chapters and the report Each job produces the files and the evidence that they were processed correctly. ### Steps 1. Convert before transcribing Downmix to mono 16 kHz 16 bit WAV for whisper.cpp specifically, from the source rather than the export. ```sh ffmpeg -i out.wav -ar 16000 -ac 1 -c:a pcm_s16le for-whisper.wav ``` 2. Detect chapter boundaries Long silences using ffmpeg's silence detection, or a supplied outline with timestamps. Do not guess from the transcript alone. Files: `src/chapters.py` ```sh ffmpeg -i in.wav -af silencedetect=n=-35dB:d=2 -f null - ``` 3. Write the report JSON and a readable summary. Before and after figures per file, the preset, and every warning. This is what makes the job auditable. Files: `src/report.py` ### Done when - [ ] The report shows before and after figures for every file - [ ] Transcript timestamps line up with the audio - [ ] Chapter marks fall at real boundaries - [ ] Skipping transcription does not slow the audio export ### Watch out - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. ## Not in this build - Adaptive per-recording processing decisions. - Cloud queues and processing that continues when your machine sleeps. - Delivery integrations to podcast hosts. - Licensed music and automatic ducking around it. ## After v1, if you want it - Speaker diarisation to label who is talking in the transcript - A loudness history chart across episodes, which catches drift over a season - Automatic removal of long silences, with the threshold exposed
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23. TARGET_LUFS=-16 # Required. True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format. TRUE_PEAK_DB=-1.5 # Required. Target loudness range in LU. Speech usually sits lower than music. LOUDNESS_RANGE=11 # Required. Noise reduction strength. Low by default; raising it introduces artefacts around consonants. DENOISE_NR=12 # Required. Where processed files and reports are written. Never the source directory. OUTPUT_DIR=./out # Optional. Optional. Leave unset to skip transcription entirely. WHISPER_MODEL_PATH=./models/ggml-base.en.bin
# Auphonic · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Auphonic, normalize loudness, reduce noise, and batch-process user-owned audio locally. The hard boundary is proprietary adaptive audio processing, cloud queues, and broad format delivery, plus audio infrastructure, distribution, and production polish. ## Product outcome A processor that produces files at a stated loudness target with headroom against clipping, leaves originals untouched, reports the measured figures before and after, and produces the same result from the same input every time. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - ffmpeg - local audio files - optional transcription API key - sufficient disk space ## Explicit non-goals for v1 - Adaptive per-recording processing decisions. - Cloud queues and processing that continues when your machine sleeps. - Delivery integrations to podcast hosts. - Licensed music and automatic ducking around it. - proprietary adaptive audio processing, cloud queues, and broad format delivery - remote studio reliability - licensed music libraries - hosting distribution - advanced mastering and support ## Success criteria - Outputs measure within tolerance of the configured target - Loudness range is preserved rather than compressed - The determinism test passes on repeated runs - Source files are confirmed unmodified by checksum - The default denoise strength has been checked by ear on the sibilant fixture
# Build brief · Auphonic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local batch processor that levels, denoises and exports spoken word audio to a real loudness target to replace Auphonic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Processing: ffmpeg with the loudnorm and afftdn filters · loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one - Runtime: Python 3.12 driving ffmpeg as a subprocess · the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation - Measurement: Two pass loudnorm: measure, then apply with linear correction · single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain - Transcription: whisper.cpp locally, optional · chapter marks and a transcript without sending anyone's recording to a service - Storage: Original files untouched, outputs written alongside with a JSON report · you must be able to rerun with different settings, which means never overwriting the source ### Data model (create this before Phase 1) - jobs(id, created_at, preset_id, state) - files(id, job_id, source_path, output_path, duration_ms, channels, sample_rate) - measurements(file_id, stage, input_i, input_tp, input_lra, input_thresh, target_offset) · stage is before or after - presets(id, name, target_lufs, true_peak_db, denoise_strength, output_format, bitrate) - The measurement rows are the product. A processed file without its before and after figures is a file you cannot defend. ### Phase 1 · Measure before you change anything Build: Run loudnorm in analysis mode and parse its JSON output: integrated loudness, true peak, loudness range and threshold. Store the measurement. Do not modify any audio in this phase. Measuring correctly is what makes every later claim checkable. Trap: Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. Done when: Analysing a file prints integrated loudness in LUFS, true peak in dBTP and loudness range in LU. Analysing a known reference tone produces the expected figure. The source file's bytes are unchanged after analysis, verified by checksum. Do not build yet: No normalisation, no denoising, no batch. One correct measurement. ### Phase 2 · Normalising to a target that is right for speech Build: Apply loudnorm in a second pass using the measured values, with linear correction so a fixed gain is applied rather than dynamic compression. Default to a podcast target of -16 LUFS with a true peak ceiling of -1.5 dBTP, and make both configurable because different destinations want different numbers. Trap: Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. Trap: A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. Done when: A processed file measures within a small tolerance of the configured target, verified by re-running the analysis from Phase 1 on the output. Its loudness range is close to the source's, showing the dynamics were not compressed. A file that was already at target is not audibly altered. Do not build yet: No noise reduction and no batch yet. ### Phase 3 · Noise reduction that does not damage the voice Build: Apply spectral denoising at a configurable strength, defaulting to gentle. Add a high pass to remove rumble below the speech range. Always keep the untouched original and make an A and B comparison easy, because denoising is where people ruin recordings. Trap: Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. Done when: A recording with steady hiss measures a lower noise floor after processing, taken from a silent passage. The speech remains free of the watery artefact at the default strength, confirmed by listening to a passage with sibilance. The original file is still present and unmodified. Do not build yet: No batch and no transcription yet. ### Phase 4 · Multiple speakers and multiple files Build: Process a directory as a job. Normalise each file to the same target so speakers recorded at different levels match. For multitrack recordings of one conversation, measure and correct each track before mixing, not after. Trap: Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. Done when: A job of five files with different source levels produces five outputs measuring within a small tolerance of each other. A multitrack conversation processed per track and then mixed has both speakers at comparable loudness. A failed file does not stop the batch and appears in the report with its error. Do not build yet: No transcription or chapters yet. ### Phase 5 · Transcript, chapters and the report Build: Optionally transcribe locally with whisper.cpp. Generate chapter marks from long silences or from a supplied outline. Write a report showing every file's before and after measurements, the preset used and any warnings. Trap: whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. Done when: A job report lists every file with its before and after integrated loudness and true peak. A transcript is produced for a test recording and its timestamps line up with the audio. Chapter marks land at real boundaries rather than mid sentence. Skipping transcription does not slow the audio export. Do not build yet: Distribution and presets sharing come next. ### Phase 6 · Presets, watch folders and running it for others Build: Named presets committed as files. A watch folder that processes new arrivals automatically. Determinism checks so the same input and preset always produce the same measured output. If others submit files, add authentication and a size limit. Trap: A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing. Done when: Dropping a file into the watch folder produces processed output and a report without intervention. A file still being copied is not processed until the copy completes. Running the same input and preset twice produces outputs with identical measurements. Presets are files under version control. Do not build yet: Nothing after this. ### Out of scope (and why) - The adaptive processing behind the hosted service, which makes per-recording decisions this pipeline makes once per preset. - Cloud queues and the ability to submit an hour of audio and close your laptop. Your processing happens on your machine and takes your machine's time. - Broad delivery integrations to hosting platforms, which are each an API with its own quirks. - Licensed music libraries and the ducking behaviour built around them. ### README must contain - The loudness target and true peak ceiling, and why podcast targets differ from streaming ones - That normalisation is two pass with linear correction, and what single pass would do to the sound - That denoising is destructive and defaults to gentle deliberately - That multitrack input must be corrected per track before mixing - That originals are never modified, and where outputs and reports are written
# Architecture · Auphonic ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg with the loudnorm and afftdn filters | loudnorm implements EBU R128 measurement and true peak limiting, which is the actual standard rather than a guess at one | | Runtime | Python 3.12 driving ffmpeg as a subprocess | the work is orchestration, measurement parsing and batch bookkeeping, not sample manipulation | | Measurement | Two pass loudnorm: measure, then apply with linear correction | single pass loudnorm compresses dynamics to hit the target and sounds worse; two pass applies a fixed gain | | Transcription | whisper.cpp locally, optional | chapter marks and a transcript without sending anyone's recording to a service | | Storage | Original files untouched, outputs written alongside with a JSON report | you must be able to rerun with different settings, which means never overwriting the source | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Measure | Loudness analysis and parsing. | ffmpeg's loudnorm implements the standard. Any replacement must produce the same units, because the report is the product. | | Normalise | Two pass gain correction. | The two pass with linear correction is the quality decision. A swap that reverts to single pass is a downgrade regardless of what else it improves. | | Clean | High pass and denoising. | Any denoiser fits behind the same interface. Keep the conservative default whatever the implementation. | | Batch | Jobs, per-file failure isolation and reporting. | Straightforward orchestration. The rule that one bad file does not stop a batch is the part that matters. | | Transcribe | Optional local transcription and chapters. | Fully optional and off the critical path. A hosted transcription service would work, at the cost of sending recordings elsewhere. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TARGET_LUFS` · required · Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23. - `TRUE_PEAK_DB` · required · True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format. - `LOUDNESS_RANGE` · required · Target loudness range in LU. Speech usually sits lower than music. - `DENOISE_NR` · required · Noise reduction strength. Low by default; raising it introduces artefacts around consonants. - `OUTPUT_DIR` · required · Where processed files and reports are written. Never the source directory. - `WHISPER_MODEL_PATH` · optional · Optional. Leave unset to skip transcription entirely. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Auphonic product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg with the loudnorm and afftdn filters, Python 3.12 driving ffmpeg as a subprocess, Two pass loudnorm: measure, then apply with linear correction, whisper.cpp locally, optional, Original files untouched, outputs written alongside with a JSON report. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. - A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing.
# Delivery milestones · Auphonic Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Measure before you change anything You can state the loudness of a file in the same units everyone else uses. ### Steps 1. Run loudnorm in analysis mode and parse the JSON The filter prints a JSON block on the error stream in print_format=json mode. Parse it rather than scraping the human readable output, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i input.wav -af loudnorm=print_format=json -f null - ``` 2. Store the measurement One row per file per stage. You will compare before and after, and that comparison is the report. Files: `src/db.py` 3. Verify against a known signal Generate a tone at a known level and confirm the measured figure matches. This calibrates your trust in everything downstream. ```sh ffmpeg -f lavfi -i "sine=frequency=1000:duration=10" -af volume=-20dB tone.wav ``` ### Done when - [ ] Analysis prints integrated loudness, true peak and loudness range - [ ] A generated reference tone measures at its expected level - [ ] The source file checksum is unchanged after analysis - [ ] Parsing uses the JSON output rather than the human readable text ### Watch out - Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak. ## M2 · Normalising to a target that is right for speech Every output lands on the same loudness target without being squashed to get there. ### Steps 1. Feed measured values into the second pass Pass measured_I, measured_TP, measured_LRA and measured_thresh back to loudnorm with linear=true. Without them, the second pass measures again and compresses. Files: `src/normalise.py` ```sh ffmpeg -i in.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-24.1:measured_TP=-3.2:measured_LRA=9.4:measured_thresh=-34.9:linear=true -ar 48000 out.wav ``` 2. Make the target a preset value Podcast, broadcast and streaming destinations differ. Store the target with the preset and record it in the report rather than hard coding one number. Files: `src/presets.py` 3. Verify by re-measuring the output Run Phase 1's analysis on the result. This is a closed loop and it is the whole quality argument. Files: `src/verify.py` ### Done when - [ ] The output measures within tolerance of the configured target - [ ] The output's loudness range is close to the source's rather than much smaller - [ ] A file already at target passes through without audible change - [ ] The applied target is recorded in the report ### Watch out - Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level. - A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason. ## M3 · Noise reduction that does not damage the voice Steady background noise is reduced, and the speech still sounds like the person. ### Steps 1. Add a high pass filter Rumble below the speech range carries no information and consumes loudness budget. A high pass is cheap and safe, unlike denoising. Files: `src/clean.py` 2. Apply spectral denoising conservatively The afftdn filter at a light default. Expose the strength but do not raise the default because a test file improved. ```sh ffmpeg -i in.wav -af "highpass=f=80,afftdn=nr=12:nf=-30" out.wav ``` 3. Make A and B comparison one command A command that plays the same passage from source and output back to back. Denoising decisions must be made by ear, not by a number. Files: `src/compare.py` ### Done when - [ ] The noise floor in a silent passage is measurably lower after processing - [ ] Sibilant passages show no watery artefact at the default strength - [ ] The original file is unmodified - [ ] The comparison command plays both versions of the same passage ### Watch out - Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further. ## M4 · Multiple speakers and multiple files A batch of recordings from different microphones ends up sounding like one conversation. ### Steps 1. Process a directory as a job Enumerate audio files, measure all, then process all. Two passes over the batch, matching the two pass design. Files: `src/batch.py` 2. Handle per-track correction before mixing For multitrack input, normalise each track to the target, then mix with a documented gain reduction to avoid summing above the ceiling. Files: `src/mix.py` 3. Keep failures visible and non-fatal One unreadable file must not abort a batch of forty. Record the error against the file and continue. Files: `src/batch.py` ### Done when - [ ] Five files at different source levels come out within tolerance of each other - [ ] A multitrack conversation has both speakers at comparable loudness - [ ] A corrupt file is reported and does not stop the batch - [ ] The mixed output does not exceed the true peak ceiling ### Watch out - Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent. ## M5 · Transcript, chapters and the report Each job produces the files and the evidence that they were processed correctly. ### Steps 1. Convert before transcribing Downmix to mono 16 kHz 16 bit WAV for whisper.cpp specifically, from the source rather than the export. ```sh ffmpeg -i out.wav -ar 16000 -ac 1 -c:a pcm_s16le for-whisper.wav ``` 2. Detect chapter boundaries Long silences using ffmpeg's silence detection, or a supplied outline with timestamps. Do not guess from the transcript alone. Files: `src/chapters.py` ```sh ffmpeg -i in.wav -af silencedetect=n=-35dB:d=2 -f null - ``` 3. Write the report JSON and a readable summary. Before and after figures per file, the preset, and every warning. This is what makes the job auditable. Files: `src/report.py` ### Done when - [ ] The report shows before and after figures for every file - [ ] Transcript timestamps line up with the audio - [ ] Chapter marks fall at real boundaries - [ ] Skipping transcription does not slow the audio export ### Watch out - whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported. ## M6 · Presets, watch folders and running it for others (production only) It runs unattended on a schedule and produces the same result every time. ### Steps 1. Make presets files Version controlled rather than stored in a database, so a change is reviewable and a past job's preset is recoverable. Files: `presets/podcast.json` 2. Add a watch folder with a settle check Wait until the file size has been stable for a few seconds before touching it. Files: `src/watch.py` 3. Assert determinism Process the same input twice and compare measurements. A difference means something in the pipeline is not being fed the measured values properly. Files: `tests/test_determinism.py` ### Done when - [ ] A dropped file is processed automatically with a report - [ ] A file still being copied is not processed early - [ ] The same input and preset produce identical measurements twice - [ ] Presets are version controlled files ### Watch out - A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing.
# Operations · Auphonic ## Backup Source recordings are the irreplaceable data and belong in an ordinary backup. Outputs and reports are regenerable from sources and presets, which is why presets are version controlled. ## Restore Restore the sources, check out the presets at the right revision, and rerun. Determinism is what makes this a real restore rather than an approximation. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For unattended operation, watch the job failure rate and the spread of output measurements. Outputs drifting from target usually means the second pass is not receiving measured values. ## Incident checklist A batch processed with the wrong preset is recoverable because sources are untouched: correct the preset and rerun. This is the reason for the never-overwrite rule and it will justify itself at least once. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Outputs measure within tolerance of the configured target - [ ] Loudness range is preserved rather than compressed - [ ] The determinism test passes on repeated runs - [ ] Source files are confirmed unmodified by checksum - [ ] The default denoise strength has been checked by ear on the sibilant fixture ## Launch constraint Do not market omitted Auphonic capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23. TARGET_LUFS=-16 # Required. True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format. TRUE_PEAK_DB=-1.5 # Required. Target loudness range in LU. Speech usually sits lower than music. LOUDNESS_RANGE=11 # Required. Noise reduction strength. Low by default; raising it introduces artefacts around consonants. DENOISE_NR=12 # Required. Where processed files and reports are written. Never the source directory. OUTPUT_DIR=./out # Optional. Optional. Leave unset to skip transcription entirely. WHISPER_MODEL_PATH=./models/ggml-base.en.bin
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Auphonic because creators pay to remove fragile audio plumbing and publishing chores from a release schedule. The recurring cost buys codec support, loudness standards, transcription, storage, feeds, analytics, and deliverability to directories, not just the visible interface.
xproprietary adaptive audio processing, cloud queues, and broad format delivery
xremote studio reliability
xlicensed music libraries
xhosting distribution
xadvanced mastering and support
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Auphonic pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| auphonic free | $0 | $0 | 2 processed audio hours/month; free credits do not roll over; multitrack productions under 20 minutes; Auphonic jingle |
| auphonic s recurring | — | $13 | 9 processed hours/month |
| auphonic m recurring | — | $30 | 21 processed hours/month |
| auphonic l recurring | — | $64 | 45 processed hours/month |
| auphonic xl recurring | — | $136 | 100 processed hours/month |
| auphonic xxl recurring | — | $290 | 250 processed hours/month |
| recurring more | custom | — | More than 1,000 processed hours/month |
| one-time 5 hours | custom | — | 5 processed hours total; credits never expire |
| one-time 10 hours | custom | — | 10 processed hours total; credits never expire |
| one-time 25 hours | custom | — | 25 processed hours total; credits never expire |
| one-time 50 hours | custom | — | 50 processed hours total; credits never expire |
| one-time 100 hours | custom | — | 100 processed hours total; credits never expire |
| one-time 250 hours | custom | — | 250 processed hours total; credits never expire |
| one-time 500 hours | custom | — | 500 processed hours total; credits never expire |
| one-time 1,000 hours | custom | — | 1,000 processed hours total; credits never expire |
| one-time 2,000 hours | custom | — | 2,000 processed hours total; credits never expire |
| one-time 3,000 hours | custom | — | 3,000 processed hours total; credits never expire |
| one-time more | custom | — | More than 3,000 processed hours total |
free tier2 processed audio hours/month; no rollover; multitrack productions under 20 minutes; outputs include an Auphonic jingle; no speech recognition or automatic shownotes
billingfree + recurring monthly/yearly plans + non-expiring one-time credits; yearly recurring prices are 20% lower
hidden costsMinimum charge is 3 minutes per production; changing an input file or creating a new production charges again; recurring credits expire monthly, auto-top-up can repurchase one-time packs, and VAT may be added.
verified 2026-08-14 · source ↗
Vibecode Auphonic
Kinda. The core of Auphonic is buildable in a weekend with the prompt on this page, but there are real gaps: proprietary adaptive audio processing, cloud queues, and broad format delivery, remote studio reliability. Read the honest list above before committing.
How much does Auphonic cost?
Auphonic's pricing is usage-based or varies by plan · Pricing is credit and hour based rather than a stable monthly subscription; canonical price is null..
What do I lose by replacing Auphonic?
Honestly: proprietary adaptive audio processing, cloud queues, and broad format delivery; remote studio reliability; licensed music libraries; hosting distribution; advanced mastering and support. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Auphonic?
Yes: Audacity (Noise reduction, loudness normalization and batch macros, with no cloud queue to blame.) The prompt is for when you want it exactly your way.