Webui #1

Merged
steve-admin merged 16 commits from webui into main 2026-10-16 18:19:21 +01:00
2 changed files with 178 additions and 4 deletions
Showing only changes of commit 57ae9deb30 - Show all commits

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_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"