rikhoffbauer2 commited on
Commit
63565aa
·
verified ·
1 Parent(s): d1fa59c

v6: Update app defaults for real music — delta=0.12, energy=-35, min_gap=0.03, NCC compare=0 (auto)

Browse files
Files changed (1) hide show
  1. app.py +65 -94
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- Gradio UI — Sample Extractor v4.
3
- NCC clustering, full parameter control, Demucs model selection.
4
  """
5
 
6
  import gradio as gr
@@ -105,7 +105,6 @@ def run_extraction(audio_in, stem_choice, demucs_model, demucs_shifts, demucs_ov
105
  zip_path = build_archive(clusters, bpm, stem_sr,
106
  midi_path=midi_path, rendered_audio=rendered)
107
 
108
- # Metrics
109
  rows = []
110
  for c in sorted(clusters, key=lambda x: x.count, reverse=True):
111
  best = c.best_hit
@@ -120,7 +119,11 @@ def run_extraction(audio_in, stem_choice, demucs_model, demucs_shifts, demucs_ov
120
  })
121
 
122
  summary = f"**Detected BPM: {bpm}** · **{len(clusters)} unique samples** from {len(hits)} hits\n\n"
123
- summary += f"Model: `{demucs_model}` · NCC threshold: `{ncc_threshold}` · Onset delta: `{onset_delta}`\n\n"
 
 
 
 
124
  summary += "| Sample | Hits | MIDI Note |\n|---|---|---|\n"
125
  for c in sorted(clusters, key=lambda x: x.count, reverse=True):
126
  summary += f"| {c.label} | {c.count} | {c.midi_note} |\n"
@@ -135,11 +138,10 @@ def run_extraction(audio_in, stem_choice, demucs_model, demucs_shifts, demucs_ov
135
 
136
  # ─── Tab 2: Evaluate ─────────────────────────────────────────────────────────
137
 
138
- def run_eval(pattern, bpm, bars, ncc_threshold, progress=gr.Progress()):
139
  progress(0.0, desc="Generating synthetic song...")
140
  song = generate_test_song(pattern_name=pattern, bars=int(bars),
141
  bpm=float(bpm), variation='medium', seed=42)
142
-
143
  detected_bpm = detect_bpm(song.drums_only, song.sr)
144
 
145
  progress(0.2, desc="Extracting...")
@@ -147,7 +149,8 @@ def run_eval(pattern, bpm, bars, ncc_threshold, progress=gr.Progress()):
147
  if not hits: return None, None, None, None, "", ""
148
 
149
  hits = classify_hits(hits)
150
- clusters = cluster_hits(hits, ncc_threshold=float(ncc_threshold))
 
151
  select_best(clusters)
152
  for c in clusters:
153
  if c.count >= 2: c.synthesized = synthesize_from_cluster(c)
@@ -161,9 +164,6 @@ def run_eval(pattern, bpm, bars, ncc_threshold, progress=gr.Progress()):
161
  for h in song.hits]
162
  report = evaluate_extraction(clusters, gt, gt_hits, song.sr, hits)
163
 
164
- mix_out = audio_tuple(song.mix, song.sr)
165
- rendered_out = audio_tuple(rendered, song.sr)
166
-
167
  summary = [
168
  {'Metric': 'Detected BPM', 'Value': f"{detected_bpm}", 'Target': f"{song.bpm}"},
169
  {'Metric': 'Clusters', 'Value': str(len(clusters)), 'Target': str(len(gt))},
@@ -173,59 +173,47 @@ def run_eval(pattern, bpm, bars, ncc_threshold, progress=gr.Progress()):
173
  ]
174
  if report.unmatched_gt:
175
  summary.append({'Metric': '⚠ Unmatched', 'Value': ', '.join(report.unmatched_gt), 'Target': 'None'})
176
-
177
  matches = [{'Cluster': m.cluster_label, 'GT': m.gt_name, 'SI-SDR': f"{m.si_sdr:.1f}",
178
  'Score': f"{m.sample_score:.1f}"} for m in report.matches]
179
 
180
  progress(1.0)
181
- return (mix_out, rendered_out, pd.DataFrame(summary),
182
- pd.DataFrame(matches) if matches else None, "", "")
183
 
184
 
185
- # ─── Tab 3: Optimize ─────────────────────────────────────────────────────────
186
 
187
  def run_optimize(n_iters, config_name, author, save_hub, progress=gr.Progress()):
188
  logs = []
189
- progress(0.0, desc="Starting optimization...")
190
- state = run_optimization(n_iterations=int(n_iters),
191
- config_name=config_name or "optimized",
192
- author=author or "anonymous",
193
- save_to_hub=bool(save_hub), log_fn=lambda m: logs.append(m))
194
  progress(1.0)
195
- hist = [{'Iter': r.iteration, 'Score': f"{r.avg_score:.1f}",
196
- 'Time': f"{r.duration_s:.1f}s"} for r in state.history]
197
  if state.history:
198
- fig, ax = plt.subplots(figsize=(10, 4))
199
  ax.plot([r.iteration for r in state.history], [r.avg_score for r in state.history], 'b-o')
200
  ax.set_xlabel('Iteration'); ax.set_ylabel('Score'); ax.grid(True, alpha=0.3); plt.tight_layout()
201
- else:
202
- fig, ax = plt.subplots(); ax.text(0.5, 0.5, "No data")
203
  return '\n'.join(logs), pd.DataFrame(hist), fig, json.dumps(state.best_config, indent=2)
204
 
205
-
206
- # ─── Tab 4: Leaderboard ──────────────────────────────────────────────────────
207
-
208
  def refresh_leaderboard():
209
  try:
210
  lb = get_leaderboard()
211
  return pd.DataFrame(lb) if lb else pd.DataFrame(), ""
212
- except Exception as e:
213
- return pd.DataFrame(), str(e)
214
 
215
 
216
  # ─── Build App ────────────────────────────────────────────────────────────────
217
 
218
- def get_stems_for_model(model_name):
219
- stems = DEMUCS_STEMS.get(model_name, ["drums", "bass", "other", "vocals"])
220
- return gr.update(choices=stems + ["all"], value=stems[0])
221
-
222
  def build_app():
223
  with gr.Blocks(title="🎵 Sample Extractor", theme=gr.themes.Soft(),
224
  css=".gradio-container{max-width:1300px!important}") as app:
225
- gr.Markdown("# 🎵 Sample Extractor v4\n"
226
- "Extract distinct sounds from audio using **NCC waveform matching** "
227
- "correctly groups identical samples regardless of velocity.\n"
228
- "Full control over Demucs model, onset detection, and clustering parameters.")
229
 
230
  with gr.Tabs():
231
  # ── Extract ──
@@ -234,51 +222,36 @@ def build_app():
234
 
235
  with gr.Accordion("🔧 Stem Separation", open=False):
236
  with gr.Row():
237
- demucs_model = gr.Dropdown(DEMUCS_MODELS, value="htdemucs_ft",
238
- label="Demucs Model")
239
- stem_dd = gr.Dropdown(['drums','bass','other','vocals','all'],
240
- value='drums', label='Stem')
241
- demucs_shifts = gr.Slider(0, 5, value=1, step=1,
242
- label='Shifts (TTA, 0=fastest)')
243
- demucs_overlap = gr.Slider(0.0, 0.5, value=0.25, step=0.05,
244
- label='Overlap')
245
 
246
  with gr.Accordion("🎯 Onset Detection", open=False):
247
  with gr.Row():
248
- onset_mode = gr.Dropdown(['auto','percussive','harmonic','broadband'],
249
- value='auto', label='Mode')
250
- onset_delta = gr.Slider(0.01, 0.5, value=0.07, step=0.01,
251
- label='Delta (sensitivity)')
252
- energy_db = gr.Slider(-70, -10, value=-45, step=1,
253
- label='Energy threshold (dB)')
254
  with gr.Row():
255
- pre_pad = gr.Slider(0.0, 0.05, value=0.005, step=0.001,
256
- label='Pre-pad (s)')
257
- min_dur = gr.Slider(0.005, 0.2, value=0.02, step=0.005,
258
- label='Min duration (s)')
259
- max_dur = gr.Slider(0.1, 5.0, value=1.5, step=0.1,
260
- label='Max duration (s)')
261
- min_gap = gr.Slider(0.005, 0.2, value=0.015, step=0.005,
262
- label='Min gap (s)')
263
-
264
- with gr.Accordion("🔗 Clustering", open=False):
265
  with gr.Row():
266
- ncc_thresh = gr.Slider(0.3, 0.99, value=0.80, step=0.01,
267
- label='NCC threshold (higher = stricter)')
268
- ncc_ms = gr.Slider(50, 1000, value=200, step=50,
269
- label='Compare window (ms)')
270
- linkage_dd = gr.Dropdown(['average', 'complete', 'single'],
271
- value='average', label='Linkage')
272
  with gr.Row():
273
- target_min = gr.Number(value=0, label='Target min clusters (0 = use threshold)',
274
- precision=0)
275
- target_max = gr.Number(value=0, label='Target max clusters (0 = use threshold)',
276
- precision=0)
277
- gr.Markdown("*Set both target min/max > 0 to auto-search for the right threshold. "
278
- "Leave at 0 to use the NCC threshold directly.*")
279
 
280
  with gr.Accordion("⚙️ Post-processing", open=False):
281
- do_synth = gr.Checkbox(value=True, label='Synthesize optimal samples from clusters')
282
 
283
  extract_btn = gr.Button("🔬 Extract Samples", variant="primary", size="lg")
284
 
@@ -286,23 +259,18 @@ def build_app():
286
  with gr.Row():
287
  stem_out = gr.Audio(type='numpy', label='Stem', interactive=False)
288
  rendered_out = gr.Audio(type='numpy', label='🔊 Reconstruction', interactive=False)
289
-
290
  gr.Markdown("### Downloads")
291
  with gr.Row():
292
  archive_file = gr.File(label="📦 ZIP Archive", interactive=False)
293
  midi_file = gr.File(label="🎹 MIDI", interactive=False)
294
- sample_files = gr.File(label="Individual WAV samples", file_count="multiple",
295
- interactive=False)
296
  metrics_tbl = gr.Dataframe(label="Extracted Samples")
297
  status_txt = gr.Textbox(visible=False)
298
 
299
- # Update available stems when model changes
300
  demucs_model.change(
301
- fn=lambda m: gr.update(choices=DEMUCS_STEMS.get(m, ["drums","bass","other","vocals"]) + ["all"]),
302
  inputs=[demucs_model], outputs=[stem_dd])
303
-
304
- extract_btn.click(
305
- run_extraction,
306
  [audio_in, stem_dd, demucs_model, demucs_shifts, demucs_overlap,
307
  onset_mode, onset_delta, energy_db, pre_pad, min_dur, max_dur, min_gap,
308
  ncc_thresh, ncc_ms, linkage_dd, target_min, target_max, do_synth],
@@ -316,7 +284,10 @@ def build_app():
316
  ev_pat = gr.Dropdown(['rock','funk','halftime'], value='rock', label='Pattern')
317
  ev_bpm = gr.Slider(80, 200, value=120, step=2, label='BPM')
318
  ev_bars = gr.Slider(2, 8, value=4, step=1, label='Bars')
319
- ev_ncc = gr.Slider(0.5, 0.99, value=0.80, step=0.01, label='NCC threshold')
 
 
 
320
  ev_btn = gr.Button("🧪 Evaluate", variant="primary", size="lg")
321
  with gr.Row():
322
  ev_mix = gr.Audio(type='numpy', label='Original', interactive=False)
@@ -324,31 +295,31 @@ def build_app():
324
  ev_summary = gr.Dataframe(label="Summary")
325
  ev_matches = gr.Dataframe(label="Matches")
326
  ev_s1 = gr.Textbox(visible=False); ev_s2 = gr.Textbox(visible=False)
327
- ev_btn.click(run_eval, [ev_pat, ev_bpm, ev_bars, ev_ncc],
328
  [ev_mix, ev_rendered, ev_summary, ev_matches, ev_s1, ev_s2])
329
 
330
  # ── Optimize ──
331
  with gr.Tab("🔄 Optimize"):
332
- gr.Markdown("### Autonomous Optimization\nTests across 6 diverse songs, saves best config to Hub.")
333
  with gr.Row():
334
- opt_n = gr.Slider(2, 30, value=5, step=1, label='Iterations')
335
- opt_name = gr.Textbox(value="optimized", label='Config name')
336
- opt_author = gr.Textbox(value="", label='Author')
337
- opt_save = gr.Checkbox(value=True, label='Save to Hub')
338
  opt_btn = gr.Button("🚀 Optimize", variant="primary", size="lg")
339
- opt_log = gr.Textbox(label="Log", lines=20, max_lines=40)
340
  opt_hist = gr.Dataframe(label="History")
341
  opt_plot = gr.Plot(label="Progress")
342
- opt_params = gr.Code(label="Best Config", language="json")
343
- opt_btn.click(run_optimize, [opt_n, opt_name, opt_author, opt_save],
344
- [opt_log, opt_hist, opt_plot, opt_params])
345
 
346
  # ── Leaderboard ──
347
  with gr.Tab("🏆 Leaderboard"):
348
  gr.Markdown("### Config Leaderboard")
349
  lb_btn = gr.Button("🔄 Refresh"); lb_tbl = gr.Dataframe()
350
  lb_s = gr.Textbox(visible=False)
351
- lb_btn.click(refresh_leaderboard, [], [lb_tbl, lb_s])
352
 
353
  return app
354
 
 
1
  """
2
+ Gradio UI — Sample Extractor v6.
3
+ NCC clustering with target range, auto-scale compare window, better defaults.
4
  """
5
 
6
  import gradio as gr
 
105
  zip_path = build_archive(clusters, bpm, stem_sr,
106
  midi_path=midi_path, rendered_audio=rendered)
107
 
 
108
  rows = []
109
  for c in sorted(clusters, key=lambda x: x.count, reverse=True):
110
  best = c.best_hit
 
119
  })
120
 
121
  summary = f"**Detected BPM: {bpm}** · **{len(clusters)} unique samples** from {len(hits)} hits\n\n"
122
+ summary += f"Model: `{demucs_model}` · Onset delta: `{onset_delta}` · Energy: `{energy_db}dB`\n\n"
123
+ if int(target_min) > 0 and int(target_max) > 0:
124
+ summary += f"Target clusters: `{int(target_min)}–{int(target_max)}`\n\n"
125
+ else:
126
+ summary += f"NCC threshold: `{ncc_threshold}`\n\n"
127
  summary += "| Sample | Hits | MIDI Note |\n|---|---|---|\n"
128
  for c in sorted(clusters, key=lambda x: x.count, reverse=True):
129
  summary += f"| {c.label} | {c.count} | {c.midi_note} |\n"
 
138
 
139
  # ─── Tab 2: Evaluate ─────────────────────────────────────────────────────────
140
 
141
+ def run_eval(pattern, bpm, bars, ncc_threshold, target_min, target_max, progress=gr.Progress()):
142
  progress(0.0, desc="Generating synthetic song...")
143
  song = generate_test_song(pattern_name=pattern, bars=int(bars),
144
  bpm=float(bpm), variation='medium', seed=42)
 
145
  detected_bpm = detect_bpm(song.drums_only, song.sr)
146
 
147
  progress(0.2, desc="Extracting...")
 
149
  if not hits: return None, None, None, None, "", ""
150
 
151
  hits = classify_hits(hits)
152
+ clusters = cluster_hits(hits, ncc_threshold=float(ncc_threshold),
153
+ target_min=int(target_min), target_max=int(target_max))
154
  select_best(clusters)
155
  for c in clusters:
156
  if c.count >= 2: c.synthesized = synthesize_from_cluster(c)
 
164
  for h in song.hits]
165
  report = evaluate_extraction(clusters, gt, gt_hits, song.sr, hits)
166
 
 
 
 
167
  summary = [
168
  {'Metric': 'Detected BPM', 'Value': f"{detected_bpm}", 'Target': f"{song.bpm}"},
169
  {'Metric': 'Clusters', 'Value': str(len(clusters)), 'Target': str(len(gt))},
 
173
  ]
174
  if report.unmatched_gt:
175
  summary.append({'Metric': '⚠ Unmatched', 'Value': ', '.join(report.unmatched_gt), 'Target': 'None'})
 
176
  matches = [{'Cluster': m.cluster_label, 'GT': m.gt_name, 'SI-SDR': f"{m.si_sdr:.1f}",
177
  'Score': f"{m.sample_score:.1f}"} for m in report.matches]
178
 
179
  progress(1.0)
180
+ return (audio_tuple(song.mix, song.sr), audio_tuple(rendered, song.sr),
181
+ pd.DataFrame(summary), pd.DataFrame(matches) if matches else None, "", "")
182
 
183
 
184
+ # ─── Tab 3 & 4: Optimize + Leaderboard ───────────────────────────────────────
185
 
186
  def run_optimize(n_iters, config_name, author, save_hub, progress=gr.Progress()):
187
  logs = []
188
+ progress(0.0)
189
+ state = run_optimization(n_iterations=int(n_iters), config_name=config_name or "optimized",
190
+ author=author or "anon", save_to_hub=bool(save_hub),
191
+ log_fn=lambda m: logs.append(m))
 
192
  progress(1.0)
193
+ hist = [{'Iter': r.iteration, 'Score': f"{r.avg_score:.1f}", 'Time': f"{r.duration_s:.1f}s"}
194
+ for r in state.history]
195
  if state.history:
196
+ fig, ax = plt.subplots(figsize=(10,4))
197
  ax.plot([r.iteration for r in state.history], [r.avg_score for r in state.history], 'b-o')
198
  ax.set_xlabel('Iteration'); ax.set_ylabel('Score'); ax.grid(True, alpha=0.3); plt.tight_layout()
199
+ else: fig, ax = plt.subplots(); ax.text(0.5,0.5,"No data")
 
200
  return '\n'.join(logs), pd.DataFrame(hist), fig, json.dumps(state.best_config, indent=2)
201
 
 
 
 
202
  def refresh_leaderboard():
203
  try:
204
  lb = get_leaderboard()
205
  return pd.DataFrame(lb) if lb else pd.DataFrame(), ""
206
+ except Exception as e: return pd.DataFrame(), str(e)
 
207
 
208
 
209
  # ─── Build App ────────────────────────────────────────────────────────────────
210
 
 
 
 
 
211
  def build_app():
212
  with gr.Blocks(title="🎵 Sample Extractor", theme=gr.themes.Soft(),
213
  css=".gradio-container{max-width:1300px!important}") as app:
214
+ gr.Markdown("# 🎵 Sample Extractor v6\n"
215
+ "Extract distinct sounds from audio using **NCC waveform matching**. "
216
+ "Set a **target cluster range** to control how many unique samples to extract.")
 
217
 
218
  with gr.Tabs():
219
  # ── Extract ──
 
222
 
223
  with gr.Accordion("🔧 Stem Separation", open=False):
224
  with gr.Row():
225
+ demucs_model = gr.Dropdown(DEMUCS_MODELS, value="htdemucs_ft", label="Demucs Model")
226
+ stem_dd = gr.Dropdown(['drums','bass','other','vocals','all'], value='drums', label='Stem')
227
+ demucs_shifts = gr.Slider(0, 5, value=1, step=1, label='Shifts (0=fastest)')
228
+ demucs_overlap = gr.Slider(0.0, 0.5, value=0.25, step=0.05, label='Overlap')
 
 
 
 
229
 
230
  with gr.Accordion("🎯 Onset Detection", open=False):
231
  with gr.Row():
232
+ onset_mode = gr.Dropdown(['auto','percussive','harmonic','broadband'], value='auto', label='Mode')
233
+ onset_delta = gr.Slider(0.01, 0.5, value=0.12, step=0.01, label='Delta (lower=more onsets)')
234
+ energy_db = gr.Slider(-70, -10, value=-35, step=1, label='Energy threshold (dB)')
 
 
 
235
  with gr.Row():
236
+ pre_pad = gr.Slider(0.0, 0.05, value=0.005, step=0.001, label='Pre-pad (s)')
237
+ min_dur = gr.Slider(0.005, 0.2, value=0.02, step=0.005, label='Min duration (s)')
238
+ max_dur = gr.Slider(0.1, 5.0, value=1.5, step=0.1, label='Max duration (s)')
239
+ min_gap = gr.Slider(0.005, 0.2, value=0.03, step=0.005, label='Min gap (s)')
240
+
241
+ with gr.Accordion("🔗 Clustering", open=True):
242
+ gr.Markdown("**Target cluster range** — set both > 0 to auto-find the right threshold:")
 
 
 
243
  with gr.Row():
244
+ target_min = gr.Number(value=5, label='Target min clusters', precision=0)
245
+ target_max = gr.Number(value=20, label='Target max clusters', precision=0)
246
+ gr.Markdown("Or set both to 0 and use manual threshold:")
 
 
 
247
  with gr.Row():
248
+ ncc_thresh = gr.Slider(0.3, 0.99, value=0.80, step=0.01, label='NCC threshold')
249
+ ncc_ms = gr.Slider(0, 1000, value=0, step=50,
250
+ label='Compare window ms (0=auto)')
251
+ linkage_dd = gr.Dropdown(['average','complete','single'], value='average', label='Linkage')
 
 
252
 
253
  with gr.Accordion("⚙️ Post-processing", open=False):
254
+ do_synth = gr.Checkbox(value=True, label='Synthesize optimal samples')
255
 
256
  extract_btn = gr.Button("🔬 Extract Samples", variant="primary", size="lg")
257
 
 
259
  with gr.Row():
260
  stem_out = gr.Audio(type='numpy', label='Stem', interactive=False)
261
  rendered_out = gr.Audio(type='numpy', label='🔊 Reconstruction', interactive=False)
 
262
  gr.Markdown("### Downloads")
263
  with gr.Row():
264
  archive_file = gr.File(label="📦 ZIP Archive", interactive=False)
265
  midi_file = gr.File(label="🎹 MIDI", interactive=False)
266
+ sample_files = gr.File(label="Individual WAV samples", file_count="multiple", interactive=False)
 
267
  metrics_tbl = gr.Dataframe(label="Extracted Samples")
268
  status_txt = gr.Textbox(visible=False)
269
 
 
270
  demucs_model.change(
271
+ fn=lambda m: gr.update(choices=DEMUCS_STEMS.get(m,["drums","bass","other","vocals"])+["all"]),
272
  inputs=[demucs_model], outputs=[stem_dd])
273
+ extract_btn.click(run_extraction,
 
 
274
  [audio_in, stem_dd, demucs_model, demucs_shifts, demucs_overlap,
275
  onset_mode, onset_delta, energy_db, pre_pad, min_dur, max_dur, min_gap,
276
  ncc_thresh, ncc_ms, linkage_dd, target_min, target_max, do_synth],
 
284
  ev_pat = gr.Dropdown(['rock','funk','halftime'], value='rock', label='Pattern')
285
  ev_bpm = gr.Slider(80, 200, value=120, step=2, label='BPM')
286
  ev_bars = gr.Slider(2, 8, value=4, step=1, label='Bars')
287
+ with gr.Row():
288
+ ev_ncc = gr.Slider(0.3, 0.99, value=0.80, step=0.01, label='NCC threshold')
289
+ ev_tmin = gr.Number(value=0, label='Target min', precision=0)
290
+ ev_tmax = gr.Number(value=0, label='Target max', precision=0)
291
  ev_btn = gr.Button("🧪 Evaluate", variant="primary", size="lg")
292
  with gr.Row():
293
  ev_mix = gr.Audio(type='numpy', label='Original', interactive=False)
 
295
  ev_summary = gr.Dataframe(label="Summary")
296
  ev_matches = gr.Dataframe(label="Matches")
297
  ev_s1 = gr.Textbox(visible=False); ev_s2 = gr.Textbox(visible=False)
298
+ ev_btn.click(run_eval, [ev_pat, ev_bpm, ev_bars, ev_ncc, ev_tmin, ev_tmax],
299
  [ev_mix, ev_rendered, ev_summary, ev_matches, ev_s1, ev_s2])
300
 
301
  # ── Optimize ──
302
  with gr.Tab("🔄 Optimize"):
303
+ gr.Markdown("### Autonomous Optimization\nTests across 6 diverse songs.")
304
  with gr.Row():
305
+ opt_n = gr.Slider(2,30,value=5,step=1,label='Iterations')
306
+ opt_name = gr.Textbox(value="optimized",label='Config name')
307
+ opt_author = gr.Textbox(value="",label='Author')
308
+ opt_save = gr.Checkbox(value=True,label='Save to Hub')
309
  opt_btn = gr.Button("🚀 Optimize", variant="primary", size="lg")
310
+ opt_log = gr.Textbox(label="Log",lines=20,max_lines=40)
311
  opt_hist = gr.Dataframe(label="History")
312
  opt_plot = gr.Plot(label="Progress")
313
+ opt_params = gr.Code(label="Best Config",language="json")
314
+ opt_btn.click(run_optimize,[opt_n,opt_name,opt_author,opt_save],
315
+ [opt_log,opt_hist,opt_plot,opt_params])
316
 
317
  # ── Leaderboard ──
318
  with gr.Tab("🏆 Leaderboard"):
319
  gr.Markdown("### Config Leaderboard")
320
  lb_btn = gr.Button("🔄 Refresh"); lb_tbl = gr.Dataframe()
321
  lb_s = gr.Textbox(visible=False)
322
+ lb_btn.click(refresh_leaderboard,[],[lb_tbl,lb_s])
323
 
324
  return app
325