CoolFace
Apppublic

AnimalMonk/audio-mastering-suite

sourceHugging Facemitupdated 6mo agoView on Hugging Face
3likes
analysis.py1195 linesDownload Raw Back to root
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