Bobby Collins
Fix Unkey v2 auth + 412 fallback for credits-based keys
4b8b932
"""Audio Mastering Suite — Gradio web application."""
import os
import gc
import time
import gradio as gr
from analysis import GeminiUnavailableError
from dsp import master_audio, master_audio_full
from presets import PRESETS
from visualization import plot_waveform_comparison, plot_spectrum_comparison
# Lazy-imported inside callbacks to avoid slowing app startup
# from analysis import recommend_settings, compare_master
_PRICING_URL = "https://huggingface.co/spaces/AnimalMonk/studio-ai-pricing"
def _dsp_settings_md(dsp):
"""Format actual DSP parameters as a markdown table."""
if not dsp:
return ""
lines = [
"### DSP Settings",
"| Parameter | Value |",
"|---|---|",
f"| **HPF** | {dsp.get('hpf_freq', 15):.0f} Hz |",
]
# EQ — could be list (manual) or dict with band1-4 (super)
eq = dsp.get("eq", {})
if isinstance(eq, list):
for i, e in enumerate(eq):
lines.append(
f"| **EQ {i+1}** | {e.get('type','peak')} @ "
f"{e.get('freq',1000):.0f} Hz, {e.get('gain_db',0):+.1f} dB, "
f"Q={e.get('q',1.0):.2f} |"
)
if not eq:
lines.append("| **EQ** | flat (no adjustments) |")
else:
for bk in ("band1", "band2", "band3", "band4", "band5", "band6"):
b = eq.get(bk, {})
g = b.get("gain_db", 0)
if abs(g) < 0.01:
lines.append(f"| **EQ {bk}** | bypassed |")
else:
lines.append(
f"| **EQ {bk}** | {b.get('type','peak')} @ "
f"{b.get('freq',1000):.0f} Hz, {g:+.1f} dB, "
f"Q={b.get('q',1.0):.2f} |"
)
# Crossovers
if "crossover_low" in dsp:
lines.append(
f"| **Crossovers** | {dsp.get('crossover_low',200):.0f} / "
f"{dsp.get('crossover_high',4000):.0f} Hz |"
)
# Compression
comp = dsp.get("compression", {})
if comp:
for bk in ("low", "mid", "high"):
bp = comp.get(bk, {})
r = bp.get("ratio", 1.0)
if r <= 1.0:
lines.append(f"| **Comp {bk}** | bypassed |")
else:
lines.append(
f"| **Comp {bk}** | {bp.get('threshold',-14):.0f} dB, "
f"{r:.1f}:1, atk {bp.get('attack_ms',30):.0f} ms, "
f"rel {bp.get('release_ms',150):.0f} ms |"
)
else:
lines.append("| **Compression** | bypassed |")
lines.append(f"| **Stereo Width** | {dsp.get('stereo_width', 100)}% |")
return "\n".join(lines)
_PREMIUM_MSG = (
"**AI features require a StudioAI API key.**\n\n"
f"**[Get an API Key]({_PRICING_URL})** — plans start at $9/month.\n\n"
"*Manual mastering is always free.*"
)
def _check_ai_key(user_key, consume=False):
"""Validate user-provided AI access key via Unkey.
Args:
user_key: The key string from the user.
consume: If True, decrement one usage credit. If False, just validate
without consuming (cost=0).
Returns:
(valid: bool, error_msg: str, remaining: int|None)
"""
if not user_key:
return (False, _PREMIUM_MSG, None)
key = user_key.strip()
# Legacy key support (transition period)
for env_var in ("AI_ACCESS_KEY", "AI_ACCESS_KEY_2"):
expected = os.environ.get(env_var, "")
if expected and key == expected.strip():
return (True, "", None) # legacy keys — no usage limit
# Unkey verification — checks key validity and rate limits
unkey_api_id = os.environ.get("UNKEY_API_ID", "")
if not unkey_api_id:
# Unkey not configured — fall back to rejecting unknown keys
return (False, _PREMIUM_MSG, None)
try:
import requests as _req
unkey_root = os.environ.get("UNKEY_ROOT_KEY", "")
auth_headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {unkey_root}",
}
cost = 1 if consume else 0
# Try with rate limits first (subscription keys with monthly_masters)
resp = _req.post(
"https://api.unkey.com/v2/keys.verifyKey",
headers=auth_headers,
json={"key": key, "ratelimits": [{"name": "monthly_masters", "cost": cost}]},
timeout=5,
)
# If rate limit doesn't exist on this key, Unkey returns 412.
# Fall back to credits-only verification.
if resp.status_code == 412:
resp = _req.post(
"https://api.unkey.com/v2/keys.verifyKey",
headers=auth_headers,
json={"key": key},
timeout=5,
)
data = resp.json()
except Exception:
return (True, "", None) # fail open on network error
result = data.get("data", data)
if not result.get("valid", False):
code = result.get("code", "UNKNOWN")
msgs = {
"NOT_FOUND": f"Invalid API key. [Get one here]({_PRICING_URL}).",
"RATE_LIMITED": (
"You've used all your AI masters this month.\n\n"
f"[Upgrade your plan]({_PRICING_URL}) for more."
),
"USAGE_EXCEEDED": (
"You've used all your AI masters this month.\n\n"
f"[Upgrade your plan]({_PRICING_URL}) for more."
),
"DISABLED": "Your API key has been disabled. Contact support.",
"EXPIRED": f"Your subscription has expired. [Renew here]({_PRICING_URL}).",
}
return (False, msgs.get(code, f"Key validation failed ({code})."), 0)
# Check remaining from either rate limits or credits
remaining = None
ratelimits = result.get("ratelimits", [])
for rl in ratelimits:
if rl.get("name") == "monthly_masters":
remaining = rl.get("remaining")
break
if remaining is None:
remaining = result.get("credits", result.get("remaining"))
return (True, "", remaining)
# ---------------------------------------------------------------------------
# Callbacks
# ---------------------------------------------------------------------------
def check_key_status(user_key):
"""Check key validity and remaining uses without consuming one."""
if not user_key or not user_key.strip():
return ""
key = user_key.strip()
# Legacy keys
for env_var in ("AI_ACCESS_KEY", "AI_ACCESS_KEY_2"):
expected = os.environ.get(env_var, "")
if expected and key == expected.strip():
return "**Key valid** — unlimited uses (legacy key)"
unkey_api_id = os.environ.get("UNKEY_API_ID", "")
if not unkey_api_id:
return ""
try:
import requests as _req
resp = _req.post(
"https://api.unkey.com/v2/keys.verifyKey",
headers={"Authorization": f"Bearer {os.environ.get('UNKEY_ROOT_KEY', '')}"},
json={"key": key, "ratelimits": [{"name": "monthly_masters", "cost": 0}]},
timeout=5,
)
data = resp.json()
result = data.get("data", data)
except Exception:
return ""
if not result.get("valid", False):
code = result.get("code", "UNKNOWN")
if code == "NOT_FOUND":
return f"**Invalid key.** [Get one here]({_PRICING_URL})"
elif code in ("RATE_LIMITED", "USAGE_EXCEEDED"):
return "**No masters remaining this month.** [Upgrade]({_PRICING_URL})"
elif code == "DISABLED":
return "**Key disabled.** Contact support."
return f"**Key error:** {code}"
tier = result.get("meta", {}).get("tier", "unknown")
ratelimits = result.get("ratelimits", [])
for rl in ratelimits:
if rl.get("name") == "monthly_masters":
remaining = rl.get("remaining", "?")
limit = rl.get("limit", "?")
return f"**{tier.capitalize()}** — {remaining}/{limit} AI masters remaining this month"
return f"**{tier.capitalize()}** — key valid"
def apply_preset(preset_name):
"""Update all sliders when a preset is selected."""
if preset_name == "-- None --" or preset_name not in PRESETS or PRESETS[preset_name] is None:
return (
gr.update(), gr.update(), gr.update(),
gr.update(), gr.update(), gr.update(),
gr.update(),
)
p = PRESETS[preset_name]
return (
p["lows_db"],
p["mid_boost_db"],
p["highs_db"],
p["bass_boost_db"],
p["bass_freq_hz"],
p["compression"],
p["stereo_width"],
)
def toggle_custom_lufs(target_choice):
"""Show the custom LUFS input only when 'Custom' is chosen."""
return gr.update(visible=(target_choice == "Custom"))
def _lock_btns():
"""Return 6 gr.Button updates to disable all AI/action buttons."""
return tuple(gr.Button(interactive=False) for _ in range(6))
def _unlock_btns():
"""Return 6 gr.Button updates to re-enable all AI/action buttons."""
return tuple(gr.Button(interactive=True) for _ in range(6))
def ai_recommend(audio_path, ai_access_key):
"""Analyze raw audio and return AI-recommended settings + reasoning.
Yields 14 values: 7 sliders + ai_values_state + ai_reasoning_display + 5 buttons.
"""
_skip = gr.Slider()
_no_change = (_skip, _skip, _skip, _skip, _skip, _skip, _skip, {})
valid, err, _remaining = _check_ai_key(ai_access_key)
if not valid:
yield (*_no_change, err, *_unlock_btns())
return
if audio_path is None:
raise gr.Error("Please upload an audio file first.")
# Immediate feedback
yield (*_no_change, "# \u26a0\ufe0f AI WORKING \u2014 PLEASE WAIT\n\n---\n\n### \u23f3 Analyzing audio with Gemini\u2026\n*This takes 15-30 seconds.*",
*_lock_btns(),
)
from analysis import recommend_settings
result = recommend_settings(audio_path)
if result is None:
yield (*_no_change, "*Set GOOGLE_API_KEY to enable AI recommendations.*", *_unlock_btns())
return
if result.get("parse_error"):
yield (*_no_change, result.get("reasoning", "*Could not parse AI response.*"), *_unlock_btns())
return
yield (
gr.Slider(label=f"Lows (200 Hz) | AI: {result['lows_db']:+.1f} dB"),
gr.Slider(label=f"Mids (1.2 kHz) | AI: {result['mid_boost_db']:+.1f} dB"),
gr.Slider(label=f"Highs (10 kHz) | AI: {result['highs_db']:+.1f} dB"),
gr.Slider(label=f"Bass Boost (dB) | AI: {result['bass_boost_db']:.1f} dB"),
gr.Slider(label=f"Bass Freq (Hz) | AI: {result['bass_freq_hz']} Hz"),
gr.Slider(label=f"Compression | AI: {result['compression']}"),
gr.Slider(label=f"Stereo Width (%) | AI: {result['stereo_width']}%"),
result,
result.get("reasoning", ""),
*_unlock_btns(),
)
def apply_ai(ai_values):
"""Populate sliders with AI-recommended values, reset labels."""
if not ai_values:
raise gr.Error("No AI recommendations yet. Click 'AI Recommend' first.")
return (
gr.Slider(value=ai_values["lows_db"], label="Lows (200 Hz)"),
gr.Slider(value=ai_values["mid_boost_db"], label="Mids (1.2 kHz)"),
gr.Slider(value=ai_values["highs_db"], label="Highs (10 kHz)"),
gr.Slider(value=ai_values["bass_boost_db"], label="Bass Boost (dB)"),
gr.Slider(value=ai_values["bass_freq_hz"], label="Bass Boost Frequency (Hz)"),
gr.Slider(value=ai_values["compression"], label="Compression: Less <-> More"),
gr.Slider(value=ai_values["stereo_width"], label="Stereo Width (%)"),
)
def process(audio_path, lows_db, mid_boost_db, highs_db, bass_boost_db, bass_freq_hz,
comp_val, width, target_choice, custom_lufs):
"""Run the mastering pipeline and return all outputs."""
if audio_path is None:
raise gr.Error("Please upload an audio file first.")
# Resolve target LUFS
if target_choice == "-14 (Streaming)":
target = -14.0
elif target_choice == "-11 (CD)":
target = -11.0
else:
target = float(custom_lufs)
output_path, original, mastered, sr, stats = master_audio(
audio_path, lows_db, mid_boost_db, highs_db, bass_boost_db, bass_freq_hz,
comp_val, width, target,
)
# Plots
waveform_fig = plot_waveform_comparison(original, mastered, sr)
spectrum_fig = plot_spectrum_comparison(original, mastered, sr)
# Stats markdown (split into two for side-by-side display)
mono_note = "\n\n*Input is mono — stereo width adjustment was skipped.*" if stats["mono"] else ""
dsp_md = _dsp_settings_md(stats.get("dsp", {}))
stats_md = (
"### Loudness Statistics\n"
"| | LUFS | True Peak (dBTP) |\n"
"|---|---|---|\n"
f"| **Original** | {stats['orig_lufs']:.1f} | {stats['orig_peak']:.1f} |\n"
f"| **Mastered** | {stats['mast_lufs']:.1f} | {stats['mast_peak']:.1f} |\n\n"
f"{mono_note}"
)
# Store data for on-demand AI comparison
master_data = {
"original": original,
"mastered": mastered,
"sr": sr,
"settings": {
"lows_db": lows_db,
"mid_boost_db": mid_boost_db,
"highs_db": highs_db,
"bass_boost_db": bass_boost_db,
"bass_freq_hz": bass_freq_hz,
"compression": comp_val,
"stereo_width": width,
"target_lufs": target,
},
}
return (
output_path,
waveform_fig, spectrum_fig,
dsp_md, stats_md,
gr.Button("AI Compare Original vs Master", variant="secondary", visible=True),
gr.DownloadButton("Download Mastered File", value=output_path, visible=True),
master_data,
"",
)
def _slider_updates(values):
"""Return 7 gr.Slider updates from a settings dict."""
return (
gr.Slider(value=values["lows_db"]),
gr.Slider(value=values["mid_boost_db"]),
gr.Slider(value=values["highs_db"]),
gr.Slider(value=values["bass_boost_db"]),
gr.Slider(value=values["bass_freq_hz"]),
gr.Slider(value=values["compression"]),
gr.Slider(value=values["stereo_width"]),
)
_NO_SLIDER_CHANGE = tuple(gr.update() for _ in range(7))
def _make_history_entry(settings_dict, mast_features, summary=""):
"""Build a history entry dict for analysis context."""
return {
"settings": {k: v for k, v in settings_dict.items() if k != "target_lufs"},
"lufs": mast_features.get("lufs", "?"),
"true_peak": mast_features.get("true_peak_dbtp", "?"),
"crest_factor": mast_features.get("crest_factor_db", "?"),
"summary": summary,
}
def ai_compare(master_data, ai_access_key, history):
"""Run AI comparison report on demand (costs Gemini tokens).
Yields 15 values: 7 sliders + ai_values_state + ai_report_display
+ analysis_history_state + 5 buttons.
"""
SKIP = gr.update()
valid, err, _remaining = _check_ai_key(ai_access_key)
if not valid:
yield (
*_NO_SLIDER_CHANGE,
SKIP,
err,
SKIP, # history unchanged
*_unlock_btns(),
)
return
if not master_data:
raise gr.Error("Master a track first, then click AI Compare.")
# Immediate feedback -- lock buttons + prominent indicator
yield (
*_NO_SLIDER_CHANGE,
SKIP,
"# \u26a0\ufe0f AI WORKING \u2014 PLEASE WAIT\n\n---\n\n### \u23f3 Comparing original vs master\u2026\n*This takes 15-30 seconds.*",
SKIP, # history unchanged
*_lock_btns(),
)
try:
from analysis import compare_master_structured, extract_features
result = compare_master_structured(
master_data["original"],
master_data["mastered"],
master_data["sr"],
master_data["settings"],
history=history,
)
if result is None or result.get("parse_error"):
report = result.get("report", "*AI comparison unavailable.*") if result else "*AI unavailable.*"
yield (
*_NO_SLIDER_CHANGE,
SKIP,
"---\n### AI Post-Master Analysis\n" + report,
SKIP, # history unchanged
*_unlock_btns(),
)
else:
report = result.get("report", "*No report provided.*")
# Add this analysis to history
mast_features = extract_features(master_data["mastered"], master_data["sr"])
new_history = list(history) + [_make_history_entry(
master_data["settings"], mast_features,
summary=report[:200] if report else "",
)]
yield (
*_slider_updates(result),
result,
"---\n### AI Post-Master Analysis\n" + report,
new_history,
*_unlock_btns(),
)
except Exception as e:
yield (
*_NO_SLIDER_CHANGE,
SKIP,
f"---\n### AI Post-Master Analysis\n*AI comparison unavailable: {e}*",
SKIP, # history unchanged
*_unlock_btns(),
)
def auto_master(audio_path, ai_access_key, target_choice, custom_lufs):
"""Iterative AI-driven mastering loop (generator).
Flow: AI Recommend → Master → AI Compare (×3 with revisions).
Yields progress to 24 outputs at each step (17 + 5 lockable buttons + history + dsp).
"""
gc.collect() # Free any previous run's data before starting
SKIP = gr.update()
_SLIDER_KEYS = ("lows_db", "mid_boost_db", "highs_db",
"bass_boost_db", "bass_freq_hz", "compression", "stereo_width")
def _yield_progress(progress, sliders=None, ai_vals=SKIP,
audio=SKIP, wf=SKIP, sp=SKIP, dsp=SKIP, stats=SKIP,
cmp_btn=SKIP, dl=SKIP, mdata=SKIP, report=SKIP,
btns=None, history=SKIP):
"""Build the 24-element tuple for each yield."""
s = sliders or (SKIP,) * 7
b = btns or (SKIP,) * 5
return (*s, ai_vals, progress,
audio, wf, sp, dsp, stats, cmp_btn, dl, mdata, report, *b, history)
def _slider_updates(vals):
"""Return 7 gr.Slider updates from a values dict."""
return (
gr.Slider(value=vals["lows_db"]),
gr.Slider(value=vals["mid_boost_db"]),
gr.Slider(value=vals["highs_db"]),
gr.Slider(value=vals["bass_boost_db"]),
gr.Slider(value=vals["bass_freq_hz"]),
gr.Slider(value=vals["compression"]),
gr.Slider(value=vals["stereo_width"]),
)
# --- Validation ---
valid, err, _remaining = _check_ai_key(ai_access_key)
if not valid:
yield _yield_progress(
err,
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
)
return
if audio_path is None:
raise gr.Error("Please upload an audio file first.")
if not os.path.exists(audio_path):
raise gr.Error("Audio file not found — please re-upload your file.")
# Resolve target LUFS
if target_choice == "-14 (Streaming)":
target = -14.0
elif target_choice == "-11 (CD)":
target = -11.0
else:
target = float(custom_lufs)
# === STEP 1: AI Recommend ===
P = "## \U0001f3db Auto Master\n\n" # progress lines (clean, no analysis text)
P += "**Pass 1 of 3**\n\n"
P += "\u23f3 *Analyzing audio with Gemini...*"
yield _yield_progress(P, history=[])
from analysis import recommend_settings, compare_master_structured, compare_master, extract_features
loop_history = [] # accumulate history across passes
try:
result = recommend_settings(audio_path)
except GeminiUnavailableError as e:
yield _yield_progress(
str(e),
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
)
return
if result is None:
yield _yield_progress("*Set GOOGLE_API_KEY to enable AI recommendations.*")
return
if result.get("parse_error"):
yield _yield_progress(result.get("reasoning", "*Could not parse AI response.*"))
return
values = {k: result[k] for k in _SLIDER_KEYS}
P += "\n\n\u2705 AI recommended initial settings"
P += "\n\n\u23f3 *Mastering with initial settings...*"
yield _yield_progress(P, sliders=_slider_updates(values), ai_vals=values)
# === STEP 2: Master pass 1 ===
output_path, original, mastered, sr, stats = master_audio(
audio_path,
values["lows_db"], values["mid_boost_db"], values["highs_db"],
values["bass_boost_db"], values["bass_freq_hz"],
values["compression"], values["stereo_width"], target,
)
P += "\n\n\u2705 Pass 1 mastered"
P += "\n\n\u23f3 *Waiting before next AI call...*"
yield _yield_progress(P)
time.sleep(15)
P += "\n\n\u23f3 *AI analyzing master and recommending adjustments...*"
yield _yield_progress(P)
# === STEP 3: AI Compare (structured) — iteration 1 ===
settings_dict = {**values, "target_lufs": target}
mast_feat = extract_features(mastered, sr)
loop_history.append(_make_history_entry(settings_dict, mast_feat, "Pass 1 initial"))
try:
result2 = compare_master_structured(original, mastered, sr, settings_dict,
history=loop_history)
except GeminiUnavailableError as e:
yield _yield_progress(
str(e),
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
)
return
if result2 is None or result2.get("parse_error"):
P += "\n\n\u26a0\ufe0f Could not parse AI adjustments \u2014 keeping current settings"
else:
values = {k: result2[k] for k in _SLIDER_KEYS}
P += "\n\n\u2705 AI suggested adjustments applied"
del result2
P += "\n\n---\n\n**Pass 2 of 3**\n\n"
P += "\u23f3 *Re-mastering with revised settings...*"
yield _yield_progress(P, sliders=_slider_updates(values), ai_vals=values)
# === STEP 4: Master pass 2 ===
output_path, original, mastered, sr, stats = master_audio(
audio_path,
values["lows_db"], values["mid_boost_db"], values["highs_db"],
values["bass_boost_db"], values["bass_freq_hz"],
values["compression"], values["stereo_width"], target,
)
P += "\n\n\u2705 Pass 2 mastered"
P += "\n\n\u23f3 *Waiting before next AI call...*"
yield _yield_progress(P)
time.sleep(15)
P += "\n\n\u23f3 *AI analyzing and recommending final adjustments...*"
yield _yield_progress(P)
# === STEP 5: AI Compare (structured) — iteration 2 ===
settings_dict = {**values, "target_lufs": target}
mast_feat = extract_features(mastered, sr)
loop_history.append(_make_history_entry(settings_dict, mast_feat, "Pass 2"))
try:
result3 = compare_master_structured(original, mastered, sr, settings_dict,
history=loop_history)
except GeminiUnavailableError as e:
yield _yield_progress(
str(e),
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
)
return
if result3 is None or result3.get("parse_error"):
P += "\n\n\u26a0\ufe0f Could not parse AI adjustments \u2014 keeping current settings"
else:
values = {k: result3[k] for k in _SLIDER_KEYS}
P += "\n\n\u2705 AI suggested final adjustments applied"
del result3
P += "\n\n---\n\n**Pass 3 of 3**\n\n"
P += "\u23f3 *Final mastering pass...*"
yield _yield_progress(P, sliders=_slider_updates(values), ai_vals=values)
# === STEP 6: Master pass 3 (final) ===
output_path, original, mastered, sr, stats = master_audio(
audio_path,
values["lows_db"], values["mid_boost_db"], values["highs_db"],
values["bass_boost_db"], values["bass_freq_hz"],
values["compression"], values["stereo_width"], target,
)
P += "\n\n\u2705 Final master complete"
P += "\n\n\u23f3 *Waiting before final AI call...*"
yield _yield_progress(P)
time.sleep(15)
P += "\n\n\u23f3 *Running final AI analysis...*"
yield _yield_progress(P)
# === STEP 7: Final AI Compare (markdown only) ===
mast_feat = extract_features(mastered, sr)
loop_history.append(_make_history_entry({**values, "target_lufs": target}, mast_feat, "Pass 3 final"))
try:
final_report = compare_master(
original, mastered, sr, {**values, "target_lufs": target},
history=loop_history,
)
except GeminiUnavailableError as e:
# Final report is non-critical — mastering is done
final_report = str(e)
except Exception as e:
final_report = f"*AI comparison unavailable: {e}*"
# Generate plots and stats
waveform_fig = plot_waveform_comparison(original, mastered, sr)
spectrum_fig = plot_spectrum_comparison(original, mastered, sr)
mono_note = "\n\n*Input is mono \u2014 stereo width adjustment was skipped.*" if stats["mono"] else ""
dsp_md = _dsp_settings_md(stats.get("dsp", {}))
stats_md = (
"### Loudness Statistics\n"
"| | LUFS | True Peak (dBTP) |\n"
"|---|---|---|\n"
f"| **Original** | {stats['orig_lufs']:.1f} | {stats['orig_peak']:.1f} |\n"
f"| **Mastered** | {stats['mast_lufs']:.1f} | {stats['mast_peak']:.1f} |\n\n"
f"{mono_note}"
)
master_data = {
"original": original,
"mastered": mastered,
"sr": sr,
"settings": {**values, "target_lufs": target},
}
# Consume one usage credit now that mastering succeeded
_check_ai_key(ai_access_key, consume=True)
P += "\n\n\u2705 **Auto Master complete!** 3 passes, 4 AI analyses."
yield (
gr.Slider(value=values["lows_db"], label="Lows (200 Hz)"),
gr.Slider(value=values["mid_boost_db"], label="Mids (1.2 kHz)"),
gr.Slider(value=values["highs_db"], label="Highs (10 kHz)"),
gr.Slider(value=values["bass_boost_db"], label="Bass Boost (dB)"),
gr.Slider(value=values["bass_freq_hz"], label="Bass Boost Frequency (Hz)"),
gr.Slider(value=values["compression"], label="Compression: Less <-> More"),
gr.Slider(value=values["stereo_width"], label="Stereo Width (%)"),
values, # ai_values_state
P, # ai_reasoning_display
output_path, # ab_player
waveform_fig, # waveform_plot
spectrum_fig, # spectrum_plot
dsp_md, # dsp_display
stats_md, # stats_display
gr.Button("AI Compare Original vs Master",
variant="secondary", visible=True), # ai_compare_btn
gr.DownloadButton("Download Mastered File",
value=output_path, visible=True), # download_file
master_data, # master_data_state
"---\n### AI Post-Master Analysis\n" + final_report, # ai_report_display
gr.Button(interactive=True), # master_btn
gr.Button(interactive=True), # apply_ai_btn
gr.Button(interactive=True), # ai_recommend_btn
gr.Button(interactive=True), # auto_master_btn
gr.Button(interactive=True), # super_ai_btn
loop_history, # analysis_history_state
)
def super_ai_master(audio_path, ai_access_key, target_choice, custom_lufs):
"""Super AI mastering loop with full parametric control (generator).
Flow:
Pass 1: AI analyzes raw audio → full-parametric settings → master
Pass 2: AI compares + adjusts ALL params (small moves) → master
Pass 3: Same as pass 2
Pass 4: Same as pass 2
Pass 5: Final AI compare — report only, no suggestions.
Yields progress to 24 outputs at each step (17 + 5 lockable buttons + history + dsp).
"""
gc.collect() # Free any previous run's data before starting
SKIP = gr.update()
def _yield_progress(progress, sliders=None, ai_vals=SKIP,
audio=SKIP, wf=SKIP, sp=SKIP, dsp=SKIP, stats=SKIP,
cmp_btn=SKIP, dl=SKIP, mdata=SKIP, report=SKIP,
btns=None, history=SKIP):
"""Build the 24-element tuple for each yield."""
s = sliders or (SKIP,) * 7
b = btns or (SKIP,) * 5
return (*s, ai_vals, progress,
audio, wf, sp, dsp, stats, cmp_btn, dl, mdata, report, *b, history)
# --- Validation ---
valid, err, _remaining = _check_ai_key(ai_access_key)
if not valid:
yield _yield_progress(
err,
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
)
return
if audio_path is None:
raise gr.Error("Please upload an audio file first.")
if not os.path.exists(audio_path):
raise gr.Error("Audio file not found — please re-upload your file.")
# Resolve target LUFS
if target_choice == "-14 (Streaming)":
target = -14.0
elif target_choice == "-11 (CD)":
target = -11.0
else:
target = float(custom_lufs)
# === PASS 1: AI full-parametric recommendation ===
P = "## \U0001f9e0 Super AI Master (beta)\n\n"
P += "**Pass 1 of 5** — AI analyzing audio with full parametric control\n\n"
P += "\u23f3 *Deep analysis with Gemini...*"
yield _yield_progress(P, history=[])
from analysis import (super_ai_recommend, super_ai_compare,
super_ai_final_report, extract_features)
loop_history = []
try:
result = super_ai_recommend(audio_path)
except GeminiUnavailableError as e:
yield _yield_progress(
str(e),
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
)
return
if result is None:
yield _yield_progress("*Set GOOGLE_API_KEY to enable AI recommendations.*")
return
if result.get("parse_error"):
yield _yield_progress(result.get("reasoning", "*Could not parse AI response.*"))
return
# Extract the full parameter set (everything except reasoning)
params = {k: v for k, v in result.items() if k != "reasoning"}
P += "\n\n\u2705 AI recommended initial settings"
P += "\n\n\u23f3 *Mastering with full parametric settings...*"
yield _yield_progress(P)
# Master pass 1
output_path, original, mastered, sr, stats = master_audio_full(
audio_path, params, target,
)
mast_feat = extract_features(mastered, sr)
loop_history.append({
"params": params,
"lufs": mast_feat.get("lufs", "?"),
"true_peak": mast_feat.get("true_peak_dbtp", "?"),
"crest_factor": mast_feat.get("crest_factor_db", "?"),
"summary": "Pass 1 initial recommendation",
})
P += "\n\n\u2705 Pass 1 mastered"
# === PASSES 2-4: AI compare + adjust + master ===
for pass_num in range(2, 5):
P += f"\n\n---\n\n**Pass {pass_num} of 5** — AI comparing and refining\n\n"
P += "\u23f3 *Waiting before next AI call...*"
yield _yield_progress(P)
time.sleep(15)
P += "\n\n\u23f3 *AI analyzing master and adjusting parameters...*"
yield _yield_progress(P)
try:
result_cmp = super_ai_compare(
original, mastered, sr, params, target,
history=loop_history,
)
except GeminiUnavailableError as e:
yield _yield_progress(
str(e),
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
)
return
if result_cmp is None or result_cmp.get("parse_error"):
P += "\n\n\u26a0\ufe0f Could not parse AI adjustments — keeping current settings"
else:
params = {k: v for k, v in result_cmp.items() if k != "report"}
P += "\n\n\u2705 AI adjustments applied"
P += f"\n\n\u23f3 *Re-mastering pass {pass_num}...*"
yield _yield_progress(P)
output_path, original, mastered, sr, stats = master_audio_full(
audio_path, params, target,
)
mast_feat = extract_features(mastered, sr)
report_snippet = ""
if result_cmp and not result_cmp.get("parse_error"):
report_snippet = result_cmp.get("report", "")[:200]
loop_history.append({
"params": params,
"lufs": mast_feat.get("lufs", "?"),
"true_peak": mast_feat.get("true_peak_dbtp", "?"),
"crest_factor": mast_feat.get("crest_factor_db", "?"),
"summary": f"Pass {pass_num}: {report_snippet}",
})
P += f"\n\n\u2705 Pass {pass_num} mastered"
# === PASS 5: Final report only ===
P += "\n\n---\n\n**Pass 5 of 5** — Final quality assessment\n\n"
P += "\u23f3 *Waiting before final AI call...*"
yield _yield_progress(P)
time.sleep(15)
P += "\n\n\u23f3 *Running final AI evaluation...*"
yield _yield_progress(P)
try:
final_report = super_ai_final_report(
original, mastered, sr, params, target,
history=loop_history,
)
except GeminiUnavailableError as e:
# Pass 5 is report-only — mastering is done, so we can still
# show results even if the report fails. Don't abort here.
final_report = str(e)
except Exception as e:
final_report = f"*AI final report unavailable: {e}*"
# Generate plots and stats
waveform_fig = plot_waveform_comparison(original, mastered, sr)
spectrum_fig = plot_spectrum_comparison(original, mastered, sr)
mono_note = "\n\n*Input is mono — stereo width adjustment was skipped.*" if stats["mono"] else ""
dsp_md = _dsp_settings_md(stats.get("dsp", {}))
stats_md = (
"### Loudness Statistics\n"
"| | LUFS | True Peak (dBTP) |\n"
"|---|---|---|\n"
f"| **Original** | {stats['orig_lufs']:.1f} | {stats['orig_peak']:.1f} |\n"
f"| **Mastered** | {stats['mast_lufs']:.1f} | {stats['mast_peak']:.1f} |\n\n"
f"{mono_note}"
)
# Super AI uses full parametric — don't map back to sliders
# (sliders have fixed ranges that can't represent the full parameter space)
master_data = {
"original": original,
"mastered": mastered,
"sr": sr,
"settings": {"target_lufs": target},
}
# Consume one usage credit now that mastering succeeded
_check_ai_key(ai_access_key, consume=True)
P += "\n\n\u2705 **Super AI Master complete!** 5 passes, full parametric control."
# Build the full settings report for display
eq = params.get("eq", {})
comp = params.get("compression", {})
settings_report = "---\n### Super AI — Applied Settings\n"
settings_report += f"**HPF:** {params.get('hpf_freq', 15)} Hz\n\n"
settings_report += "**EQ:**\n"
for bk in ("band1", "band2", "band3", "band4", "band5", "band6"):
b = eq.get(bk, {})
if abs(b.get("gain_db", 0)) < 0.01:
settings_report += f"- {bk}: bypassed\n"
else:
settings_report += (f"- {bk}: {b.get('type','peak')} @ {b.get('freq',1000):.0f} Hz, "
f"{b.get('gain_db',0):+.1f} dB, Q={b.get('q',1.0):.2f}\n")
settings_report += f"\n**Crossovers:** {params.get('crossover_low', 200)} / {params.get('crossover_high', 4000)} Hz\n\n"
settings_report += "**Compression:**\n"
for bk in ("low", "mid", "high"):
bp = comp.get(bk, {})
settings_report += (f"- {bk}: threshold {bp.get('threshold',-14):.1f} dB, "
f"ratio {bp.get('ratio',1.0):.1f}:1, "
f"attack {bp.get('attack_ms',30):.1f} ms, "
f"release {bp.get('release_ms',150):.1f} ms\n")
settings_report += f"\n**Stereo Width:** {params.get('stereo_width', 100)}%\n\n"
# Final yield — Super AI doesn't update sliders (params exceed slider ranges)
SKIP = gr.update()
yield (
SKIP, SKIP, SKIP, SKIP, SKIP, SKIP, SKIP, # leave sliders unchanged
{}, # ai_values_state
P, # ai_reasoning_display
output_path, # ab_player
waveform_fig, # waveform_plot
spectrum_fig, # spectrum_plot
dsp_md, # dsp_display
stats_md, # stats_display
gr.Button("AI Compare Original vs Master",
variant="secondary", visible=True), # ai_compare_btn
gr.DownloadButton("Download Mastered File",
value=output_path, visible=True), # download_file
master_data, # master_data_state
settings_report + "---\n### AI Final Report\n" + final_report, # ai_report_display
gr.Button(interactive=True), # master_btn
gr.Button(interactive=True), # apply_ai_btn
gr.Button(interactive=True), # ai_recommend_btn
gr.Button(interactive=True), # auto_master_btn
gr.Button(interactive=True), # super_ai_btn
loop_history, # analysis_history_state
)
# ---------------------------------------------------------------------------
# UI
# ---------------------------------------------------------------------------
with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
gr.HTML(
'<div style="display:flex;align-items:center;gap:16px;">'
'<img src="data:image/png;base64,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" alt="s.AI logo" style="height:80px;border-radius:12px;">'
'<div>'
'<h1 style="margin:0;">Audio Mastering Suite</h1>'
'<p style="margin:4px 0 0 0;font-size:0.9em;">by AnimalMonk &nbsp;|&nbsp; '
'<a href="https://discord.gg/StudioAI" target="_blank">Join us on Discord</a></p>'
'</div></div>'
)
# --- Preset, AI Recommend & Target LUFS ---
with gr.Row():
preset_dropdown = gr.Dropdown(
label="Preset",
choices=list(PRESETS.keys()),
value="-- None --",
)
ai_access_input = gr.Textbox(
label="AI Access Key",
placeholder="Enter key to unlock AI",
type="password",
scale=1,
autofocus=False,
elem_id="ai-access-key",
)
ai_recommend_btn = gr.Button("AI Recommend", variant="secondary")
auto_master_btn = gr.Button("Auto Master", variant="primary")
target_dropdown = gr.Dropdown(
label="Target LUFS",
choices=["-14 (Streaming)", "-11 (CD)", "Custom"],
value="-14 (Streaming)",
)
custom_lufs_input = gr.Number(
label="Custom LUFS",
value=-14.0,
visible=False,
)
super_ai_btn = gr.Button("\U0001f9e0 Super AI (beta)", variant="primary", size="lg")
key_status_display = gr.Markdown("")
gr.HTML(
f'<p style="margin:0;font-size:14px;color:#ccc;">'
f'<a href="{_PRICING_URL}" target="_blank" rel="noopener" '
f'style="color:#7c6cf0;font-weight:bold;text-decoration:underline;">'
f'Get an API Key</a>'
f' &mdash; plans start at $9/month. '
f'<em>Manual mastering is always free.</em></p>'
)
# Disable browser password autofill on the AI key field
gr.HTML("""<script>
document.addEventListener('DOMContentLoaded', function() {
var el = document.querySelector('#ai-access-key input');
if (el) { el.autocomplete = 'off'; el.setAttribute('autocomplete', 'off'); }
});
// Gradio may render late — retry after a short delay
setTimeout(function() {
var el = document.querySelector('#ai-access-key input');
if (el) { el.autocomplete = 'off'; el.setAttribute('autocomplete', 'off'); }
}, 1500);
</script>""", visible=False)
# --- Upload (full width) ---
audio_input = gr.Audio(
label="Upload Audio (WAV / MP3)",
type="filepath",
sources=["upload"],
)
# --- AI Recommendations (below upload) ---
ai_reasoning_display = gr.Markdown(value="", visible=True)
# --- Apply AI Recommended Settings (below AI reasoning) ---
apply_ai_btn = gr.Button("Apply AI Recommended Settings", variant="secondary")
# --- Control sliders ---
with gr.Row():
lows_slider = gr.Slider(
label="Lows (200 Hz)",
minimum=-3.0, maximum=3.0, value=0.0, step=0.1,
)
mid_boost_slider = gr.Slider(
label="Mids (1.2 kHz)",
minimum=-3.0, maximum=3.0, value=0.0, step=0.1,
)
highs_slider = gr.Slider(
label="Highs (10 kHz)",
minimum=-3.0, maximum=3.0, value=0.0, step=0.1,
)
with gr.Row():
with gr.Column():
bass_boost_slider = gr.Slider(
label="Bass Boost (dB)",
minimum=0.0, maximum=3.0, value=0.0, step=0.1,
)
bass_freq_slider = gr.Slider(
label="Bass Boost Frequency (Hz)",
minimum=40, maximum=100, value=60, step=1,
)
with gr.Column():
comp_slider = gr.Slider(
label="Compression: Less <-> More",
minimum=0, maximum=100, value=0, step=1,
)
width_slider = gr.Slider(
label="Stereo Width (%)",
minimum=80, maximum=150, value=100, step=1,
)
# --- State for AI values, master data, and analysis history ---
master_data_state = gr.State({})
ai_values_state = gr.State({})
analysis_history_state = gr.State([])
# --- Master button ---
master_btn = gr.Button("Master It!", variant="primary", size="lg")
# --- AI Post-Master Comparison (above playback) ---
ai_report_display = gr.Markdown(value="", visible=True, label="AI Analysis")
# --- Playback ---
ab_player = gr.Audio(label="Mastered", interactive=False)
ai_compare_btn = gr.Button("AI Compare Original vs Master", variant="secondary", visible=False)
download_file = gr.DownloadButton("Download Mastered File", visible=False)
# --- Visualization ---
with gr.Row():
waveform_plot = gr.Plot(label="Waveform Comparison")
spectrum_plot = gr.Plot(label="Spectrum Comparison")
with gr.Row():
dsp_display = gr.Markdown()
stats_display = gr.Markdown()
# --- Event wiring ---
preset_dropdown.change(
apply_preset,
inputs=[preset_dropdown],
outputs=[lows_slider, mid_boost_slider, highs_slider,
bass_boost_slider, bass_freq_slider,
comp_slider, width_slider],
)
target_dropdown.change(
toggle_custom_lufs,
inputs=[target_dropdown],
outputs=[custom_lufs_input],
)
ai_access_input.change(
check_key_status,
inputs=[ai_access_input],
outputs=[key_status_display],
)
ai_recommend_btn.click(
ai_recommend,
inputs=[audio_input, ai_access_input],
outputs=[
lows_slider, mid_boost_slider, highs_slider,
bass_boost_slider, bass_freq_slider,
comp_slider, width_slider,
ai_values_state,
ai_reasoning_display,
master_btn, apply_ai_btn, ai_recommend_btn,
auto_master_btn, ai_compare_btn, super_ai_btn,
],
)
apply_ai_btn.click(
apply_ai,
inputs=[ai_values_state],
outputs=[
lows_slider, mid_boost_slider, highs_slider,
bass_boost_slider, bass_freq_slider,
comp_slider, width_slider,
],
)
master_btn.click(
process,
inputs=[
audio_input, lows_slider, mid_boost_slider, highs_slider,
bass_boost_slider, bass_freq_slider,
comp_slider, width_slider,
target_dropdown, custom_lufs_input,
],
outputs=[
ab_player,
waveform_plot, spectrum_plot,
dsp_display, stats_display,
ai_compare_btn, download_file,
master_data_state,
ai_report_display,
],
)
ai_compare_btn.click(
ai_compare,
inputs=[master_data_state, ai_access_input, analysis_history_state],
outputs=[
lows_slider, mid_boost_slider, highs_slider,
bass_boost_slider, bass_freq_slider,
comp_slider, width_slider,
ai_values_state,
ai_report_display,
analysis_history_state,
master_btn, apply_ai_btn, ai_recommend_btn,
auto_master_btn, ai_compare_btn, super_ai_btn,
],
)
auto_master_btn.click(
auto_master,
inputs=[audio_input, ai_access_input, target_dropdown, custom_lufs_input],
outputs=[
lows_slider, mid_boost_slider, highs_slider,
bass_boost_slider, bass_freq_slider,
comp_slider, width_slider,
ai_values_state,
ai_reasoning_display,
ab_player,
waveform_plot, spectrum_plot,
dsp_display, stats_display,
ai_compare_btn, download_file,
master_data_state,
ai_report_display,
master_btn, apply_ai_btn, ai_recommend_btn,
auto_master_btn, super_ai_btn,
analysis_history_state,
],
)
super_ai_btn.click(
super_ai_master,
inputs=[audio_input, ai_access_input, target_dropdown, custom_lufs_input],
outputs=[
lows_slider, mid_boost_slider, highs_slider,
bass_boost_slider, bass_freq_slider,
comp_slider, width_slider,
ai_values_state,
ai_reasoning_display,
ab_player,
waveform_plot, spectrum_plot,
dsp_display, stats_display,
ai_compare_btn, download_file,
master_data_state,
ai_report_display,
master_btn, apply_ai_btn, ai_recommend_btn,
auto_master_btn, super_ai_btn,
analysis_history_state,
],
)
if __name__ == "__main__":
import os as _os
_os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
demo.launch(server_name="0.0.0.0", server_port=7860, share=False, ssr_mode=False)