Add a Mastering tab and auto-apply toggle for generated voice output

Pedalboard-based chain (warmth EQ, presence dip, compression, light
reverb, hard-ceiling limiter) fixes the spectral imbalance behind
generated TTS sounding tinny - see mastering/master.py docstring and
commit history for the before/after spectral analysis and the A/B tuning
pass against real character clips. Pipeline Studios Generate step can
auto-apply it; the standalone tab works on any clip.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
steve-adminandClaude Sonnet 5 committed 2026-10-16 11:08:53 +00:00
1 parent b4fef19993
commit 57ae9deb30
2 files changed
+178 -4

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
"""
Mastering chain for generated TTS/voice clips: fixes the "tinny" spectral
imbalance common to neural vocoder output (excess 3-6kHz presence energy,
starved low end relative to a naturally-recorded voice - see the project's
spectral comparison: raw recordings skew toward 200-1000Hz, generated clips
dump ~35% of their energy into 3-6kHz) and adds the density/space that
reads as "produced" rather than dry/synthetic.
Chain order matters: EQ before compression (so the compressor reacts to
the corrected tonal balance, not the raw tinny one), reverb after
compression (so the room tail doesn't get squashed), limiter last.
"""
import argparse
import numpy as np
import soundfile as sf
from pedalboard import (
Compressor,
HighpassFilter,
HighShelfFilter,
Limiter,
LowShelfFilter,
PeakFilter,
Pedalboard,
Reverb,
)
def build_chain(
warmth_db: float,
presence_cut_db: float,
air_db: float,
comp_ratio: float,
reverb_wet: float,
) -> Pedalboard:
return Pedalboard([
HighpassFilter(cutoff_frequency_hz=60),
LowShelfFilter(cutoff_frequency_hz=150, gain_db=warmth_db, q=0.7),
PeakFilter(cutoff_frequency_hz=3800, gain_db=presence_cut_db, q=1.0),
HighShelfFilter(cutoff_frequency_hz=6500, gain_db=air_db, q=0.7),
Compressor(threshold_db=-20, ratio=comp_ratio, attack_ms=12, release_ms=120),
Reverb(room_size=0.08, damping=0.85, wet_level=reverb_wet, dry_level=1.0 - reverb_wet),
Limiter(threshold_db=-1.0, release_ms=100),
])
def main() -> None:
parser = argparse.ArgumentParser(description="Apply a studio mastering chain to a voice clip")
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--warmth-db", type=float, default=5.5, help="Low-shelf boost @150Hz")
parser.add_argument("--presence-cut-db", type=float, default=-5.0, help="Peak cut @3.8kHz (the tinny band)")
parser.add_argument("--air-db", type=float, default=-4.5, help="High-shelf @6.5kHz, for softness")
parser.add_argument("--comp-ratio", type=float, default=2.5)
parser.add_argument("--reverb-wet", type=float, default=0.025)
args = parser.parse_args()
audio, sr = sf.read(args.input, always_2d=True)
audio = audio.T.astype(np.float32) # pedalboard wants (channels, samples)
board = build_chain(
args.warmth_db, args.presence_cut_db, args.air_db, args.comp_ratio, args.reverb_wet
)
processed = board(audio, sr)
# pedalboard's Limiter is attack/release-based, not a true brick-wall
# limiter - fast transients can still punch through its threshold (seen
# in practice: in-memory peaks hit 0dBFS despite threshold_db=-1.0,
# producing audible clipping distortion on playback). Hard safety
# ceiling here guarantees the written file never clips regardless.
target_peak = 10 ** (-1.0 / 20)
peak = np.max(np.abs(processed))
if peak > target_peak:
processed = processed * (target_peak / peak)
sf.write(args.output, processed.T, sr)
print(f"[INFO] Saved {args.output}")
if __name__ == "__main__":
main()
+96 -4
View File
@@ -62,6 +62,21 @@ SFX_SA3_GENERATE = SFX_SA3_DIR / "generate.py"
SFX_MODEL_SA3 = "Stable Audio 3 (Small-SFX)"
SFX_MODEL_MOSS = "MOSS-SoundEffect"
# Mastering - own isolated venv (pedalboard), fixes the "tinny" spectral
# imbalance generated TTS output has (see mastering/master.py's docstring
# for the measured before/after). Defaults below match what Steve approved
# after an A/B listen on real character clips.
MASTERING_DIR = REPO_ROOT / "mastering"
MASTERING_PYTHON = MASTERING_DIR / ".venv" / "bin" / "python3"
MASTERING_SCRIPT = MASTERING_DIR / "master.py"
MASTERING_DEFAULTS = {
"warmth_db": 5.5,
"presence_cut_db": -5.0,
"air_db": -4.5,
"comp_ratio": 2.5,
"reverb_wet": 0.025,
}
MODEL_BASE = "Base (full quality)"
MODEL_FLASH = "Flash (fast draft, 4-step)"
@@ -406,7 +421,38 @@ def reset_to_original(history: list):
# ---------------------------------------------------------------------------
def generate_speech(reference: str, text: str, language: str):
def apply_mastering(
audio_path: str,
warmth_db: float = MASTERING_DEFAULTS["warmth_db"],
presence_cut_db: float = MASTERING_DEFAULTS["presence_cut_db"],
air_db: float = MASTERING_DEFAULTS["air_db"],
comp_ratio: float = MASTERING_DEFAULTS["comp_ratio"],
reverb_wet: float = MASTERING_DEFAULTS["reverb_wet"],
) -> str:
"""Run mastering/master.py's pedalboard chain (warmth EQ, presence dip,
compression, a touch of reverb, hard-ceiling limiter) on an existing
clip. Shared by the standalone Mastering tab and Pipeline Studio's
optional auto-apply-after-generation toggle."""
if not audio_path:
raise gr.Error("No audio to master yet.")
out_path = WORKDIR / f"mastered_{int(time.time())}.wav"
_run(
[
str(MASTERING_PYTHON), str(MASTERING_SCRIPT),
"--input", audio_path,
"--output", str(out_path),
"--warmth-db", str(warmth_db),
"--presence-cut-db", str(presence_cut_db),
"--air-db", str(air_db),
"--comp-ratio", str(comp_ratio),
"--reverb-wet", str(reverb_wet),
],
cwd=MASTERING_DIR,
)
return str(out_path)
def generate_speech(reference: str, text: str, language: str, master_after: bool):
if not reference:
raise gr.Error("Set a voice reference first (upload, strip, and/or edit it above).")
if not text or not text.strip():
@@ -430,8 +476,9 @@ def generate_speech(reference: str, text: str, language: str):
cwd=TTS_DIR,
env=env,
)
transcript = _whisper_transcribe(str(out_path))
return str(out_path), transcript
final_path = apply_mastering(str(out_path)) if master_after else str(out_path)
transcript = _whisper_transcribe(final_path)
return final_path, transcript
def generate_sound_effect(description: str, model_choice: str, duration: float):
@@ -838,6 +885,10 @@ with gr.Blocks(title="Audio-demo pipeline studio") as demo:
gr.Markdown("## 3. Generate (MOSS-TTS)")
gen_text = gr.Textbox(label="Text to speak", lines=6)
gen_language = gr.Dropdown(LANGUAGES, value="English", label="Language")
gen_master_cb = gr.Checkbox(
label="Apply mastering after generation (warmth/de-tin/light reverb - see Mastering tab)",
value=True,
)
generate_btn = gr.Button("Generate speech", variant="primary")
generated_audio = gr.Audio(label="Generated speech", type="filepath")
gen_transcript_box = gr.Textbox(label="Whisper transcript of generated speech")
@@ -878,7 +929,7 @@ with gr.Blocks(title="Audio-demo pipeline studio") as demo:
outputs=[current_ref_audio, history_state, history_label, ref_transcript_box],
)
generate_btn.click(
generate_speech, inputs=[current_ref_audio, gen_text, gen_language],
generate_speech, inputs=[current_ref_audio, gen_text, gen_language, gen_master_cb],
outputs=[generated_audio, gen_transcript_box],
)
use_ref_transcript_btn.click(lambda t: t, inputs=[ref_transcript_box], outputs=[gen_text])
@@ -927,6 +978,47 @@ with gr.Blocks(title="Audio-demo pipeline studio") as demo:
outputs=[sfx_save_status],
)
with gr.Tab("Mastering"):
gr.Markdown(
"# Mastering\n"
"Fixes the \"tinny\" spectral imbalance common to generated "
"TTS output (too much 3-6kHz presence, not enough low end) "
"and adds a bit of warmth/density/space. Works on any clip - "
"Pipeline Studio's Generate step can also apply this "
"automatically (see its checkbox)."
)
with gr.Row():
with gr.Column():
master_input = gr.Audio(label="Input audio", type="filepath")
master_warmth = gr.Slider(
minimum=0, maximum=10, step=0.5, value=MASTERING_DEFAULTS["warmth_db"],
label="Warmth (low-shelf boost @150Hz, dB)",
)
master_presence = gr.Slider(
minimum=-10, maximum=0, step=0.5, value=MASTERING_DEFAULTS["presence_cut_db"],
label="De-tin (presence cut @3.8kHz, dB)",
)
master_air = gr.Slider(
minimum=-10, maximum=2, step=0.5, value=MASTERING_DEFAULTS["air_db"],
label="Softness (high-shelf @6.5kHz, dB)",
)
master_comp = gr.Slider(
minimum=1, maximum=6, step=0.5, value=MASTERING_DEFAULTS["comp_ratio"],
label="Compression ratio",
)
master_reverb = gr.Slider(
minimum=0, maximum=0.2, step=0.005, value=MASTERING_DEFAULTS["reverb_wet"],
label="Reverb wet level",
)
master_btn = gr.Button("Apply mastering", variant="primary")
with gr.Column():
master_output = gr.Audio(label="Mastered output", type="filepath")
master_btn.click(
apply_mastering,
inputs=[master_input, master_warmth, master_presence, master_air, master_comp, master_reverb],
outputs=[master_output],
)
with gr.Tab("Characters"):
gr.Markdown(
f"# The Service Station cast\n"