AdityaK007 commited on
Commit
6f6aeaa
Β·
verified Β·
1 Parent(s): 6af463a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +226 -107
app.py CHANGED
@@ -2,93 +2,183 @@ import gradio as gr
2
  import librosa
3
  import numpy as np
4
  import pandas as pd
5
- from sklearn.cluster import KMeans
6
- from sklearn.metrics.pairwise import cosine_similarity
 
 
 
7
  import plotly.express as px
 
8
  import os
9
  import tempfile
10
- from scipy.signal import get_window as scipy_get_window # βœ… Fix here
11
 
12
  # ----------------------------
13
- # Core Functions
14
  # ----------------------------
15
 
16
- def load_audio(file_path):
17
- y, sr = librosa.load(file_path, sr=None)
18
- return y, sr
19
-
20
- def segment_audio(y, sr, frame_length_ms, hop_length_ms, window_type):
21
- frame_length = int(frame_length_ms * sr / 1000)
22
- hop_length = int(hop_length_ms * sr / 1000)
23
- if frame_length > len(y):
24
- raise ValueError("Frame length is longer than audio duration.")
25
- frames = librosa.util.frame(y, frame_length=frame_length, hop_length=hop_length)
26
-
27
- if window_type != "rectangular":
28
- # βœ… Use scipy.signal.get_window instead of librosa.get_window
29
- window = scipy_get_window(window_type, frame_length)
30
- frames = frames * window[:, None]
31
- return frames, sr
32
-
33
- def extract_features(frames, sr):
34
  features = []
35
  n_mfcc = 13
 
 
36
  for i in range(frames.shape[1]):
37
  frame = frames[:, i]
38
  feat = {}
39
 
40
- # RMS Energy
41
  rms = np.mean(librosa.feature.rms(y=frame)[0])
42
  feat["rms"] = float(rms)
43
 
44
- # Spectral Centroid
45
  sc = np.mean(librosa.feature.spectral_centroid(y=frame, sr=sr)[0])
46
  feat["spectral_centroid"] = float(sc)
47
 
48
- # Zero Crossing Rate
49
  zcr = np.mean(librosa.feature.zero_crossing_rate(frame)[0])
50
  feat["zcr"] = float(zcr)
51
 
52
- # MFCCs (1-13)
53
  mfccs = librosa.feature.mfcc(y=frame, sr=sr, n_mfcc=n_mfcc)
54
  for j in range(n_mfcc):
55
  feat[f"mfcc_{j+1}"] = float(np.mean(mfccs[j]))
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  features.append(feat)
58
- return pd.DataFrame(features)
59
 
60
- def compare_frames(near_df, far_df, metrics):
61
- min_len = min(len(near_df), len(far_df))
62
- near = near_df.iloc[:min_len].reset_index(drop=True)
63
- far = far_df.iloc[:min_len].reset_index(drop=True)
64
-
65
  results = {"frame_index": list(range(min_len))}
66
 
 
 
 
 
 
 
 
 
 
67
  if "Euclidean Distance" in metrics:
68
- euclidean = np.linalg.norm(near.values - far.values, axis=1)
69
- results["euclidean_dist"] = euclidean.tolist()
70
-
71
  if "Cosine Similarity" in metrics:
72
- cos_sim = []
73
  for i in range(min_len):
74
- a = near.iloc[i].values.reshape(1, -1)
75
- b = far.iloc[i].values.reshape(1, -1)
76
- sim = cosine_similarity(a, b)[0][0]
77
- cos_sim.append(float(sim))
78
- results["cosine_similarity"] = cos_sim
79
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  return pd.DataFrame(results)
81
 
82
- def cluster_frames(features_df, n_clusters):
83
- X = features_df.values
84
- kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
85
- labels = kmeans.fit_predict(X)
 
 
 
 
 
 
 
 
 
 
 
 
86
  features_df = features_df.copy()
87
  features_df["cluster"] = labels
88
  return features_df
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  # ----------------------------
91
- # Gradio App Logic
92
  # ----------------------------
93
 
94
  def analyze_audio_pair(
@@ -97,75 +187,77 @@ def analyze_audio_pair(
97
  frame_length_ms,
98
  hop_length_ms,
99
  window_type,
 
 
 
100
  n_clusters,
101
- comparison_metrics
102
  ):
103
  if not near_file or not far_file:
104
- raise gr.Error("Please upload both near-field and far-field audio files.")
105
-
106
- # Load audios
107
  y_near, sr_near = load_audio(near_file)
108
  y_far, sr_far = load_audio(far_file)
109
 
110
- # Resample if needed
111
  if sr_near != sr_far:
112
  y_far = librosa.resample(y_far, orig_sr=sr_far, target_sr=sr_near)
113
  sr = sr_near
114
  else:
115
  sr = sr_near
116
 
117
- # Segment
118
  frames_near, _ = segment_audio(y_near, sr, frame_length_ms, hop_length_ms, window_type)
119
  frames_far, _ = segment_audio(y_far, sr, frame_length_ms, hop_length_ms, window_type)
120
 
121
- # Extract features
122
- near_features = extract_features(frames_near, sr)
123
- far_features = extract_features(frames_far, sr)
124
 
125
- # Compare
126
- comparison_df = compare_frames(near_features, far_features, comparison_metrics)
127
 
128
- # Cluster near-field
129
- clustered_df = cluster_frames(near_features, n_clusters)
 
 
130
 
131
  # Plots
132
  plot_comparison = None
133
- if "Euclidean Distance" in comparison_metrics:
 
134
  plot_comparison = px.line(
135
  comparison_df,
136
  x="frame_index",
137
- y="euclidean_dist",
138
- title="Euclidean Distance Between Near & Far Frames"
139
  )
140
- elif "Cosine Similarity" in comparison_metrics:
141
- plot_comparison = px.line(
142
- comparison_df,
143
- x="frame_index",
144
- y="cosine_similarity",
145
- title="Cosine Similarity Between Near & Far Frames"
146
- )
147
- else:
148
- # Fallback
149
- col = comparison_df.columns[1] if len(comparison_df.columns) > 1 else "frame_index"
150
- plot_comparison = px.line(comparison_df, x="frame_index", y=col, title="Frame Comparison")
151
 
152
- # Scatter plot
153
  plot_scatter = None
154
- if "rms" in clustered_df.columns and "spectral_centroid" in clustered_df.columns:
155
- plot_scatter = px.scatter(
156
- clustered_df,
157
- x="rms",
158
- y="spectral_centroid",
159
- color="cluster",
160
- title="Clustered Frames (RMS vs Spectral Centroid)",
161
- labels={"rms": "RMS Energy", "spectral_centroid": "Spectral Centroid"}
162
- )
 
 
163
  else:
164
- plot_scatter = px.scatter(title="Not enough features for scatter plot")
 
 
 
165
 
166
- return plot_comparison, comparison_df, plot_scatter, clustered_df
 
 
 
 
 
 
167
 
168
- def export_results(plot_comparison, comparison_df, plot_scatter, clustered_df):
169
  temp_dir = tempfile.mkdtemp()
170
  comp_path = os.path.join(temp_dir, "frame_comparisons.csv")
171
  cluster_path = os.path.join(temp_dir, "clustered_frames.csv")
@@ -174,31 +266,56 @@ def export_results(plot_comparison, comparison_df, plot_scatter, clustered_df):
174
  return [comp_path, cluster_path]
175
 
176
  # ----------------------------
177
- # Gradio Interface
178
  # ----------------------------
179
 
180
- with gr.Blocks(title="Near vs Far Field Audio Analyzer") as demo:
181
- gr.Markdown("# πŸŽ™οΈ Near vs Far Field Speech Analysis Platform")
182
- gr.Markdown("Upload simultaneous near-field and far-field `.wav` recordings. Analyze, compare, and cluster frames.")
 
 
 
 
183
 
184
  with gr.Row():
185
- near_file = gr.File(label="Upload Near-Field Audio (.wav)", file_types=[".wav"])
186
- far_file = gr.File(label="Upload Far-Field Audio (.wav)", file_types=[".wav"])
187
 
188
  with gr.Accordion("βš™οΈ Frame Settings", open=True):
189
  frame_length_ms = gr.Slider(10, 500, value=50, step=1, label="Frame Length (ms)")
190
  hop_length_ms = gr.Slider(1, 250, value=25, step=1, label="Hop Length (ms)")
191
  window_type = gr.Dropdown(["hann", "hamming", "rectangular"], value="hann", label="Window Type")
192
 
193
- with gr.Accordion("πŸ“Š Comparison & Clustering", open=True):
194
- n_clusters = gr.Slider(2, 20, value=5, step=1, label="Number of Clusters (KMeans)")
195
  comparison_metrics = gr.CheckboxGroup(
196
- ["Euclidean Distance", "Cosine Similarity"],
197
- value=["Euclidean Distance", "Cosine Similarity"],
198
- label="Comparison Metrics"
 
 
 
 
 
 
 
 
199
  )
200
 
201
- btn = gr.Button("πŸš€ Analyze Audio Pair")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
  with gr.Tabs():
204
  with gr.Tab("πŸ“ˆ Frame Comparison"):
@@ -209,7 +326,10 @@ with gr.Blocks(title="Near vs Far Field Audio Analyzer") as demo:
209
  cluster_plot = gr.Plot()
210
  cluster_table = gr.Dataframe()
211
 
212
- with gr.Tab("πŸ“€ Export Results"):
 
 
 
213
  export_btn = gr.Button("πŸ’Ύ Download CSVs")
214
  export_files = gr.Files()
215
 
@@ -218,21 +338,20 @@ with gr.Blocks(title="Near vs Far Field Audio Analyzer") as demo:
218
  inputs=[
219
  near_file, far_file,
220
  frame_length_ms, hop_length_ms, window_type,
 
 
 
221
  n_clusters,
222
- comparison_metrics
223
  ],
224
- outputs=[comp_plot, comp_table, cluster_plot, cluster_table]
225
  )
226
 
227
  export_btn.click(
228
  fn=export_results,
229
- inputs=[comp_plot, comp_table, cluster_plot, cluster_table],
230
  outputs=export_files
231
  )
232
 
233
- # ----------------------------
234
- # Launch
235
- # ----------------------------
236
-
237
  if __name__ == "__main__":
238
  demo.launch()
 
2
  import librosa
3
  import numpy as np
4
  import pandas as pd
5
+ from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
6
+ from sklearn.metrics.pairwise import cosine_similarity, euclidean_distances
7
+ from scipy.spatial.distance import jensenshannon
8
+ from scipy.stats import pearsonr
9
+ from scipy.signal import get_window as scipy_get_window
10
  import plotly.express as px
11
+ import plotly.graph_objects as go
12
  import os
13
  import tempfile
 
14
 
15
  # ----------------------------
16
+ # Enhanced Feature Extraction (with spectral bins)
17
  # ----------------------------
18
 
19
+ def extract_features_with_spectrum(frames, sr):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  features = []
21
  n_mfcc = 13
22
+ n_fft = 2048
23
+
24
  for i in range(frames.shape[1]):
25
  frame = frames[:, i]
26
  feat = {}
27
 
28
+ # Basic features
29
  rms = np.mean(librosa.feature.rms(y=frame)[0])
30
  feat["rms"] = float(rms)
31
 
 
32
  sc = np.mean(librosa.feature.spectral_centroid(y=frame, sr=sr)[0])
33
  feat["spectral_centroid"] = float(sc)
34
 
 
35
  zcr = np.mean(librosa.feature.zero_crossing_rate(frame)[0])
36
  feat["zcr"] = float(zcr)
37
 
 
38
  mfccs = librosa.feature.mfcc(y=frame, sr=sr, n_mfcc=n_mfcc)
39
  for j in range(n_mfcc):
40
  feat[f"mfcc_{j+1}"] = float(np.mean(mfccs[j]))
41
 
42
+ # Spectral bins for lost frequencies
43
+ S = np.abs(librosa.stft(frame, n_fft=n_fft))
44
+ S_db = librosa.amplitude_to_db(S, ref=np.max)
45
+ freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft)
46
+
47
+ # Split spectrum: low (<2kHz), mid (2-4kHz), high (>4kHz)
48
+ low_mask = freqs <= 2000
49
+ mid_mask = (freqs > 2000) & (freqs <= 4000)
50
+ high_mask = freqs > 4000
51
+
52
+ feat["low_freq_energy"] = float(np.mean(S_db[low_mask]))
53
+ feat["mid_freq_energy"] = float(np.mean(S_db[mid_mask]))
54
+ feat["high_freq_energy"] = float(np.mean(S_db[high_mask]))
55
+
56
+ # Store full spectrum for later (optional)
57
+ feat["spectrum"] = S_db # will be used for heatmap
58
+
59
  features.append(feat)
60
+ return features
61
 
62
+ def compare_frames_enhanced(near_feats, far_feats, metrics):
63
+ min_len = min(len(near_feats), len(far_feats))
 
 
 
64
  results = {"frame_index": list(range(min_len))}
65
 
66
+ # Prepare vectors
67
+ near_df = pd.DataFrame([f for f in near_feats[:min_len]])
68
+ far_df = pd.DataFrame([f for f in far_feats[:min_len]])
69
+
70
+ # Remove non-numeric columns
71
+ near_vec = near_df.drop(columns=["spectrum"], errors="ignore").values
72
+ far_vec = far_df.drop(columns=["spectrum"], errors="ignore").values
73
+
74
+ # 1. Euclidean Distance
75
  if "Euclidean Distance" in metrics:
76
+ results["euclidean_dist"] = np.linalg.norm(near_vec - far_vec, axis=1).tolist()
77
+
78
+ # 2. Cosine Similarity
79
  if "Cosine Similarity" in metrics:
80
+ cos_vals = []
81
  for i in range(min_len):
82
+ a, b = near_vec[i].reshape(1, -1), far_vec[i].reshape(1, -1)
83
+ cos_vals.append(float(cosine_similarity(a, b)[0][0]))
84
+ results["cosine_similarity"] = cos_vals
85
+
86
+ # 3. Pearson Correlation
87
+ if "Pearson Correlation" in metrics:
88
+ corr_vals = []
89
+ for i in range(min_len):
90
+ corr, _ = pearsonr(near_vec[i], far_vec[i])
91
+ corr_vals.append(float(corr) if not np.isnan(corr) else 0.0)
92
+ results["pearson_corr"] = corr_vals
93
+
94
+ # 4. KL Divergence (on normalized features)
95
+ if "KL Divergence" in metrics:
96
+ kl_vals = []
97
+ for i in range(min_len):
98
+ p = near_vec[i] - near_vec[i].min() + 1e-8
99
+ q = far_vec[i] - far_vec[i].min() + 1e-8
100
+ p /= p.sum()
101
+ q /= q.sum()
102
+ kl = np.sum(p * np.log(p / q))
103
+ kl_vals.append(float(kl))
104
+ results["kl_divergence"] = kl_vals
105
+
106
+ # 5. Jensen-Shannon Divergence (symmetric, safer)
107
+ if "Jensen-Shannon Divergence" in metrics:
108
+ js_vals = []
109
+ for i in range(min_len):
110
+ p = near_vec[i] - near_vec[i].min() + 1e-8
111
+ q = far_vec[i] - far_vec[i].min() + 1e-8
112
+ p /= p.sum()
113
+ q /= q.sum()
114
+ js = jensenshannon(p, q)
115
+ js_vals.append(float(js))
116
+ results["js_divergence"] = js_vals
117
+
118
+ # 6. Lost High Frequencies Ratio
119
+ if "High-Freq Loss Ratio" in metrics:
120
+ loss_ratios = []
121
+ for i in range(min_len):
122
+ near_high = near_feats[i]["high_freq_energy"]
123
+ far_high = far_feats[i]["high_freq_energy"]
124
+ # Ratio: how much high-freq energy is lost (positive = loss)
125
+ ratio = near_high - far_high # in dB
126
+ loss_ratios.append(float(ratio))
127
+ results["high_freq_loss_db"] = loss_ratios
128
+
129
+ # 7. Spectral Centroid Shift
130
+ if "Spectral Centroid Shift" in metrics:
131
+ shifts = []
132
+ for i in range(min_len):
133
+ shift = near_feats[i]["spectral_centroid"] - far_feats[i]["spectral_centroid"]
134
+ shifts.append(float(shift))
135
+ results["centroid_shift"] = shifts
136
+
137
  return pd.DataFrame(results)
138
 
139
+ def cluster_frames_custom(features_df, cluster_features, algo, n_clusters=5, eps=0.5):
140
+ if not cluster_features:
141
+ raise gr.Error("Please select at least one feature for clustering.")
142
+
143
+ X = features_df[cluster_features].values
144
+
145
+ if algo == "KMeans":
146
+ model = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
147
+ elif algo == "Agglomerative":
148
+ model = AgglomerativeClustering(n_clusters=n_clusters)
149
+ elif algo == "DBSCAN":
150
+ model = DBSCAN(eps=eps, min_samples=3)
151
+ else:
152
+ raise ValueError("Unknown clustering algorithm")
153
+
154
+ labels = model.fit_predict(X)
155
  features_df = features_df.copy()
156
  features_df["cluster"] = labels
157
  return features_df
158
 
159
+ def plot_spectral_difference(near_feats, far_feats, frame_idx=0):
160
+ if frame_idx >= len(near_feats):
161
+ frame_idx = 0
162
+ near_spec = near_feats[frame_idx]["spectrum"]
163
+ far_spec = far_feats[frame_idx]["spectrum"]
164
+ diff = near_spec - far_spec # positive = energy lost in far-field
165
+
166
+ fig = go.Figure(data=go.Heatmap(
167
+ z=[diff],
168
+ colorscale='RdBu',
169
+ zmid=0,
170
+ colorbar=dict(title="dB Difference")
171
+ ))
172
+ fig.update_layout(
173
+ title=f"Spectral Difference (Frame {frame_idx}): Near - Far",
174
+ xaxis_title="Frequency Bins",
175
+ yaxis_title="",
176
+ height=300
177
+ )
178
+ return fig
179
+
180
  # ----------------------------
181
+ # Main Analysis Function
182
  # ----------------------------
183
 
184
  def analyze_audio_pair(
 
187
  frame_length_ms,
188
  hop_length_ms,
189
  window_type,
190
+ comparison_metrics,
191
+ cluster_features,
192
+ clustering_algo,
193
  n_clusters,
194
+ dbscan_eps
195
  ):
196
  if not near_file or not far_file:
197
+ raise gr.Error("Upload both audio files.")
198
+
 
199
  y_near, sr_near = load_audio(near_file)
200
  y_far, sr_far = load_audio(far_file)
201
 
 
202
  if sr_near != sr_far:
203
  y_far = librosa.resample(y_far, orig_sr=sr_far, target_sr=sr_near)
204
  sr = sr_near
205
  else:
206
  sr = sr_near
207
 
 
208
  frames_near, _ = segment_audio(y_near, sr, frame_length_ms, hop_length_ms, window_type)
209
  frames_far, _ = segment_audio(y_far, sr, frame_length_ms, hop_length_ms, window_type)
210
 
211
+ near_feats = extract_features_with_spectrum(frames_near, sr)
212
+ far_feats = extract_features_with_spectrum(frames_far, sr)
 
213
 
214
+ # Comparison
215
+ comparison_df = compare_frames_enhanced(near_feats, far_feats, comparison_metrics)
216
 
217
+ # Clustering (on near-field)
218
+ near_df = pd.DataFrame(near_feats)
219
+ near_df = near_df.drop(columns=["spectrum"], errors="ignore")
220
+ clustered_df = cluster_frames_custom(near_df, cluster_features, clustering_algo, n_clusters, dbscan_eps)
221
 
222
  # Plots
223
  plot_comparison = None
224
+ if comparison_df.shape[1] > 1:
225
+ metric_to_plot = [col for col in comparison_df.columns if col != "frame_index"][0]
226
  plot_comparison = px.line(
227
  comparison_df,
228
  x="frame_index",
229
+ y=metric_to_plot,
230
+ title=f"{metric_to_plot.replace('_', ' ').title()} Over Time"
231
  )
 
 
 
 
 
 
 
 
 
 
 
232
 
233
+ # Scatter: user-selected features
234
  plot_scatter = None
235
+ if len(cluster_features) >= 2:
236
+ x_feat, y_feat = cluster_features[0], cluster_features[1]
237
+ if x_feat in clustered_df.columns and y_feat in clustered_df.columns:
238
+ plot_scatter = px.scatter(
239
+ clustered_df,
240
+ x=x_feat,
241
+ y=y_feat,
242
+ color="cluster",
243
+ title=f"Clustering: {x_feat} vs {y_feat}",
244
+ hover_data=["cluster"]
245
+ )
246
  else:
247
+ plot_scatter = px.scatter(title="Select β‰₯2 features for scatter plot")
248
+
249
+ # Spectral difference heatmap (first frame)
250
+ spec_heatmap = plot_spectral_difference(near_feats, far_feats, frame_idx=0)
251
 
252
+ return (
253
+ plot_comparison,
254
+ comparison_df,
255
+ plot_scatter,
256
+ clustered_df,
257
+ spec_heatmap
258
+ )
259
 
260
+ def export_results(comparison_df, clustered_df):
261
  temp_dir = tempfile.mkdtemp()
262
  comp_path = os.path.join(temp_dir, "frame_comparisons.csv")
263
  cluster_path = os.path.join(temp_dir, "clustered_frames.csv")
 
266
  return [comp_path, cluster_path]
267
 
268
  # ----------------------------
269
+ # Gradio UI
270
  # ----------------------------
271
 
272
+ # Get feature names dynamically
273
+ dummy_features = ["rms", "spectral_centroid", "zcr"] + [f"mfcc_{i}" for i in range(1,14)] + \
274
+ ["low_freq_energy", "mid_freq_energy", "high_freq_energy"]
275
+
276
+ with gr.Blocks(title="Advanced Near vs Far Field Analyzer") as demo:
277
+ gr.Markdown("# πŸŽ™οΈ Advanced Near vs Far Field Speech Analyzer")
278
+ gr.Markdown("Upload simultaneous recordings. Analyze **lost frequencies**, **frame degradation**, and **cluster by custom attributes**.")
279
 
280
  with gr.Row():
281
+ near_file = gr.File(label="Near-Field Audio (.wav)", file_types=[".wav"])
282
+ far_file = gr.File(label="Far-Field Audio (.wav)", file_types=[".wav"])
283
 
284
  with gr.Accordion("βš™οΈ Frame Settings", open=True):
285
  frame_length_ms = gr.Slider(10, 500, value=50, step=1, label="Frame Length (ms)")
286
  hop_length_ms = gr.Slider(1, 250, value=25, step=1, label="Hop Length (ms)")
287
  window_type = gr.Dropdown(["hann", "hamming", "rectangular"], value="hann", label="Window Type")
288
 
289
+ with gr.Accordion("πŸ“Š Comparison Metrics", open=True):
 
290
  comparison_metrics = gr.CheckboxGroup(
291
+ choices=[
292
+ "Euclidean Distance",
293
+ "Cosine Similarity",
294
+ "Pearson Correlation",
295
+ "KL Divergence",
296
+ "Jensen-Shannon Divergence",
297
+ "High-Freq Loss Ratio",
298
+ "Spectral Centroid Shift"
299
+ ],
300
+ value=["High-Freq Loss Ratio", "Cosine Similarity"],
301
+ label="Select Comparison Metrics"
302
  )
303
 
304
+ with gr.Accordion("🧩 Clustering Configuration", open=True):
305
+ cluster_features = gr.CheckboxGroup(
306
+ choices=dummy_features,
307
+ value=["rms", "spectral_centroid", "high_freq_energy"],
308
+ label="Features to Use for Clustering"
309
+ )
310
+ clustering_algo = gr.Radio(
311
+ ["KMeans", "Agglomerative", "DBSCAN"],
312
+ value="KMeans",
313
+ label="Clustering Algorithm"
314
+ )
315
+ n_clusters = gr.Slider(2, 20, value=5, step=1, label="Number of Clusters (for KMeans/Agglomerative)")
316
+ dbscan_eps = gr.Slider(0.1, 2.0, value=0.5, step=0.1, label="DBSCAN eps (neighborhood radius)")
317
+
318
+ btn = gr.Button("πŸš€ Analyze")
319
 
320
  with gr.Tabs():
321
  with gr.Tab("πŸ“ˆ Frame Comparison"):
 
326
  cluster_plot = gr.Plot()
327
  cluster_table = gr.Dataframe()
328
 
329
+ with gr.Tab("πŸ” Spectral Analysis"):
330
+ spec_heatmap = gr.Plot(label="Spectral Difference (Near - Far)")
331
+
332
+ with gr.Tab("πŸ“€ Export"):
333
  export_btn = gr.Button("πŸ’Ύ Download CSVs")
334
  export_files = gr.Files()
335
 
 
338
  inputs=[
339
  near_file, far_file,
340
  frame_length_ms, hop_length_ms, window_type,
341
+ comparison_metrics,
342
+ cluster_features,
343
+ clustering_algo,
344
  n_clusters,
345
+ dbscan_eps
346
  ],
347
+ outputs=[comp_plot, comp_table, cluster_plot, cluster_table, spec_heatmap]
348
  )
349
 
350
  export_btn.click(
351
  fn=export_results,
352
+ inputs=[comp_table, cluster_table],
353
  outputs=export_files
354
  )
355
 
 
 
 
 
356
  if __name__ == "__main__":
357
  demo.launch()