Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import numpy as np
|
| 3 |
+
import scipy.signal as signal
|
| 4 |
+
import librosa
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
+
|
| 8 |
+
def run_forensic_analysis(vocal_file, master_file):
|
| 9 |
+
if not vocal_file or not master_file:
|
| 10 |
+
return None, None, None, "โ Error: Please upload both assets to run diagnostics."
|
| 11 |
+
|
| 12 |
+
# Load audio clips (Optimized downsampling & 60-second execution cap)
|
| 13 |
+
y_v, sr_v = librosa.load(vocal_file, sr=22050, duration=60.0)
|
| 14 |
+
y_m, sr_m = librosa.load(master_file, sr=22050, duration=60.0)
|
| 15 |
+
|
| 16 |
+
# =================================================================
|
| 17 |
+
# MODULE 1: STRUCTURAL RECURRENCE MAPPER (SSM)
|
| 18 |
+
# =================================================================
|
| 19 |
+
# Extract chroma features to map harmonic patterns
|
| 20 |
+
chroma = librosa.feature.chroma_stft(y=y_m, sr=sr_m, hop_length=1024)
|
| 21 |
+
chroma = librosa.util.normalize(chroma, axis=0)
|
| 22 |
+
ssm = np.dot(chroma.T, chroma)
|
| 23 |
+
|
| 24 |
+
fig_ssm = go.Figure(data=gr.Plot(go.Heatmap(
|
| 25 |
+
z=ssm, colorscale='Viridis', showscale=False,
|
| 26 |
+
x0=0, dx=1024/sr_m, y0=0, dy=1024/sr_m
|
| 27 |
+
)))
|
| 28 |
+
fig_ssm.update_layout(
|
| 29 |
+
title="Structural Recurrence Matrix (Pattern Matches Over Time)",
|
| 30 |
+
xaxis_title="Timeline (Seconds)",
|
| 31 |
+
yaxis_title="Timeline (Seconds)",
|
| 32 |
+
height=400, margin=dict(l=10, r=10, t=40, b=10)
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# =================================================================
|
| 36 |
+
# MODULE 2: MIX FORENSICS & FREQUENCY HEALTH
|
| 37 |
+
# =================================================================
|
| 38 |
+
centroid = librosa.feature.spectral_centroid(y=y_m, sr=sr_m, hop_length=512)[0]
|
| 39 |
+
flatness = librosa.feature.spectral_flatness(y=y_m, hop_length=512)[0]
|
| 40 |
+
t_m = librosa.times_like(centroid, sr=sr_m, hop_length=512)
|
| 41 |
+
|
| 42 |
+
rms_m = librosa.feature.rms(y=y_m)[0]
|
| 43 |
+
crest_factor = np.max(np.abs(y_m)) / (np.mean(rms_m) + 1e-6)
|
| 44 |
+
|
| 45 |
+
fig_mix = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.1)
|
| 46 |
+
fig_mix.add_trace(go.Scatter(x=t_m, y=centroid, mode='lines', name='Centroid (Brightness)', line=dict(color='#FF4B4B')), row=1, col=1)
|
| 47 |
+
fig_mix.add_trace(go.Scatter(x=t_m, y=flatness, mode='lines', name='Flatness (Noise/Harsh)', line=dict(color='#00E5FF')), row=2, col=1)
|
| 48 |
+
|
| 49 |
+
fig_mix.update_layout(
|
| 50 |
+
title="Spectral Diagnostic Vectors",
|
| 51 |
+
xaxis2_title="Timeline (Seconds)",
|
| 52 |
+
height=400, showlegend=False, margin=dict(l=10, r=10, t=40, b=10)
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# =================================================================
|
| 56 |
+
# MODULE 3: SYLLABLE VELOCITY & PHRASING DENSITY
|
| 57 |
+
# =================================================================
|
| 58 |
+
onset_v = librosa.onset.onset_strength(y=y_v, sr=sr_v, hop_length=512)
|
| 59 |
+
t_v = librosa.times_like(onset_v, sr=sr_v, hop_length=512)
|
| 60 |
+
|
| 61 |
+
peaks, _ = signal.find_peaks(onset_v, height=0.2, distance=10)
|
| 62 |
+
peak_times = t_v[peaks]
|
| 63 |
+
|
| 64 |
+
# Calculate local density (syllables per second window)
|
| 65 |
+
window_sz = 2.0
|
| 66 |
+
density_y = []
|
| 67 |
+
for t in t_v:
|
| 68 |
+
count = np.sum((peak_times >= t - window_sz/2) & (peak_times <= t + window_sz/2))
|
| 69 |
+
density_y.append(count / window_sz)
|
| 70 |
+
|
| 71 |
+
fig_vocal = go.Figure()
|
| 72 |
+
fig_vocal.add_trace(go.Scatter(x=t_v, y=density_y, mode='lines', fill='tozeroy', line=dict(color='#00FF66', width=2)))
|
| 73 |
+
fig_vocal.update_layout(
|
| 74 |
+
title="Lyrical Delivery Velocity (Syllables Per Second)",
|
| 75 |
+
xaxis_title="Timeline (Seconds)",
|
| 76 |
+
yaxis_title="Syllables/Sec",
|
| 77 |
+
height=350, margin=dict(l=10, r=10, t=40, b=10)
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# =================================================================
|
| 81 |
+
# DIAGNOSTIC SUMMARY GENERATOR
|
| 82 |
+
# =================================================================
|
| 83 |
+
avg_flatness = np.mean(flatness)
|
| 84 |
+
mix_critique = "Balanced clean spectrum." if avg_flatness < 0.01 else "High harshness/noise ratios detected."
|
| 85 |
+
if crest_factor > 5.0:
|
| 86 |
+
dynamics_status = "Dynamic, punchy transient preservation."
|
| 87 |
+
else:
|
| 88 |
+
dynamics_status = "Highly compressed / squashed brickwall mix."
|
| 89 |
+
|
| 90 |
+
report = f"""### ๐ Forensic Lab Insights (First 60s)
|
| 91 |
+
* **Dynamic Crest Factor:** {crest_factor:.2f} $\\rightarrow$ *{dynamics_status}*
|
| 92 |
+
* **Total Vocal Artifact Onsets:** {len(peaks)} individual syllables mapped.
|
| 93 |
+
* **Peak Delivery Speed:** {max(density_y):.1f} syllables per second.
|
| 94 |
+
* **Acoustic Blueprint Profile:** {mix_critique}
|
| 95 |
+
|
| 96 |
+
> **How to interpret charts:** > * **Recurrence Map:** Look for solid diagonal block shapes. These explicitly flag identical choruses or looping sample patterns.
|
| 97 |
+
> * **Spectral Vectors:** Sharp dips or valleys reveal filtering tricks, transitions, or frequency drops.
|
| 98 |
+
> * **Delivery Velocity:** Waves illustrate the emotional phrasing of a performance, showing where the artist rushes or drags words."""
|
| 99 |
+
|
| 100 |
+
return fig_ssm, fig_mix, fig_vocal, report
|
| 101 |
+
|
| 102 |
+
# =====================================================================
|
| 103 |
+
# INTERFACE LAYOUT
|
| 104 |
+
# =====================================================================
|
| 105 |
+
with gr.Blocks(title="Audio Forensic Analytics Lab") as demo:
|
| 106 |
+
gr.Markdown("# ๐ฌ Audio Forensic & Structural Intelligence Lab")
|
| 107 |
+
gr.Markdown("Drop clean stems extracted from Space 1 to extract advanced frequency blueprints, structural maps, and vocal data matrices.")
|
| 108 |
+
|
| 109 |
+
with gr.Row():
|
| 110 |
+
vocal_input = gr.Audio(type="filepath", label="Upload Isolated Vocal Stem (vocals.wav)")
|
| 111 |
+
master_input = gr.Audio(type="filepath", label="Upload Instrumental or Full Mix Master")
|
| 112 |
+
|
| 113 |
+
analyze_btn = gr.Button("Deploy Forensic Diagnosis Engines", variant="primary")
|
| 114 |
+
|
| 115 |
+
with gr.Tab("1. Structural Recurrence Map"):
|
| 116 |
+
plot_ssm = gr.Plot()
|
| 117 |
+
with gr.Tab("2. Mix Health & Spectral Vectors"):
|
| 118 |
+
plot_mix = gr.Plot()
|
| 119 |
+
with gr.Tab("3. Lyrical Velocity Matrix"):
|
| 120 |
+
plot_vocal = gr.Plot()
|
| 121 |
+
|
| 122 |
+
text_report = gr.Markdown()
|
| 123 |
+
|
| 124 |
+
analyze_btn.click(
|
| 125 |
+
fn=run_forensic_analysis,
|
| 126 |
+
inputs=[vocal_input, master_input],
|
| 127 |
+
outputs=[plot_ssm, plot_mix, plot_vocal, text_report]
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
if __name__ == "__main__":
|
| 131 |
+
demo.launch()
|