Webui #1
No files matched your search
@@ -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
@@ -62,6 +62,21 @@ SFX_SA3_GENERATE = SFX_SA3_DIR / "generate.py"
|
|||||||
SFX_MODEL_SA3 = "Stable Audio 3 (Small-SFX)"
|
SFX_MODEL_SA3 = "Stable Audio 3 (Small-SFX)"
|
||||||
SFX_MODEL_MOSS = "MOSS-SoundEffect"
|
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_BASE = "Base (full quality)"
|
||||||
MODEL_FLASH = "Flash (fast draft, 4-step)"
|
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:
|
if not reference:
|
||||||
raise gr.Error("Set a voice reference first (upload, strip, and/or edit it above).")
|
raise gr.Error("Set a voice reference first (upload, strip, and/or edit it above).")
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
@@ -430,8 +476,9 @@ def generate_speech(reference: str, text: str, language: str):
|
|||||||
cwd=TTS_DIR,
|
cwd=TTS_DIR,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
transcript = _whisper_transcribe(str(out_path))
|
final_path = apply_mastering(str(out_path)) if master_after else str(out_path)
|
||||||
return str(out_path), transcript
|
transcript = _whisper_transcribe(final_path)
|
||||||
|
return final_path, transcript
|
||||||
|
|
||||||
|
|
||||||
def generate_sound_effect(description: str, model_choice: str, duration: float):
|
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)")
|
gr.Markdown("## 3. Generate (MOSS-TTS)")
|
||||||
gen_text = gr.Textbox(label="Text to speak", lines=6)
|
gen_text = gr.Textbox(label="Text to speak", lines=6)
|
||||||
gen_language = gr.Dropdown(LANGUAGES, value="English", label="Language")
|
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")
|
generate_btn = gr.Button("Generate speech", variant="primary")
|
||||||
generated_audio = gr.Audio(label="Generated speech", type="filepath")
|
generated_audio = gr.Audio(label="Generated speech", type="filepath")
|
||||||
gen_transcript_box = gr.Textbox(label="Whisper transcript of generated speech")
|
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],
|
outputs=[current_ref_audio, history_state, history_label, ref_transcript_box],
|
||||||
)
|
)
|
||||||
generate_btn.click(
|
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],
|
outputs=[generated_audio, gen_transcript_box],
|
||||||
)
|
)
|
||||||
use_ref_transcript_btn.click(lambda t: t, inputs=[ref_transcript_box], outputs=[gen_text])
|
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],
|
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"):
|
with gr.Tab("Characters"):
|
||||||
gr.Markdown(
|
gr.Markdown(
|
||||||
f"# The Service Station cast\n"
|
f"# The Service Station cast\n"
|
||||||
|
|||||||
Reference in new issue
Block a user