AnimalMonk/audio-mastering-suite
3
1# Build: 2026-03-21T15:25:58.145163+00:002"""AI-powered audio analysis using Gemini Pro — feature extraction and recommendations."""3 4import json5import os6import numpy as np7from scipy.signal import welch8 9 10# ---------------------------------------------------------------------------11# Audio feature extraction12# ---------------------------------------------------------------------------13 14_BANDS = [15 ("Sub-bass", 20, 60),16 ("Bass", 60, 250),17 ("Low-Mids", 250, 500),18 ("Mids", 500, 2000),19 ("Upper-Mids", 2000, 6000),20 ("Highs", 6000, 20000),21]22 23 24def extract_features(audio, sample_rate):25 """Extract audio features for AI analysis (basic — used by Auto Master).26 27 Args:28 audio: numpy array, shape (samples,) or (samples, channels).29 sample_rate: int.30 31 Returns:32 dict with spectral, dynamic, and stereo measurements.33 """34 # Convert to mono for spectral analysis35 if audio.ndim == 2:36 mono = audio.mean(axis=1)37 else:38 mono = audio39 40 # --- Spectral analysis via Welch ---41 nperseg = min(8192, len(mono))42 freqs, psd = welch(mono, fs=sample_rate, nperseg=nperseg)43 44 # Spectral centroid45 total_energy = np.sum(psd)46 if total_energy > 0:47 spectral_centroid = float(np.sum(freqs * psd) / total_energy)48 else:49 spectral_centroid = 0.050 51 # Spectral rolloff (85%)52 cumulative = np.cumsum(psd)53 if total_energy > 0:54 rolloff_idx = np.searchsorted(cumulative, 0.85 * total_energy)55 spectral_rolloff = float(freqs[min(rolloff_idx, len(freqs) - 1)])56 else:57 spectral_rolloff = 0.058 59 # Band energy distribution (dB) — use float() to avoid numpy float3260 band_energy = {}61 for name, lo, hi in _BANDS:62 mask = (freqs >= lo) & (freqs < hi)63 band_rms = float(np.sqrt(np.mean(psd[mask]))) if np.any(mask) else 0.064 if band_rms > 0:65 band_energy[name] = round(20.0 * np.log10(band_rms), 1)66 else:67 band_energy[name] = -100.068 69 # --- Dynamics (cast to Python float for JSON serialization) ---70 rms = float(np.sqrt(np.mean(mono ** 2)))71 peak = float(np.max(np.abs(mono)))72 73 rms_db = round(20.0 * np.log10(rms), 1) if rms > 0 else -100.074 peak_db = round(20.0 * np.log10(peak), 1) if peak > 0 else -100.075 crest_factor = round(peak_db - rms_db, 1)76 dynamic_range = crest_factor # simplified: same as crest factor for full-file77 78 # --- Stereo correlation ---79 is_mono = audio.ndim == 1 or audio.shape[1] == 180 if not is_mono:81 left = audio[:, 0]82 right = audio[:, 1]83 correlation = np.corrcoef(left, right)[0, 1]84 stereo_correlation = round(float(correlation), 3)85 else:86 stereo_correlation = None87 88 # --- Loudness (reuse existing functions, lazy import) ---89 from loudness import measure_loudness, measure_true_peak90 lufs = measure_loudness(audio, sample_rate)91 true_peak = measure_true_peak(audio, sample_rate)92 93 return {94 "spectral_centroid_hz": round(float(spectral_centroid), 1),95 "spectral_rolloff_hz": round(float(spectral_rolloff), 1),96 "band_energy": band_energy,97 "rms_db": float(rms_db),98 "peak_db": float(peak_db),99 "crest_factor_db": float(crest_factor),100 "dynamic_range_db": float(dynamic_range),101 "stereo_correlation": stereo_correlation,102 "lufs": round(float(lufs), 1) if not np.isinf(lufs) else -100.0,103 "true_peak_dbtp": float(true_peak),104 "is_mono": is_mono,105 }106 107 108# ---------------------------------------------------------------------------109# Detailed feature extraction — Super AI mode110# ---------------------------------------------------------------------------111 112# 24 analysis bands for fine-grained spectral view113_DETAIL_BANDS = [114 ("20-40", 20, 40),115 ("40-60", 40, 60),116 ("60-100", 60, 100),117 ("100-150", 100, 150),118 ("150-200", 150, 200),119 ("200-300", 200, 300),120 ("300-400", 300, 400),121 ("400-600", 400, 600),122 ("600-800", 600, 800),123 ("800-1k", 800, 1000),124 ("1k-1.5k", 1000, 1500),125 ("1.5k-2k", 1500, 2000),126 ("2k-3k", 2000, 3000),127 ("3k-4k", 3000, 4000),128 ("4k-5k", 4000, 5000),129 ("5k-6k", 5000, 6000),130 ("6k-8k", 6000, 8000),131 ("8k-10k", 8000, 10000),132 ("10k-12k", 10000, 12000),133 ("12k-16k", 12000, 16000),134 ("16k-20k", 16000, 20000),135]136 137# 3 compression bands matching the DSP crossover defaults138_COMP_BANDS = [139 ("low", 20, 200),140 ("mid", 200, 4000),141 ("high", 4000, 20000),142]143 144 145def extract_features_detailed(audio, sample_rate):146 """Extract rich spectral + dynamic features for Super AI mode.147 148 Builds on extract_features() and adds:149 - 24-band spectral profile (fine-grained EQ map)150 - Spectral peak/resonance detection (top problematic frequencies)151 - Per-compression-band dynamics (RMS, peak, crest factor)152 - Spectral flatness (tonal vs noisy character)153 - Spectral tilt (bass-heavy vs bright)154 - Short-time dynamic variation (verse vs chorus energy)155 - Per-band stereo correlation156 157 All numpy — runs in milliseconds on CPU.158 """159 from scipy.signal import find_peaks160 161 base = extract_features(audio, sample_rate)162 163 if audio.ndim == 2:164 mono = audio.mean(axis=1)165 else:166 mono = audio167 168 # --- High-resolution spectral analysis ---169 nperseg = min(16384, len(mono))170 freqs, psd = welch(mono, fs=sample_rate, nperseg=nperseg)171 172 # 24-band spectral profile (dB)173 spectral_profile = {}174 for name, lo, hi in _DETAIL_BANDS:175 mask = (freqs >= lo) & (freqs < hi)176 if np.any(mask):177 band_rms = float(np.sqrt(np.mean(psd[mask])))178 spectral_profile[name] = round(20.0 * np.log10(max(band_rms, 1e-12)), 1)179 else:180 spectral_profile[name] = -100.0181 182 # --- Spectral peaks / resonances ---183 # Smooth the PSD, find prominent peaks184 psd_db = 10.0 * np.log10(np.maximum(psd, 1e-20))185 # Use a wider window for smoothing to avoid noise peaks186 kernel_size = min(31, len(psd_db) // 4)187 if kernel_size % 2 == 0:188 kernel_size += 1189 if kernel_size >= 3:190 kernel = np.ones(kernel_size) / kernel_size191 psd_smooth = np.convolve(psd_db, kernel, mode="same")192 else:193 psd_smooth = psd_db194 195 # Find peaks that stand out above the smoothed curve196 prominence_threshold = 3.0 # at least 3 dB above neighbors197 peak_indices, peak_props = find_peaks(198 psd_db,199 prominence=prominence_threshold,200 distance=max(1, int(50 / (freqs[1] - freqs[0]))) # at least 50 Hz apart201 )202 203 # Sort by prominence and take top 8204 if len(peak_indices) > 0:205 prominences = peak_props["prominences"]206 top_idx = np.argsort(prominences)[::-1][:8]207 resonances = []208 for idx in top_idx:209 pi = peak_indices[idx]210 if freqs[pi] >= 30: # skip sub-bass noise211 resonances.append({212 "freq_hz": round(float(freqs[pi]), 1),213 "level_db": round(float(psd_db[pi]), 1),214 "prominence_db": round(float(prominences[idx]), 1),215 })216 else:217 resonances = []218 219 # --- Spectral flatness (Wiener entropy) ---220 # 1.0 = white noise, 0.0 = pure tone221 psd_pos = psd[psd > 0]222 if len(psd_pos) > 0:223 geo_mean = np.exp(np.mean(np.log(psd_pos)))224 arith_mean = np.mean(psd_pos)225 spectral_flatness = round(float(geo_mean / arith_mean), 4)226 else:227 spectral_flatness = 0.0228 229 # --- Spectral tilt (slope of energy across frequency) ---230 # Negative = bass-heavy, positive = bright231 if len(freqs) > 1 and np.any(psd > 0):232 log_freqs = np.log10(np.maximum(freqs[1:], 1.0)) # skip DC233 log_psd = 10.0 * np.log10(np.maximum(psd[1:], 1e-20))234 coeffs = np.polyfit(log_freqs, log_psd, 1)235 spectral_tilt = round(float(coeffs[0]), 2) # dB/decade236 else:237 spectral_tilt = 0.0238 239 # --- Per-compression-band dynamics ---240 comp_band_dynamics = {}241 for name, lo, hi in _COMP_BANDS:242 mask = (freqs >= lo) & (freqs < hi)243 if np.any(mask):244 band_psd = psd[mask]245 band_rms = float(np.sqrt(np.mean(band_psd)))246 band_peak = float(np.sqrt(np.max(band_psd)))247 rms_db = round(20.0 * np.log10(max(band_rms, 1e-12)), 1)248 peak_db = round(20.0 * np.log10(max(band_peak, 1e-12)), 1)249 comp_band_dynamics[name] = {250 "rms_db": rms_db,251 "peak_db": peak_db,252 "crest_db": round(peak_db - rms_db, 1),253 }254 else:255 comp_band_dynamics[name] = {"rms_db": -100.0, "peak_db": -100.0, "crest_db": 0.0}256 257 # --- Short-time dynamic variation ---258 # Split audio into ~4-second chunks and measure RMS of each259 chunk_samples = int(4.0 * sample_rate)260 n_chunks = max(1, len(mono) // chunk_samples)261 chunk_rms_list = []262 for i in range(n_chunks):263 chunk = mono[i * chunk_samples : (i + 1) * chunk_samples]264 c_rms = float(np.sqrt(np.mean(chunk ** 2)))265 if c_rms > 0:266 chunk_rms_list.append(20.0 * np.log10(c_rms))267 else:268 chunk_rms_list.append(-100.0)269 270 if len(chunk_rms_list) > 1:271 chunk_arr = np.array(chunk_rms_list)272 dynamic_variation = {273 "min_rms_db": round(float(np.min(chunk_arr)), 1),274 "max_rms_db": round(float(np.max(chunk_arr)), 1),275 "range_db": round(float(np.max(chunk_arr) - np.min(chunk_arr)), 1),276 "std_db": round(float(np.std(chunk_arr)), 2),277 "n_chunks": n_chunks,278 }279 else:280 dynamic_variation = {281 "min_rms_db": chunk_rms_list[0] if chunk_rms_list else -100.0,282 "max_rms_db": chunk_rms_list[0] if chunk_rms_list else -100.0,283 "range_db": 0.0, "std_db": 0.0, "n_chunks": 1,284 }285 286 # --- Per-band stereo correlation ---287 is_mono = audio.ndim == 1 or audio.shape[1] == 1288 stereo_band_corr = {}289 if not is_mono:290 from scipy.signal import butter, sosfilt291 292 left = audio[:, 0].astype(np.float64)293 right = audio[:, 1].astype(np.float64)294 295 band_edges = [(20, 200), (200, 2000), (2000, 8000), (8000, min(20000, sample_rate // 2 - 1))]296 band_names = ["low", "low_mid", "high_mid", "high"]297 298 for bname, lo, hi in zip(band_names, *zip(*band_edges)):299 try:300 sos = butter(4, [lo, hi], btype="band", fs=sample_rate, output="sos")301 l_filt = sosfilt(sos, left)302 r_filt = sosfilt(sos, right)303 corr = np.corrcoef(l_filt, r_filt)[0, 1]304 stereo_band_corr[bname] = round(float(corr), 3)305 except Exception:306 stereo_band_corr[bname] = None307 308 # --- Merge into base features ---309 base["spectral_profile_24band"] = spectral_profile310 base["resonances"] = resonances311 base["spectral_flatness"] = spectral_flatness312 base["spectral_tilt_db_per_decade"] = spectral_tilt313 base["comp_band_dynamics"] = comp_band_dynamics314 base["dynamic_variation"] = dynamic_variation315 base["stereo_band_correlation"] = stereo_band_corr if not is_mono else None316 317 return base318 319 320# ---------------------------------------------------------------------------321# Gemini API wrapper322# ---------------------------------------------------------------------------323 324class GeminiUnavailableError(Exception):325 """Raised when Gemini API is unavailable after retries."""326 pass327 328 329def _call_gemini(system_prompt, user_prompt):330 """Call Gemini 2.5 Pro via OpenRouter (preferred) or Google direct API.331 332 Uses OPENROUTER_API_KEY if set, otherwise falls back to GOOGLE_API_KEY.333 Retries up to 3 times on server errors (5xx) with escalating delays.334 Raises GeminiUnavailableError on persistent failure.335 """336 import time337 import requests as _requests338 339 openrouter_key = os.environ.get("OPENROUTER_API_KEY")340 google_key = os.environ.get("GOOGLE_API_KEY")341 342 if not openrouter_key and not google_key:343 return None344 345 if openrouter_key:346 # OpenRouter — OpenAI-compatible format347 url = "https://openrouter.ai/api/v1/chat/completions"348 headers = {349 "Authorization": f"Bearer {openrouter_key}",350 "Content-Type": "application/json",351 }352 payload = {353 "model": "google/gemini-2.5-pro",354 "messages": [355 {"role": "system", "content": system_prompt},356 {"role": "user", "content": user_prompt},357 ],358 }359 360 def _parse_response(data):361 return data["choices"][0]["message"]["content"]362 else:363 # Google direct API364 url = (365 "https://generativelanguage.googleapis.com/v1beta/models/"366 f"gemini-2.5-pro:generateContent?key={google_key}"367 )368 headers = {"Content-Type": "application/json"}369 payload = {370 "system_instruction": {"parts": [{"text": system_prompt}]},371 "contents": [{"role": "user", "parts": [{"text": user_prompt}]}],372 }373 374 def _parse_response(data):375 return data["candidates"][0]["content"]["parts"][0]["text"]376 377 last_error = None378 retry_delays = [10, 20, 30] # 3 retries: wait 10s, 20s, 30s379 for attempt in range(4): # initial + 3 retries380 try:381 resp = _requests.post(url, headers=headers, json=payload, timeout=120)382 resp.raise_for_status()383 data = resp.json()384 return _parse_response(data)385 except _requests.exceptions.HTTPError as e:386 last_error = e387 status = getattr(resp, "status_code", 0)388 if status >= 500 and attempt < 3:389 time.sleep(retry_delays[attempt])390 continue391 break392 except Exception as e:393 last_error = e394 if attempt == 0:395 time.sleep(5)396 continue397 break398 399 provider = "OpenRouter" if openrouter_key else "Google Gemini"400 raise GeminiUnavailableError(401 f"**{provider} is temporarily unavailable.**\n\n"402 "This is an issue with the AI provider's servers, not with StudioAI.\n\n"403 "Please try again in a few minutes. If the problem persists, "404 "try again in a few hours.\n\n"405 "No usage credit was consumed for this attempt.\n\n"406 f"*Technical details: {last_error}*"407 )408 409 410# ---------------------------------------------------------------------------411# Phase 1: AI-recommended settings412# ---------------------------------------------------------------------------413 414_SIGNAL_FLOW = """415SIGNAL FLOW (fixed processing order):4161. PRE-GAIN DROP — Input is normalized to -18 LUFS internal working level (stepped attenuator). This prevents EQ clipping on hot masters.4172. HPF 15 Hz — Always-on 2nd-order Butterworth high-pass filter (12 dB/oct). Subsonic cleanup only; -3 dB at 15 Hz, negligible loss above 35 Hz. There is NO low-pass filter — source is band-limited by sample rate (Nyquist).4183. 4-BAND PARAMETRIC EQ (user-adjustable):419 - Bass Boost: Peak filter (Q=2.0), variable center 40-100 Hz, range 0 to +3.0 dB, step 0.5 dB420 - Lows: Low shelf at 200 Hz (Q=1.0), range -3.0 to +3.0 dB, step 0.5 dB421 - Highs: High shelf at 10 kHz (Q=0.7, gentle slope), range -3.0 to +3.0 dB, step 0.5 dB — this is the "air" band, no LPF to fight it422 - Mids: Peak filter at 1.2 kHz (Q=1.0, wide bell), range -3.0 to +3.0 dB, step 0.1 dB4234. MULTIBAND COMPRESSION — 3-band dynamics processing (no makeup gain):424 - Placed before stereo width so the compressor sees EQ'd audio without M/S side-channel energy affecting per-band behaviour.425 - Two Linkwitz-Riley 4th-order crossovers split the signal at 200 Hz and 4 kHz.426 - LOGARITHMIC SLIDER CURVE: The 0-100 slider uses a quadratic (t²) mapping — the bottom half of the slider (0-50) covers the transparent-to-light range, while aggressive compression is concentrated in the top 30% (70-100). This gives fine control in the musical "sweet spot."427 - LOW band (< 200 Hz): Firmest control. Attack scales 80→20ms (lets kick breathe at low settings, catches bass transients at high). Ratio 1.2:1→3.0:1, threshold -16→-24 dB, release 200→120ms.428 - MID band (200 Hz – 4 kHz): Musical peak control. Attack scales 30→10ms (transparent at low settings, tames snare/vocal peaks at high). Ratio 1.1:1→2.5:1, threshold -14→-24 dB, release 150→100ms.429 - HIGH band (> 4 kHz): Transient control. Attack scales 10→3ms (light de-essing at low, catches cymbal/click transients at high). Ratio 1.05:1→2.0:1, threshold -12→-22 dB, release 80→40ms.430 - TRUE BYPASS when slider = 0 (no compressor in chain at all).431 - Higher compression values actively reduce the crest factor (peak-to-loudness ratio), which allows LUFS normalization to push louder without the true peak ceiling pulling level back down.432 - Bands are summed back to full-range after compression. No makeup gain — LUFS normalization handles level.4335. STEREO WIDTH — Frequency-selective M/S matrix (after dynamics for stable imaging):434 - Linkwitz-Riley 4th-order crossover at 200 Hz splits signal into low band and high band435 - Low band (< 200 Hz): untouched — keeps bass mono-safe436 - High band (≥ 200 Hz): M/S encode → width scaling → M/S decode437 - Energy-preserving: mid_scale = sqrt(2/(1+w²)), side_scale = w × mid_scale438 - Range: 80% (narrow) to 150% (wide). 100% = no change. Clip protection after summing.4396. LUFS NORMALIZATION — Measures integrated loudness (ITU-R BS.1770-4, K-weighted, gated) and applies uniform linear gain to hit the target LUFS exactly. Targets: -14 (streaming), -11 (CD), or custom.4407. SOFT CLIPPER — Piecewise tanh saturation after LUFS normalization. A knee sits 2 dB below the -0.1 dBTP ceiling. Everything below the knee is perfectly linear (zero processing). Only the tips of peaks above the knee are shaped with a tanh curve that asymptotes to the ceiling. This is NOT a limiter — it's analog-style waveshaping that typically affects only the top 1-2 dB of the loudest transients. LUFS is preserved (transient tips contribute almost nothing to integrated loudness) while true-peak is reduced significantly.4418. TRUE PEAK CEILING (safety net) — After the soft clipper, the true peak (4x oversampled, ITU-R BS.1770) is measured. If any residual inter-sample peaks still exceed -0.1 dBTP, the entire signal is scaled down by exactly the overshoot. This rarely engages thanks to the soft clipper, but guarantees compliance.442"""443 444_RECOMMEND_SYSTEM = f"""You are an expert audio mastering engineer. Analyze the audio measurements below and recommend optimal mastering settings for this tool.445 446{_SIGNAL_FLOW}447 448AVAILABLE CONTROLS (these are the ONLY parameters you can recommend):449- lows_db: Low shelf at 200 Hz, -3.0 to +3.0 dB450- mid_boost_db: Peak at 1.2 kHz (Q=1.0), -3.0 to +3.0 dB451- highs_db: High shelf at 10 kHz (Q=0.7), -3.0 to +3.0 dB452- bass_boost_db: Peak (Q=2.0), 0 to +3.0 dB453- bass_freq_hz: Center freq for bass boost, 40-100 Hz454- compression: 0 (bypass/off) to 100 (heavy). 0 = true bypass (no processing)455- stereo_width: 80-150%. 100 = no change. Only affects frequencies above 200 Hz.456 457IMPORTANT CONTEXT:458- The 15 Hz HPF is always active and cannot be adjusted — do not try to compensate for it.459- There is no LPF, so the 10 kHz high shelf has full authority over the air band with no interference.460- LUFS normalization at the end restores loudness automatically — do not worry about overall level, focus on spectral balance and dynamics.461- TRUE PEAK CEILING: A -0.1 dBTP ceiling is enforced after LUFS normalization. If the audio has a high crest factor (large peaks relative to loudness), the ceiling will pull the final level below the target LUFS. To allow the track to hit the target LUFS, recommend enough compression to reduce the crest factor. Look at the crest_factor_db measurement — values above ~10 dB suggest compression in the 40-70 range; above ~14 dB may need 60-85.462- The compression slider uses a LOGARITHMIC (quadratic) curve. Slider values 0-50 cover subtle/transparent compression. Values 50-75 are moderate. Values 75-100 are aggressive. Recommend accordingly — a slider value of 30 is very light, 50 is moderate, 70+ is firm.463- If the audio already sounds well-balanced, recommend conservative or zero settings. Not everything needs processing.464 465Return ONLY a valid JSON object with these exact keys. The "reasoning" field must contain your actual markdown explanation (3-5 bullet points explaining why you chose these values):466{{467 "lows_db": number,468 "mid_boost_db": number,469 "highs_db": number,470 "bass_boost_db": number,471 "bass_freq_hz": integer,472 "compression": integer,473 "stereo_width": integer,474 "reasoning": "### AI Analysis\\n- **Lows:** reason for lows_db choice\\n- **Highs:** reason for highs_db choice\\n- ... (write your actual analysis here, do NOT return this template literally)"475}}476 477Keep values within the valid ranges. Be conservative — subtle moves are better than aggressive ones."""478 479 480def _clamp_settings(d):481 """Clamp AI-returned slider values to valid ranges in-place and return *d*."""482 d["lows_db"] = max(-3.0, min(3.0, float(d.get("lows_db", 0))))483 d["mid_boost_db"] = max(-3.0, min(3.0, float(d.get("mid_boost_db", 0))))484 d["highs_db"] = max(-3.0, min(3.0, float(d.get("highs_db", 0))))485 d["bass_boost_db"] = max(0, min(3.0, float(d.get("bass_boost_db", 0))))486 d["bass_freq_hz"] = max(40, min(100, int(d.get("bass_freq_hz", 60))))487 d["compression"] = max(0, min(100, int(d.get("compression", 50))))488 d["stereo_width"] = max(80, min(150, int(d.get("stereo_width", 100))))489 return d490 491 492def _strip_json(text):493 """Strip markdown code fences from a JSON response and parse it."""494 text = text.strip()495 if text.startswith("```"):496 lines = text.split("\n")497 text = "\n".join(lines[1:-1])498 return json.loads(text)499 500 501def recommend_settings(audio_path):502 """Analyze raw audio and return AI-recommended mastering settings.503 504 Args:505 audio_path: path to the uploaded audio file.506 507 Returns:508 dict with recommended slider values and reasoning markdown,509 or None if AI is unavailable.510 """511 from dsp import load_audio512 audio, sr = load_audio(audio_path)513 features = extract_features(audio, sr)514 515 user_prompt = f"""Analyze this audio and recommend mastering settings:516 517**Audio Measurements:**518- Integrated Loudness: {features['lufs']} LUFS519- True Peak: {features['true_peak_dbtp']} dBTP520- RMS Level: {features['rms_db']} dB521- Crest Factor: {features['crest_factor_db']} dB522- Spectral Centroid: {features['spectral_centroid_hz']} Hz523- Spectral Rolloff (85%): {features['spectral_rolloff_hz']} Hz524- Stereo Correlation: {features['stereo_correlation'] if features['stereo_correlation'] is not None else 'N/A (mono)'}525- Mono: {features['is_mono']}526 527**Band Energy (dB):**528{chr(10).join(f'- {k}: {v} dB' for k, v in features['band_energy'].items())}529 530Return the JSON object with recommended settings."""531 532 response = _call_gemini(_RECOMMEND_SYSTEM, user_prompt)533 if response is None:534 return None535 536 # Parse JSON from response (Gemini may wrap it in markdown code fence)537 try:538 result = _strip_json(response)539 _clamp_settings(result)540 541 if "reasoning" not in result:542 result["reasoning"] = "*No explanation provided.*"543 544 return result545 except (json.JSONDecodeError, KeyError, TypeError):546 return {"reasoning": response, "parse_error": True}547 548 549# ---------------------------------------------------------------------------550# Phase 2: Post-master comparison report551# ---------------------------------------------------------------------------552 553_COMPARE_SYSTEM = f"""You are an expert audio mastering engineer reviewing a completed master. You are evaluating the output of a specific mastering tool with the following architecture:554 555{_SIGNAL_FLOW}556 557IMPORTANT — When assessing the master:558- The 15 Hz HPF is always active. Any sub-bass roll-off below ~30 Hz is intentional subsonic cleanup, NOT a problem. Do not flag it.559- There is no LPF. The full spectrum above the HPF is passed through, so the 10 kHz high shelf has full authority over the air band.560- LUFS normalization is the final stage — it applies uniform linear gain. Loudness differences between original and mastered are intentional (target LUFS). Focus on spectral shape and dynamics, not absolute level.561- Compression at slider=0 means TRUE BYPASS (compressor was not in the chain at all). Do not comment on compression characteristics if it was bypassed.562- Stereo width only affects frequencies above 200 Hz (Linkwitz-Riley crossover). Bass mono-compatibility is always preserved.563- When suggesting improvements, ONLY recommend changes to the 7 available controls (lows_db, mid_boost_db, highs_db, bass_boost_db, bass_freq_hz, compression 0-100, stereo_width 80-150%). Do not suggest changes the tool cannot make (e.g., adjusting per-band attack times, changing crossover frequencies, changing the HPF frequency). The multiband compression is automatic — the user only controls the single 0-100 slider.564- TRUE PEAK CEILING: If the mastered true peak is at -0.1 dBTP and the LUFS is below target, the peak ceiling pulled the level down. The fix is more compression (higher slider value) to reduce crest factor, NOT removing the ceiling. Mention this trade-off when relevant.565- The compression slider uses a LOGARITHMIC (quadratic) curve: 0-50 = subtle/transparent, 50-75 = moderate, 75-100 = aggressive. Factor this into your slider recommendations.566 567Format your response as markdown with these sections:568### Overall Assessment569(1-2 sentences — was the mastering effective for the material?)570 571### What Worked Well572(bullet points referencing specific measurement changes)573 574### Suggested Improvements575(bullet points with specific slider value recommendations using the 7 available controls. If the master is good, say so — not every master needs changes.)576 577### Technical Notes578(any concerns about dynamics, phase coherence, or frequency balance that the available controls could address)579 580Be concise and specific. Reference actual measurement deltas between original and mastered."""581 582 583def compare_master(original, mastered, sample_rate, settings_dict, history=None):584 """Compare original vs mastered audio and return AI quality report.585 586 Args:587 original: numpy array of original audio.588 mastered: numpy array of mastered audio.589 sample_rate: int.590 settings_dict: dict with the mastering settings that were applied.591 history: list of dicts from previous analyses (optional).592 593 Returns:594 str: markdown-formatted comparison report, or fallback message.595 """596 orig_features = extract_features(original, sample_rate)597 mast_features = extract_features(mastered, sample_rate)598 599 # Build the multiband compression details from slider value600 from dsp import map_multiband_compression601 comp_val = settings_dict.get("compression", 50)602 band_params = map_multiband_compression(comp_val)603 604 def _fmt_band(params):605 return (f"threshold {params[0]:.1f} dB, ratio {params[1]:.2f}:1, "606 f"attack {params[2]:.0f} ms, release {params[3]:.0f} ms")607 608 history_text = _format_history(history or [])609 610 user_prompt = f"""Compare the original and mastered audio:611 612**ORIGINAL Audio:**613- Loudness: {orig_features['lufs']} LUFS | True Peak: {orig_features['true_peak_dbtp']} dBTP614- RMS: {orig_features['rms_db']} dB | Crest Factor: {orig_features['crest_factor_db']} dB615- Spectral Centroid: {orig_features['spectral_centroid_hz']} Hz | Rolloff: {orig_features['spectral_rolloff_hz']} Hz616- Stereo Correlation: {orig_features['stereo_correlation'] if orig_features['stereo_correlation'] is not None else 'N/A (mono)'}617- Band Energy: {json.dumps(orig_features['band_energy'])}618 619**MASTERED Audio:**620- Loudness: {mast_features['lufs']} LUFS | True Peak: {mast_features['true_peak_dbtp']} dBTP621- RMS: {mast_features['rms_db']} dB | Crest Factor: {mast_features['crest_factor_db']} dB622- Spectral Centroid: {mast_features['spectral_centroid_hz']} Hz | Rolloff: {mast_features['spectral_rolloff_hz']} Hz623- Stereo Correlation: {mast_features['stereo_correlation'] if mast_features['stereo_correlation'] is not None else 'N/A (mono)'}624- Band Energy: {json.dumps(mast_features['band_energy'])}625 626**Settings Applied:**627- Lows (200 Hz shelf): {settings_dict.get('lows_db', 0)} dB628- Mids (1.2 kHz peak): {settings_dict.get('mid_boost_db', 0)} dB629- Highs (10 kHz shelf): {settings_dict.get('highs_db', 0)} dB630- Bass Boost: {settings_dict.get('bass_boost_db', 0)} dB @ {settings_dict.get('bass_freq_hz', 60)} Hz631- Compression: slider {comp_val}/100 (multiband, 3 bands)632 - Low (< 200 Hz): {_fmt_band(band_params['low'])}633 - Mid (200 Hz-4 kHz): {_fmt_band(band_params['mid'])}634 - High (> 4 kHz): {_fmt_band(band_params['high'])}635- Stereo Width: {settings_dict.get('stereo_width', 100)}%636- Target LUFS: {settings_dict.get('target_lufs', -14)}{history_text}"""637 638 response = _call_gemini(_COMPARE_SYSTEM, user_prompt)639 if response is None:640 return "*Set GOOGLE_API_KEY to enable AI comparison report.*"641 return response642 643 644# ---------------------------------------------------------------------------645# Phase 3: Structured comparison (for Auto Master loop)646# ---------------------------------------------------------------------------647 648_COMPARE_STRUCTURED_SYSTEM = f"""You are an expert audio mastering engineer reviewing a completed master. You are evaluating the output of a specific mastering tool with the following architecture:649 650{_SIGNAL_FLOW}651 652IMPORTANT — When assessing the master:653- The 15 Hz HPF is always active. Any sub-bass roll-off below ~30 Hz is intentional subsonic cleanup, NOT a problem. Do not flag it.654- There is no LPF. The full spectrum above the HPF is passed through, so the 10 kHz high shelf has full authority over the air band.655- LUFS normalization is the final stage — it applies uniform linear gain. Loudness differences between original and mastered are intentional (target LUFS). Focus on spectral shape and dynamics, not absolute level.656- Compression at slider=0 means TRUE BYPASS (compressor was not in the chain at all). Do not comment on compression characteristics if it was bypassed.657- Stereo width only affects frequencies above 200 Hz (Linkwitz-Riley crossover). Bass mono-compatibility is always preserved.658- When suggesting improvements, ONLY recommend changes to the 7 available controls. Do not suggest changes the tool cannot make (e.g., adjusting per-band attack times, changing crossover frequencies, changing the HPF frequency). The multiband compression is automatic — the user only controls the single 0-100 slider.659- TRUE PEAK CEILING: If the mastered true peak is at -0.1 dBTP and the LUFS is below target, the peak ceiling pulled the level down. The fix is more compression (higher slider value) to reduce crest factor, NOT removing the ceiling. Adjust your revised compression value accordingly.660- The compression slider uses a LOGARITHMIC (quadratic) curve: 0-50 = subtle/transparent, 50-75 = moderate, 75-100 = aggressive. Factor this into your slider recommendations.661 662Return ONLY a valid JSON object (no markdown fences, no extra text) with these exact keys:663{{664 "lows_db": <number, -3.0 to +3.0>,665 "mid_boost_db": <number, -3.0 to +3.0>,666 "highs_db": <number, -3.0 to +3.0>,667 "bass_boost_db": <number, 0 to +3.0>,668 "bass_freq_hz": <integer, 40 to 100>,669 "compression": <integer, 0 to 100>,670 "stereo_width": <integer, 80 to 150>,671 "report": "<your full markdown comparison report here — Overall Assessment, What Worked Well, Suggested Improvements, Technical Notes>"672}}673 674The numeric values should be your REVISED recommended settings for a re-master based on what you hear in the measurements.675The "report" field should contain the full markdown analysis.676Be concise and specific. Reference actual measurement deltas.677 678Do NOT return the template above literally — fill in your actual analysis and values."""679 680 681def _format_history(history):682 """Format analysis history for inclusion in prompts."""683 if not history:684 return ""685 lines = ["\n\n**PREVIOUS ANALYSIS HISTORY** (oldest first — use this to avoid recommending settings that already failed or oscillating between values):"]686 for i, entry in enumerate(history, 1):687 lines.append(f"\n--- Pass {i} ---")688 lines.append(f"Settings tried: {json.dumps({k: v for k, v in entry.get('settings', {}).items() if k != 'target_lufs'})}")689 lines.append(f"Result: LUFS={entry.get('lufs', '?')}, True Peak={entry.get('true_peak', '?')} dBTP, Crest Factor={entry.get('crest_factor', '?')} dB")690 if entry.get("summary"):691 lines.append(f"AI assessment: {entry['summary']}")692 lines.append("\nIMPORTANT: Do NOT oscillate. If a previous pass moved a setting in one direction and it helped, continue refining in that direction. If it didn't help, try a DIFFERENT approach rather than reverting to a value that was already tried.")693 return "\n".join(lines)694 695 696def compare_master_structured(original, mastered, sample_rate, settings_dict,697 history=None):698 """Compare original vs mastered and return structured values + report.699 700 Same analysis as compare_master() but returns a dict with revised slider701 values and a markdown report, for use in the Auto Master loop.702 703 Args:704 history: list of dicts from previous analyses (optional). Each entry705 has keys: settings, lufs, true_peak, crest_factor, summary.706 707 Returns:708 dict with keys: lows_db, mid_boost_db, highs_db, bass_boost_db,709 bass_freq_hz, compression, stereo_width, report.710 On parse error: {"report": raw_text, "parse_error": True}.711 On API failure: None.712 """713 orig_features = extract_features(original, sample_rate)714 mast_features = extract_features(mastered, sample_rate)715 716 from dsp import map_multiband_compression717 comp_val = settings_dict.get("compression", 50)718 band_params = map_multiband_compression(comp_val)719 720 def _fmt_band(params):721 return (f"threshold {params[0]:.1f} dB, ratio {params[1]:.2f}:1, "722 f"attack {params[2]:.0f} ms, release {params[3]:.0f} ms")723 724 history_text = _format_history(history or [])725 726 user_prompt = f"""Compare the original and mastered audio and return your revised settings as JSON:727 728**ORIGINAL Audio:**729- Loudness: {orig_features['lufs']} LUFS | True Peak: {orig_features['true_peak_dbtp']} dBTP730- RMS: {orig_features['rms_db']} dB | Crest Factor: {orig_features['crest_factor_db']} dB731- Spectral Centroid: {orig_features['spectral_centroid_hz']} Hz | Rolloff: {orig_features['spectral_rolloff_hz']} Hz732- Stereo Correlation: {orig_features['stereo_correlation'] if orig_features['stereo_correlation'] is not None else 'N/A (mono)'}733- Band Energy: {json.dumps(orig_features['band_energy'])}734 735**MASTERED Audio:**736- Loudness: {mast_features['lufs']} LUFS | True Peak: {mast_features['true_peak_dbtp']} dBTP737- RMS: {mast_features['rms_db']} dB | Crest Factor: {mast_features['crest_factor_db']} dB738- Spectral Centroid: {mast_features['spectral_centroid_hz']} Hz | Rolloff: {mast_features['spectral_rolloff_hz']} Hz739- Stereo Correlation: {mast_features['stereo_correlation'] if mast_features['stereo_correlation'] is not None else 'N/A (mono)'}740- Band Energy: {json.dumps(mast_features['band_energy'])}741 742**Settings Applied:**743- Lows (200 Hz shelf): {settings_dict.get('lows_db', 0)} dB744- Mids (1.2 kHz peak): {settings_dict.get('mid_boost_db', 0)} dB745- Highs (10 kHz shelf): {settings_dict.get('highs_db', 0)} dB746- Bass Boost: {settings_dict.get('bass_boost_db', 0)} dB @ {settings_dict.get('bass_freq_hz', 60)} Hz747- Compression: slider {comp_val}/100 (multiband, 3 bands)748 - Low (< 200 Hz): {_fmt_band(band_params['low'])}749 - Mid (200 Hz-4 kHz): {_fmt_band(band_params['mid'])}750 - High (> 4 kHz): {_fmt_band(band_params['high'])}751- Stereo Width: {settings_dict.get('stereo_width', 100)}%752- Target LUFS: {settings_dict.get('target_lufs', -14)}{history_text}753 754Return the JSON object with your revised settings and comparison report."""755 756 response = _call_gemini(_COMPARE_STRUCTURED_SYSTEM, user_prompt)757 if response is None:758 return None759 760 try:761 result = _strip_json(response)762 _clamp_settings(result)763 if "report" not in result:764 result["report"] = "*No report provided.*"765 return result766 except (json.JSONDecodeError, KeyError, TypeError):767 return {"report": response, "parse_error": True}768 769 770# ---------------------------------------------------------------------------771# Super AI mode — full parametric control772# ---------------------------------------------------------------------------773 774_SUPER_SIGNAL_FLOW = """775AUDIO ANALYSIS DATA YOU RECEIVE:776You will receive detailed measurements for each audio file, including:777- Standard: LUFS, true peak, RMS, crest factor, spectral centroid, spectral rolloff, stereo correlation778- 24-Band Spectral Profile: Fine-grained energy (dB) across 21 frequency bands from 20 Hz to 20 kHz.779 USE THIS to make precise EQ decisions — you can see exactly where energy buildups, dips, and imbalances are.780- Spectral Resonances: Top 8 most prominent spectral peaks with frequency, level, and prominence (dB above neighbors).781 USE THIS to identify harsh or ringing frequencies that need surgical EQ cuts.782- Spectral Flatness: 0.0 = pure tonal, 1.0 = white noise. Tells you how tonal vs noisy the material is.783- Spectral Tilt: dB/decade slope. Negative = bass-heavy, positive = bright. Guides overall tonal balance decisions.784- Per-Compression-Band Dynamics: RMS, peak, and crest factor for each of the 3 compression bands (low/mid/high).785 USE THIS to set compression thresholds and ratios per band — you can see which bands need taming.786- Dynamic Variation: Min/max/range/std of RMS across 4-second chunks of the track.787 Tells you how much the track varies (quiet verse vs loud chorus). High range = preserve dynamics. Low range = already compressed.788- Per-Band Stereo Correlation: Correlation for low, low-mid, high-mid, and high frequency bands.789 USE THIS to make stereo width decisions — low correlation = wide, high = narrow/mono.790 791SIGNAL FLOW (fixed processing order — you control ALL parameters):7921. PRE-GAIN DROP — Input is normalized to -18 LUFS (automatic, not adjustable).7932. HIGH-PASS FILTER — Adjustable cutoff (10-80 Hz). Default 15 Hz, 12 dB/oct Butterworth.7943. 6-BAND FULLY PARAMETRIC EQ — Each band is independently configurable:795 - band1 through band6: type (peak/low_shelf/high_shelf), frequency (20-20000 Hz), gain (-6 to +6 dB), Q (0.1-10.0)796 You can use any combination of shelf and peak filters at any frequency. The normal UI locks these to fixed frequencies and ±3 dB — you are NOT limited to that. You have full parametric EQ control.797 Set gain_db to 0 on any band you don't need — unused bands are bypassed automatically.7984. MULTIBAND COMPRESSION — 3-band dynamics with per-band control:799 - crossover_low: adjustable crossover frequency for low/mid split (default 200 Hz)800 - crossover_high: adjustable crossover frequency for mid/high split (default 4000 Hz)801 - Each band (low, mid, high) has independently adjustable: threshold (-20 to 0 dB), ratio (1.0-20.0), attack_ms (0.1-200 ms), release_ms (10-500 ms)802 - Ratio 1.0 = bypass for that band803 - No makeup gain — LUFS normalization restores level.8045. STEREO WIDTH — M/S matrix, frequency-selective (crossover at 200 Hz, bass stays mono). Range 80-150%.8056. LUFS NORMALIZATION — Automatic to target LUFS (fixed by user, DO NOT change).8067. SOFT CLIPPER — Piecewise tanh saturation, knee 2 dB below -0.1 dBTP ceiling. Always active. Linear below knee, tanh above. This is a safety net — NOT a creative tool.8078. TRUE PEAK CEILING — Safety net at -0.1 dBTP. Scales signal down if residual peaks exceed ceiling.808 809CONSTRAINTS (DO NOT VIOLATE):810- Target LUFS is fixed by the user. Do not change it.811- The soft clipper and true peak ceiling must remain as-is (automatic safety nets).812- You cannot add new processing stages — only adjust the parameters described above.813 814TRUE PEAK GUIDANCE (IMPORTANT):815- True peak between -1.0 and -0.1 dBTP is the IDEAL goal, but it is NOT always achievable.816- Source material that is already heavily limited or compressed (e.g., AI-generated tracks from Suno, Udio, etc.) has a very low crest factor (peak-to-loudness ratio). When such material is normalized DOWN to a streaming LUFS target (e.g., -14 LUFS), the true peak will naturally drop well below -1.0 dBTP. This is correct and expected behavior.817- DO NOT over-compress or crush dynamics to try to force the true peak higher. Dynamics preservation is MORE important than hitting a specific true peak number.818- If the source material has a low crest factor, acknowledge this in your analysis and accept the true peak wherever it naturally lands after LUFS normalization.819- Only use compression for tonal shaping and dynamic control — NEVER to artificially raise the true peak.820"""821 822_SUPER_RECOMMEND_SYSTEM = f"""You are a world-class audio mastering engineer with decades of experience. You have FULL control over every parameter in the mastering chain. Analyze the audio measurements and recommend optimal settings.823 824{_SUPER_SIGNAL_FLOW}825 826MASTERING PHILOSOPHY:827- LESS IS MORE. A great master sounds like a better version of the original, not a different song.828- Most EQ moves should be ±1 to ±2 dB. Moves beyond ±3 dB are RARE and require strong justification.829- If the source audio already sounds good in a frequency range, LEAVE IT ALONE. Do not EQ for the sake of EQ.830- Use surgical EQ moves — small cuts are often more effective than boosts.831- Compression thresholds should be set so compression only engages on peaks, NOT constantly. A threshold of -35 dB or lower means the compressor is always compressing — that destroys dynamics. Typical mastering thresholds are -15 to -8 dB.832- Match compression to the genre and dynamic character of the material.833- Preserve the artist's intent — enhance, don't transform.834- True peak between -1.0 and -0.1 dBTP is ideal, but do NOT sacrifice dynamics to achieve it. If the source is already heavily compressed, the true peak may land below -1.0 dBTP at the target LUFS — that is acceptable.835 836TONAL DIRECTION (apply to all masters):837- Aim for a slightly WARM overall tone — a SUBTLE richness in the low-mids (200-500 Hz) and smooth, non-harsh highs. This means maybe +0.5 to +1.5 dB shelf, NOT +3 dB or more.838- High shelf boosts above +1.5 dB will make the master sound harsh and brittle — avoid this.839 840HEAVY, UNCONSTRAINED BASS — Protect low-end punch and transient impact (40-100 Hz) at all costs:841- Rule 1: Prioritize additive EQ (Band 1/Band 2) for bass weight. DO NOT cut any frequencies below 150 Hz with EQ. The HPF already handles rumble removal. Any EQ band targeting frequencies below 150 Hz should have POSITIVE gain (boost) or be bypassed (0 dB). Cutting sub-bass removes the punch and weight from the track.842- Rule 2: DO NOT over-compress the < 200 Hz band. If the source crest factor is already low, default the Low-Band Compressor to BYPASS (Ratio 1:1) or use a very slow attack (>60 ms) so the kick drum transient escapes untouched.843- Rule 3: The sub-bass should feel physical, anchored, and wide open.844- Rule 4: HPF cutoff must stay at or below 25 Hz for bass-heavy material. Only raise it above 30 Hz if measurements show significant rumble below 20 Hz.845 846Return ONLY a valid JSON object with this exact structure (no markdown fences):847{{848 "hpf_freq": <float, 10-80>,849 "eq": {{850 "band1": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},851 "band2": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},852 "band3": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},853 "band4": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},854 "band5": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},855 "band6": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}}856 }},857 "compression": {{858 "low": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}},859 "mid": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}},860 "high": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}}861 }},862 "crossover_low": <float Hz>,863 "crossover_high": <float Hz>,864 "stereo_width": <int, 80-150>,865 "reasoning": "### AI Analysis\\n- **EQ:** reason for EQ choices\\n- **Dynamics:** reason for compression settings\\n- **Stereo:** reason for width choice\\n(write your ACTUAL analysis — do NOT return this template literally)"866}}867 868Be musical and intentional. Every parameter should have a reason."""869 870_SUPER_COMPARE_SYSTEM = f"""You are a world-class audio mastering engineer reviewing a completed master. You have FULL control over every parameter and can make surgical adjustments.871 872{_SUPER_SIGNAL_FLOW}873 874REVIEW GUIDELINES:875- Compare original vs mastered measurements carefully.876- Make VERY SMALL adjustments — typically ±0.5 dB EQ tweaks or 1-2 dB threshold changes. If you changed a parameter by more than ±1 dB on the previous pass, do NOT change it again unless the measurements clearly show a problem.877- If something sounds good, LEAVE IT ALONE. The best revision is often the smallest one.878- Compression thresholds should be -15 to -8 dB for mastering. If you see a threshold below -20 dB, raise it — that compressor is over-compressing.879- Focus on what the measurements tell you: spectral balance, dynamics, stereo image.880- True peak between -1.0 and -0.1 dBTP is ideal, but do NOT over-compress to force it. If the source has a low crest factor, accept the true peak wherever it lands naturally.881- LUFS target is fixed — do not try to change it.882- Reference the previous analysis history to avoid oscillating between settings.883- Each revision should be a refinement, not a reset. Aim for 1-2 parameter changes per pass, not 5+.884 885TONAL DIRECTION (maintain across all revisions):886- The master should have a slightly WARM overall tone — SUBTLE richness in the low-mids (200-500 Hz) and smooth, non-harsh highs. Avoid clinical or brittle sound.887- High shelf boosts above +1.5 dB will make the master harsh — pull them back if present.888 889HEAVY, UNCONSTRAINED BASS — Protect low-end punch and transient impact (40-100 Hz) at all costs:890- Rule 1: Prioritize additive EQ (Band 1/Band 2) for bass weight. DO NOT cut any frequencies below 150 Hz with EQ. If a previous pass cut sub-bass, UNDO that cut (set gain to 0 or positive). Cutting sub-bass removes punch and weight.891- Rule 2: DO NOT over-compress the < 200 Hz band. If the source crest factor is already low, default the Low-Band Compressor to BYPASS (Ratio 1:1) or use a very slow attack (>60 ms) so the kick drum transient escapes untouched.892- Rule 3: The sub-bass should feel physical, anchored, and wide open.893- Rule 4: HPF cutoff must stay at or below 25 Hz for bass-heavy material. Only raise it above 30 Hz if measurements show significant rumble below 20 Hz.894 895CREST FACTOR CHECK (passes 2-4):896- If the crest factor in the < 200 Hz range decreases between passes, you have over-compressed the kick drum. BACK OFF the Low-Band compressor ratio or lengthen the attack time. Do not lose the punch.897- Sidechain Emulation: Treat the Low-Band compressor as if it has a 100 Hz HPF on its sidechain detector. Do not let sustained sub-bass notes clamp down on the rhythmic transients.898 899Return ONLY a valid JSON object with this exact structure (no markdown fences):900{{901 "hpf_freq": <float, 10-80>,902 "eq": {{903 "band1": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},904 "band2": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},905 "band3": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},906 "band4": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},907 "band5": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},908 "band6": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}}909 }},910 "compression": {{911 "low": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}},912 "mid": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}},913 "high": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}}914 }},915 "crossover_low": <float Hz>,916 "crossover_high": <float Hz>,917 "stereo_width": <int, 80-150>,918 "report": "<your full markdown comparison report — Overall Assessment, What Worked Well, Suggested Improvements, Technical Notes>"919}}920 921The numeric values should be your REVISED settings for a re-master. Make small, targeted adjustments.922The "report" field must contain your actual markdown analysis."""923 924_SUPER_FINAL_REPORT_SYSTEM = f"""You are a world-class audio mastering engineer writing a final quality report. You are evaluating whether a master meets professional distribution standards.925 926{_SUPER_SIGNAL_FLOW}927 928TONAL DIRECTION (evaluate against these goals):929- The desired outcome is a slightly WARM overall tone with smooth, non-harsh highs.930- HEAVY, UNCONSTRAINED BASS — the low end (40-100 Hz) should feel physical, punchy, and anchored. Evaluate whether the kick drum transients survived the compression stage. If sub-bass was cut by EQ or crushed by compression, flag it as a failure.931- HPF should be at or below 25 Hz for bass-heavy material.932- Evaluate whether the final master achieves this tonal character.933 934Write a comprehensive final report in markdown format covering:935 936### Overall Assessment937(Was the mastering effective? Does it meet professional standards? Does it achieve the desired warm tone with enhanced bass?)938 939### Spectral Balance940(Evaluate frequency balance — low end warmth, midrange richness, high end smoothness)941 942### Dynamics & Loudness943(LUFS, true peak compliance, crest factor, dynamic range preservation)944 945### Stereo Image946(Width, mono compatibility, balance)947 948### Processing Summary949(What the mastering chain did — EQ moves, compression character, etc.)950 951### Verdict952(Pass/fail for streaming distribution. Any remaining concerns?)953 954Be specific. Reference actual measurements. This is the FINAL report — no suggestions for changes, just an honest evaluation of the finished master."""955 956 957def _clamp_super_params(d):958 """Clamp Super AI parameters to safe ranges."""959 d["hpf_freq"] = max(10.0, min(80.0, float(d.get("hpf_freq", 15.0))))960 d["stereo_width"] = max(80, min(150, int(d.get("stereo_width", 100))))961 d["crossover_low"] = max(80.0, min(500.0, float(d.get("crossover_low", 200.0))))962 d["crossover_high"] = max(1000.0, min(10000.0, float(d.get("crossover_high", 4000.0))))963 964 eq = d.get("eq", {})965 for bk in ("band1", "band2", "band3", "band4", "band5", "band6"):966 band = eq.get(bk, {})967 band["freq"] = max(20.0, min(20000.0, float(band.get("freq", 1000))))968 band["gain_db"] = max(-6.0, min(6.0, float(band.get("gain_db", 0))))969 band["q"] = max(0.1, min(10.0, float(band.get("q", 1.0))))970 if band.get("type") not in ("peak", "low_shelf", "high_shelf"):971 band["type"] = "peak"972 eq[bk] = band973 d["eq"] = eq974 975 comp = d.get("compression", {})976 for bk in ("low", "mid", "high"):977 bp = comp.get(bk, {})978 bp["threshold"] = max(-20.0, min(0.0, float(bp.get("threshold", -14.0))))979 bp["ratio"] = max(1.0, min(20.0, float(bp.get("ratio", 1.0))))980 bp["attack_ms"] = max(0.1, min(200.0, float(bp.get("attack_ms", 30.0))))981 bp["release_ms"] = max(10.0, min(500.0, float(bp.get("release_ms", 150.0))))982 comp[bk] = bp983 d["compression"] = comp984 985 return d986 987 988def _format_super_settings(params):989 """Format Super AI parameters into readable text for prompts."""990 eq = params.get("eq", {})991 comp = params.get("compression", {})992 lines = [993 f"- HPF: {params.get('hpf_freq', 15)} Hz",994 ]995 for bk in ("band1", "band2", "band3", "band4", "band5", "band6"):996 b = eq.get(bk, {})997 if abs(b.get("gain_db", 0)) < 0.01:998 lines.append(f"- EQ {bk}: bypassed (0 dB)")999 else:1000 lines.append(f"- EQ {bk}: {b.get('type','peak')} @ {b.get('freq',1000)} Hz, "1001 f"{b.get('gain_db',0):+.1f} dB, Q={b.get('q',1.0):.2f}")1002 lines.append(f"- Crossovers: {params.get('crossover_low', 200)} Hz / {params.get('crossover_high', 4000)} Hz")1003 for bk in ("low", "mid", "high"):1004 bp = comp.get(bk, {})1005 lines.append(f"- Comp {bk}: threshold {bp.get('threshold',-14):.1f} dB, "1006 f"ratio {bp.get('ratio',1.0):.2f}:1, "1007 f"attack {bp.get('attack_ms',30):.1f} ms, "1008 f"release {bp.get('release_ms',150):.1f} ms")1009 lines.append(f"- Stereo Width: {params.get('stereo_width', 100)}%")1010 return "\n".join(lines)1011 1012 1013def _format_detailed_features(features, label="Audio"):1014 """Format detailed features from extract_features_detailed() for prompts."""1015 lines = [1016 f"**{label} — Core Measurements:**",1017 f"- Loudness: {features['lufs']} LUFS | True Peak: {features['true_peak_dbtp']} dBTP",1018 f"- RMS: {features['rms_db']} dB | Crest Factor: {features['crest_factor_db']} dB",1019 f"- Spectral Centroid: {features['spectral_centroid_hz']} Hz | Rolloff: {features['spectral_rolloff_hz']} Hz",1020 f"- Spectral Flatness: {features.get('spectral_flatness', 'N/A')} (0=tonal, 1=noise)",1021 f"- Spectral Tilt: {features.get('spectral_tilt_db_per_decade', 'N/A')} dB/decade (negative=bass-heavy, positive=bright)",1022 f"- Stereo Correlation: {features['stereo_correlation'] if features['stereo_correlation'] is not None else 'N/A (mono)'}",1023 ]1024 1025 # 24-band spectral profile1026 profile = features.get("spectral_profile_24band")1027 if profile:1028 lines.append(f"\n**{label} — 24-Band Spectral Profile (dB):**")1029 for band, val in profile.items():1030 bar = "█" * max(0, int((val + 60) / 2)) if val > -60 else ""1031 lines.append(f" {band:>8s}: {val:>7.1f} dB {bar}")1032 1033 # Resonances1034 resonances = features.get("resonances", [])1035 if resonances:1036 lines.append(f"\n**{label} — Spectral Resonances (peaks above neighbors):**")1037 for r in resonances:1038 lines.append(f" {r['freq_hz']:>8.1f} Hz: {r['level_db']:+.1f} dB "1039 f"(prominence: {r['prominence_db']:.1f} dB)")1040 1041 # Per-compression-band dynamics1042 cbd = features.get("comp_band_dynamics")1043 if cbd:1044 lines.append(f"\n**{label} — Per-Compression-Band Dynamics:**")1045 for band_name in ("low", "mid", "high"):1046 bd = cbd.get(band_name, {})1047 lines.append(f" {band_name:>4s}: RMS {bd.get('rms_db', '?')} dB, "1048 f"Peak {bd.get('peak_db', '?')} dB, "1049 f"Crest {bd.get('crest_db', '?')} dB")1050 1051 # Dynamic variation1052 dv = features.get("dynamic_variation")1053 if dv:1054 lines.append(f"\n**{label} — Dynamic Variation (4-sec chunks, {dv.get('n_chunks', '?')} chunks):**")1055 lines.append(f" RMS range: {dv.get('min_rms_db', '?')} to {dv.get('max_rms_db', '?')} dB "1056 f"(span: {dv.get('range_db', '?')} dB, σ: {dv.get('std_db', '?')} dB)")1057 1058 # Per-band stereo correlation1059 sbc = features.get("stereo_band_correlation")1060 if sbc:1061 lines.append(f"\n**{label} — Per-Band Stereo Correlation:**")1062 for band_name in ("low", "low_mid", "high_mid", "high"):1063 val = sbc.get(band_name)1064 lines.append(f" {band_name:>8s}: {val if val is not None else 'N/A'}")1065 1066 # Original 6-band energy for backward compat1067 lines.append(f"\n**{label} — 6-Band Energy Summary:**")1068 for k, v in features.get("band_energy", {}).items():1069 lines.append(f" {k}: {v} dB")1070 1071 return "\n".join(lines)1072 1073 1074def _format_super_history(history):1075 """Format Super AI analysis history for prompts."""1076 if not history:1077 return ""1078 lines = ["\n\n**PREVIOUS ANALYSIS HISTORY** (use this to avoid oscillating — refine, don't reset):"]1079 for i, entry in enumerate(history, 1):1080 lines.append(f"\n--- Pass {i} ---")1081 lines.append(f"Settings:\n{_format_super_settings(entry.get('params', {}))}")1082 lines.append(f"Result: LUFS={entry.get('lufs', '?')}, True Peak={entry.get('true_peak', '?')} dBTP, "1083 f"Crest Factor={entry.get('crest_factor', '?')} dB")1084 if entry.get("summary"):1085 lines.append(f"AI assessment: {entry['summary']}")1086 lines.append("\nIMPORTANT: Do NOT oscillate. Refine incrementally. If a setting helped, keep it and fine-tune. "1087 "If it didn't help, try a DIFFERENT approach rather than reverting.")1088 return "\n".join(lines)1089 1090 1091def super_ai_recommend(audio_path):1092 """Analyze raw audio and return full-parametric AI mastering settings.1093 1094 Returns:1095 dict with full parameter set + reasoning, or None.1096 """1097 from dsp import load_audio1098 audio, sr = load_audio(audio_path)1099 features = extract_features_detailed(audio, sr)1100 1101 user_prompt = f"""Analyze this audio and recommend full mastering parameters:1102 1103{_format_detailed_features(features, "INPUT")}1104 1105Return the JSON object with your recommended full parameter set.1106Use the 24-band spectral profile to make precise EQ decisions.1107Use the resonances to identify frequencies that need surgical cuts.1108Use the per-band dynamics to set compression thresholds and ratios.1109Use the dynamic variation to decide how aggressively to compress."""1110 1111 response = _call_gemini(_SUPER_RECOMMEND_SYSTEM, user_prompt)1112 if response is None:1113 return None1114 1115 try:1116 result = _strip_json(response)1117 _clamp_super_params(result)1118 if "reasoning" not in result:1119 result["reasoning"] = "*No explanation provided.*"1120 return result1121 except (json.JSONDecodeError, KeyError, TypeError):1122 return {"reasoning": response, "parse_error": True}1123 1124 1125def super_ai_compare(original, mastered, sample_rate, params, target_lufs,1126 history=None):1127 """Compare original vs mastered with full-parametric revision.1128 1129 Returns:1130 dict with revised full params + report, or None.1131 """1132 orig_features = extract_features_detailed(original, sample_rate)1133 mast_features = extract_features_detailed(mastered, sample_rate)1134 history_text = _format_super_history(history or [])1135 1136 user_prompt = f"""Compare original vs mastered audio and return revised full parameters:1137 1138{_format_detailed_features(orig_features, "ORIGINAL")}1139 1140{_format_detailed_features(mast_features, "MASTERED")}1141 1142**Settings Applied:**1143{_format_super_settings(params)}1144- Target LUFS: {target_lufs}{history_text}1145 1146Return the JSON with your REVISED full parameter set and comparison report.1147Make SMALL, incremental adjustments — refine what's working, fix what isn't.1148Compare the 24-band profiles to see exactly where the EQ moved things.1149Compare per-band dynamics to evaluate compression effectiveness.1150Check if resonances were tamed or if new ones were introduced."""1151 1152 response = _call_gemini(_SUPER_COMPARE_SYSTEM, user_prompt)1153 if response is None:1154 return None1155 1156 try:1157 result = _strip_json(response)1158 _clamp_super_params(result)1159 if "report" not in result:1160 result["report"] = "*No report provided.*"1161 return result1162 except (json.JSONDecodeError, KeyError, TypeError):1163 return {"report": response, "parse_error": True}1164 1165 1166def super_ai_final_report(original, mastered, sample_rate, params, target_lufs,1167 history=None):1168 """Generate a final quality assessment report (no new settings).1169 1170 Returns:1171 str: markdown report.1172 """1173 orig_features = extract_features_detailed(original, sample_rate)1174 mast_features = extract_features_detailed(mastered, sample_rate)1175 history_text = _format_super_history(history or [])1176 1177 user_prompt = f"""Write a final mastering quality report for this completed master:1178 1179{_format_detailed_features(orig_features, "ORIGINAL")}1180 1181{_format_detailed_features(mast_features, "MASTERED")}1182 1183**Final Settings Applied:**1184{_format_super_settings(params)}1185- Target LUFS: {target_lufs}{history_text}1186 1187Write the final quality report. No suggestions — just an honest assessment of whether this master meets professional standards.1188Reference specific frequency bands and measurements from the detailed analysis above."""1189 1190 response = _call_gemini(_SUPER_FINAL_REPORT_SYSTEM, user_prompt)1191 if response is None:1192 return "*AI final report unavailable.*"1193 return response1194# v4.21195 