Podcast Editing & Mastering
Turns raw podcast or voice-over recordings into a publishable final product: noise reduction, trim leading/trailing silence, vocal EQ and compression, loudness normalization to approximately -16 LUFS, export to MP3. Optionally add intro/outro music and segment-based music, and provide editing suggestions (which long pauses can be tightened, where retakes are) for user confirmation before cutting.
The scripts/ and references/ directories referred to below are located within this skill's directory. At runtime, use the full path, for example: bash <skill-directory>/scripts/edit_podcast.sh.
1. Clarify with the User Before Starting
- ffmpeg must be installed locally (free, open-source audio tool). If not present, ask the user to install it: macOS:
brew install ffmpeg, Linux: use the system package manager (e.g.,apt install ffmpeg). The script searches in this order: environment variableFFMPEG→ffmpegin system PATH → common installation paths/opt/homebrew/bin,/usr/local/bin. Tested with versions 6.0 and 8.1. - Requires bash and Python 3.9+, using only the standard library (no additional Python packages needed).
- Only supports macOS and Linux: Tested on macOS; Linux should work; Windows requires WSL (not tested).
- First-time noise reduction will download a public noise reduction model (~300KB) from the internet (RNNoise), stored in
~/.cache/ethan-podcast-post/, and verified via SHA-256. If download fails or checksum mismatch, automatically falls back to ffmpeg's built-in afftdn noise reduction. The script will print which method it used; you must inform the user. Model attribution is inreferences/third-party.md. - All processing is local: No audio is uploaded, no charges apply.
- What we don't do: We do not generate or clone voices, do not dub, do not transcribe (if subtitles are needed, ask the user to provide an SRT file), do not publish to any platform, and do not provide music (background music must be supplied by the user, who must confirm they have the rights to use it).
2. File Safety (Mandatory)
- Original recording is read-only: The script only reads it; it does not modify, move, or delete it.
- All output goes to a new directory: Creates a new directory next to the recording (e.g.,
Episode3_post/). Do not place output in the same directory as the recording or background music. The script will stop if it detects the output directory is the same as the input file's directory. - No overwriting without permission: The script stops if the output directory already contains a final file,
audio_raw.wav, orvoice_master.wav. To overwrite, first tell the user which files will be overwritten and that the old files cannot be recovered. Only proceed after the user agrees and setsOVERWRITE=1before re-running. The same rule applies to editing suggestions, cutting, and vocal processing scripts: they stop if identically named output exists; proceed only after the user agrees to use--overwrite. - Confirm before deletion: Any deletion (including cleanup of large files like
audio_raw.wav) must be communicated to the user—specify which file and why—and get explicit consent before proceeding. Intermediate files are placed in a temporary subdirectory within the output directory, created for that run only; at the end, only this subdirectory is cleaned. - What to cut is determined by the user: The editing suggestions are just a candidate list; nothing is cut until the user confirms.
3. Processing Workflow
First, run an environment check and inform the user of the results:
bash <skill-directory>/scripts/edit_podcast.sh --check
This will list the found ffmpeg, its version, whether 250Hz dynamic EQ is enabled (see section 6), and whether the noise reduction model is ready. If ffmpeg is not found, it will provide installation instructions; install it before proceeding.
Step 1: First Pass – Produce Vocal Master and Preview Version
KEEP_MASTER=1 bash <skill-directory>/scripts/edit_podcast.sh <recording-file> <output-directory>/pass1 <name>
- The recording can be wav, m4a, mp3, mp4, or any format ffmpeg can read. For dual or multi-speaker conversations, add
INTERVIEW_MODE=1(stronger compression, and automatically levels out volume differences between speakers). - Processing order: Extract to 48kHz WAV → Quality check → Trim leading/trailing silence (internal pauses are left untouched) → De-click → 80Hz high-pass → Noise reduction → 250Hz dynamic EQ → De-esser → Compression → 5kHz brightness boost → Linear gain to -16 LUFS → Limiter → Export MP3 → Re-measure loudness.
- Outputs:
<name>_final.mp3: The final file without music (also serves as the preview version).voice_master.wav: The vocal master (with leading/trailing silence trimmed); used for subsequent transcription, editing suggestions, and cutting.audio_raw.wav: A WAV copy of the original recording (written to the output directory only after all processing succeeds); useful for re-processing later, but can be large.
- Review the following items in the output and inform the user:
- If tail volume is below -80dB: The recording may have been interrupted mid-way; confirm this with the user first.
- Noise reduction method used: RNNoise or afftdn.
- Final loudness should be -16 LUFS ±1, true peak not higher than -1.5 dBTP. If the script reports an out-of-range value, inform the user accurately.
- If the recording is too quiet or empty, the script will error and stop, producing no empty final file. The error will include a suggested value based on the recording's peak level: first confirm with the user that the recording indeed contains speech, then re-run with
INPUT_GAIN=<suggested-value>(in dB, this is added on top of the current INPUT_GAIN setting) to bring the entire segment to a normal level. When this error occurs, no files remain in the output directory, so you can re-run using the same directory. Also advise the user to increase the recording input volume for future recordings.
Step 2: Editing Suggestions (Candidates Only, No Audio Changes)
Transcription must be generated from voice_master.wav from Step 1 (or from the un-music-version MP3 from Step 1, as their timestamps are identical). Do not use transcription from the original recording: its leading/trailing silence has not been trimmed, so timestamps will not align, and the detected positions will be off.
Find long pauses that can be tightened:
python3 <skill-directory>/scripts/edit_cuts.py suggest <output-directory>/pass1/voice_master.wav \
[--srt subtitles.srt] [--protect 15:00.0-15:03.0 ...]
- By default, pauses longer than 1.5 seconds are candidates, suggested to be tightened to 1.2 seconds (keeping half the room noise on each side of the pause).
- Pauses deliberately left by the user (e.g., silence after an important statement) can be marked with
--protect start-end, with 0.5 second buffer on each side, and will not be suggested for cutting. - If obvious long pauses are not found, the noise floor may be too high; try lowering
--noisefrom the default -45 to -40 and re-run. - If a 40-minute segment has more than 15 candidates or a cumulative candidate length exceeding 60 seconds, the script will warn that the parameters are too aggressive: increase
--min-pause, and do not cut all of them.
Find retake segments marked by a retake phrase (requires subtitles):
python3 <skill-directory>/scripts/edit_cuts.py retakes <output-directory>/pass1/voice_master.wav --srt subtitles.srt [--phrase your-phrase]
- The convention during recording: mistake → pause >1 second → calmly say a retake phrase → pause >1 second → re-speak from the beginning of the sentence.
- Default phrase is "重来重来" (Chinese for "start over"). Use
--phraseto set your own phrase. If the transcription software often writes the phrase as homophones, use multiple--phraseflags to include all variant spellings. Choose a phrase that would not appear naturally in the content; saying it twice in a row reduces false positives. - The script finds the retake phrase in the subtitles, matches the re-spoken sentence's beginning back to the incorrectly spoken sentence, and outputs cut intervals with confidence levels (high / mid / low). Low-confidence suggestions must be auditioned by the user before cutting.
Both commands output voice_master_suggested_cuts.tsv and voice_master_retake_cuts.tsv respectively (will not overwrite existing files; user must agree to use --overwrite). Present the candidates to the user one by one (time, context, time saved). After the user confirms which cuts to apply, copy those rows into a new cuts.tsv file (tab-separated: start, end, notes; times in MM:SS.s format).
Step 3: Apply Cuts
python3 <skill-directory>/scripts/edit_cuts.py apply <output-directory>/pass1/voice_master.wav --cuts cuts.tsv [--srt subtitles.srt]
- Cuts are sample-accurate, with 15ms equal-power crossfade at seams. Outputs a 24-bit
voice_master_edited.wavand an edit mapvoice_master_edited.edit_map.json(recording source and position for each retained segment). - When
--srtis provided, intact subtitle entries are shifted accordingly. If a subtitle is cut in half, the script refuses to apply—remove--srtand re-run, then ask the user to re-transcribe the edited master. - After cutting, ask the user to listen to a few seconds around each cut seam: no clicks, no double inhalation, no tonal discontinuity.
Step 4 (Optional): Apply Vocal Effects to Specific Segments (e.g., quotes, memories)
python3 <skill-directory>/scripts/voice_fx.py <edited-master.wav> --fx fx.tsv
python3 <skill-directory>/scripts/voice_fx.py --list # View the five presets
- Presets:
quote(reading letters, others' text),archive(old newspapers, documents),telephone(relayed phone conversation),memory(recollection, dream),radio(old broadcast, max once per episode). - fx.tsv format: tab-separated columns: preset, start, end, fade-in, fade-out, notes. Use
-for default fade durations. - Processing preserves sample-accurate length and loudness; outputs
<original-name>_fx.wav.
Step 5 (Optional): Music and Cue Sheet
Only do this if the user provides background music. First write the cue sheet, then lint it. The cue sheet must be placed in the second-pass output directory, with filename <name>_cues.tsv.
Cue sheet format: tab-separated, 7 columns. Lines starting with # are ignored. Write - to use default value. time is in MM:SS.s relative to the vocal master timeline. Times below are examples:
# type time src dur vol fade_in fade_out
stinger 02:00.0 - 3 - - -
break 12:00.0 -@20 8 0.5 2 2
bed 18:00.0 -@40 30 0.08 - -
ending 40:00.0 -@90 - - - -
| Type | Purpose | dur | Default Volume |
|---|---|---|---|
| stinger | Short musical accent, overlaid on voice | Default 6 s | 0.12 |
| bed / post | Music bed under voice; auto-duck when voice appears | Required | 0.12 |
| break | Cuts voice, inserts pure music segment, resumes voice after music ends; total episode length increases by dur seconds | Required | 0.5 |
| ending | Outro music: ducks under speech, fades up after speech ends, then fades out | Write - | 0.22 |
src: Write-to use the currently passed background music file;-@20means start from the 20th second of the background music. You can also write the full path to another music file, optionally with@seconds(without@, start from the beginning).- For
stinger, writing-without@defaults to starting from the 60th second of the background music; for other types without@, start from the beginning. - After writing the cue sheet, lint it first. Fix any ❌ errors before proceeding:
python3 <skill-directory>/scripts/cues_lint.py <name>_cues.tsv --master <edited-master.wav> --bg <background-music>
This checks format, boundaries, media existence, window overlaps, music-to-speech ratio (recommended ≤15%), and provides a "master → final" timestamp conversion table for writing the program timeline.
Step 6: Second Pass – Produce Final File
MASTER_WAV=<edited-master.wav> bash <skill-directory>/scripts/edit_podcast.sh - <background-music> <output-directory>/final <name>
- First argument is
-. WhenMASTER_WAVis set, the script skips extraction, noise reduction, and normalization; it directly overlays cue sheet music, mixes, and exports. - If background music is provided and the audio is longer than 80 seconds, intro/outro music is automatically added: intro music plays for 3 seconds before voice enters; after voice ends, music continues for about 7 seconds then fades out; music automatically ducks when voice is present.
- If no cutting and no music is needed, the Step 1 final file is the final product; skip this step.
Step 7: Verification
The script already does a final loudness measurement. To independently verify (using the ffmpeg path shown by --check):
"<ffmpeg-path>" -hide_banner -nostats -i <final.mp3> -filter_complex ebur128=peak=true -f null - 2>&1 | tail -12
- Pass: Integrated loudness within -16 LUFS ±1, True peak no higher than -1.5 dBFS (dBTP).
- Ask the user to listen with headphones: music should not overpower the voice; music should not jump back abruptly during pauses; each cut seam should sound natural; the ending should not be cut off; deliberately left pauses should remain.
- When delivering, tell the user: final file location, noise reduction method, measured loudness and true peak values, and which cuts were applied. If values are out of spec, report honestly; do not claim they passed.
4. Editing Principles
- Cut only mistakes, not breaths: Cut stumbles, reading distractions, retake waste segments. Do not cut breaths, thinking sounds like "um", inter-sentence hesitations, or deliberately left silence. When in doubt, leave it in.
- Pauses under 0.8 seconds remain untouched; 0.8–1.5 second inter-sentence pauses are kept by default; longer pauses require evidence of stumbling or distraction to tighten.
- Place cut points at natural breath boundaries; do not truncate the beginning or end of words or trailing sounds. Breaths are either fully kept or fully removed.
- Background music should be quieter than the voice; avoid songs with prominent vocals or heavy midrange (1–3kHz). Prefer placing melodic music between segments (break) rather than under continuous speech. Total music duration should generally not exceed 15% of the final file.
- Detailed guidelines are in
references/editing-style-guide.md(style and packaging),references/editing-essentials.md(execution and verification), andreferences/audio-glossary.md(terminology).
5. Environment Variables (All Optional)
| Variable | Default | Purpose |
|---|---|---|
FFMPEG | Auto-detect | Specify ffmpeg path |
DENOISE | rnnoise | rnnoise / afftdn / off |
RNNOISE_MODEL | ~/.cache/ethan-podcast-post/bd.rnnn | Path to noise reduction model; can point to an existing file |
RNNOISE_URL | Public GitHub model | Alternative mirror URL if internet is restricted (SHA-256 still checked after download) |
INPUT_GAIN | 0 | dB to boost (or cut) the entire recording before processing; set to the value from the error message when recording is too quiet |
TRIM_THRESHOLD | -50 | dB threshold for trimming leading/trailing silence; lower if gentle sounds at start/end are being cut, e.g., -60 |
INTERVIEW_MODE | 0 | 1 = dual or multi-speaker conversation |
KEEP_MASTER | 0 | 1 = keep vocal master voice_master.wav |
MASTER_WAV | (empty) | Vocal master path for second pass |
MUSIC_VOLUME | 0.12 | Background music volume ratio |
INTRO_DURATION | 40 | Intro/outro music window in seconds; skipped if audio is shorter than 2x this value |
PREROLL / TAIL | 3 / 7 | Intro music lead seconds / seconds of music after voice ends |
OUTRO_SEEK | (empty) | Seconds into the track from which to take outro music |
MP3_BITRATE | 160k | Final MP3 bitrate |
OVERWRITE | 0 | 1 = allow overwriting existing output (must first obtain user consent) |
6. Frequently Asked Questions
- Cannot find ffmpeg: Ask the user to install it (macOS:
brew install ffmpeg, Linux: use package manager), or specify the path withFFMPEG=/full/path/to/ffmpeg. - ffmpeg version: The script reads the version number. The meaning of parameters for the 250Hz dynamic EQ function changed between ffmpeg 6 and 8; both versions have been calibrated using sine wave signals to produce identical results. For unverified versions (e.g., 7.x), this stage is skipped with a notice, but all other processing continues. The trim parameters have also been adjusted for new behavior; 6.x and 8.x cut identically.
- Error "Almost nothing left after trimming silence" or "Cannot measure loudness after processing": The recording is too quiet or empty. Confirm there is speech, then re-run with the
INPUT_GAINvalue suggested in the error message (loudness below -70 LUFS is unmeasurable; adjusting trim threshold alone is insufficient). If there truly is no audio, the error will persist and no empty final file will be produced. - Noise reduction used afftdn: The model download failed. You can set
RNNOISE_URLto an alternative mirror, or useRNNOISE_MODELto point to an existing model file, then re-run (ask the user before overwriting any existing output). - Tail volume below -80dB: The recording likely stopped mid-way; confirm with the user first. Do not treat this as a trimming issue.
- No retake segments found: Verify that the transcription was generated from
voice_master.wav, then check how the retake phrase was transcribed (it may have been misspelled). Use--phraseto add that spelling. - See all commands executed: Run with
bash -x <skill-directory>/scripts/edit_podcast.sh .... - Building ffmpeg filter arguments in zsh: When a variable is immediately followed by a colon, use
${VAR}:...; otherwise, zsh may interpret$VAR:aor$VAR:ras modifiers, silently altering the parameter.
License: Code MIT, Documentation CC BY 4.0.