rikhoffbauer2 commited on
Commit
c8ec577
·
verified ·
1 Parent(s): a0d19cc

v9: Pass stem audio to cluster_hits for transient extraction, add quantize/subdivision to MIDI export"

Browse files
Files changed (1) hide show
  1. app.py +100 -150
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- Gradio UI — Sample Extractor v8.
3
- Auto-tune with parameter locking.
4
  """
5
 
6
  import gradio as gr
@@ -28,107 +28,75 @@ def audio_tuple(a, sr):
28
  if pk > 0: a = a / pk * 0.95
29
  return (sr, a)
30
 
31
-
32
- # ─── Auto-tune with locks ────────────────────────────────────────────────────
33
-
34
  def run_auto_tune(audio_in, stem_choice, demucs_model, demucs_shifts, demucs_overlap,
35
- onset_mode,
36
- # Current values (used when locked)
37
- cur_delta, cur_energy, cur_gap, cur_tmin, cur_tmax,
38
- # Lock flags
39
- lock_delta, lock_energy, lock_gap, lock_targets,
40
- progress=gr.Progress()):
41
- if audio_in is None:
42
- return [gr.update()] * 5 + ["Upload audio first", ""]
43
-
44
- # Build locks dict from checkboxes
45
  locks = {}
46
  if lock_delta: locks['onset_delta'] = float(cur_delta)
47
  if lock_energy: locks['energy_threshold_db'] = float(cur_energy)
48
  if lock_gap: locks['min_gap'] = float(cur_gap)
49
- if lock_targets:
50
- locks['target_min'] = int(cur_tmin)
51
- locks['target_max'] = int(cur_tmax)
52
-
53
- progress(0.0, desc="Loading audio...")
54
- sr_in, data = audio_in
55
- data = data.astype(np.float32)
56
- if data.ndim > 1: data = data.mean(axis=1)
57
- pk = np.abs(data).max()
58
- if pk > 0: data = data / pk
59
-
60
- with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
61
- sf.write(f.name, data, sr_in); tmp = f.name
62
-
63
  try:
64
- progress(0.05, desc=f"Extracting {stem_choice} stem...")
65
- stem_audio, stem_sr = extract_stem(tmp, stem=stem_choice, device="cpu",
66
- model_name=demucs_model, shifts=int(demucs_shifts), overlap=float(demucs_overlap))
67
-
68
- lock_desc = ', '.join(f'{k}={v}' for k, v in locks.items()) if locks else 'none'
69
- progress(0.15, desc=f"Auto-tuning (locked: {lock_desc})...")
70
- best_params, best_score, log_lines = auto_tune(
71
- stem_audio, stem_sr, mode=onset_mode, locks=locks)
72
-
73
- progress(1.0, desc=f"Score: {best_score:.1f}")
74
-
75
- log_text = '\n'.join(log_lines[-30:])
76
- lock_info = f"🔒 Locked: {lock_desc}" if locks else "No locks — all params tuned freely"
77
- summary = (f"**Auto-tune complete!** Score: **{best_score:.1f}/100**\n\n"
78
- f"{lock_info}\n\n"
79
- f"Click **Extract Samples** to run with these settings.")
80
-
81
- # Return updated values — only update unlocked params
82
  return [
83
- gr.update(value=best_params['onset_delta']) if not lock_delta else gr.update(),
84
- gr.update(value=best_params['energy_threshold_db']) if not lock_energy else gr.update(),
85
- gr.update(value=best_params['min_gap']) if not lock_gap else gr.update(),
86
- gr.update(value=best_params.get('target_min', 5)) if not lock_targets else gr.update(),
87
- gr.update(value=best_params.get('target_max', 20)) if not lock_targets else gr.update(),
88
- summary,
89
- log_text,
90
- ]
91
- finally:
92
- os.unlink(tmp)
93
-
94
-
95
- # ─── Extract ──────────────────────────────────────────────────────────────────
96
 
97
  def run_extraction(audio_in, stem_choice, demucs_model, demucs_shifts, demucs_overlap,
98
  onset_mode, onset_delta, energy_db, pre_pad, min_dur, max_dur, min_gap,
99
- ncc_threshold, ncc_compare_ms, linkage, target_min, target_max,
100
- do_synthesize, progress=gr.Progress()):
101
  if audio_in is None: return [None]*8
102
- progress(0.0, desc="Loading...")
103
- sr_in, data = audio_in; data = data.astype(np.float32)
104
- if data.ndim > 1: data = data.mean(axis=1)
105
- pk = np.abs(data).max()
106
- if pk > 0: data = data / pk
107
- with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
108
- sf.write(f.name, data, sr_in); tmp = f.name
109
  try:
110
- progress(0.05, desc=f"Stem ({demucs_model})...")
111
- sa, ssr = extract_stem(tmp, stem=stem_choice, device="cpu",
112
- model_name=demucs_model, shifts=int(demucs_shifts), overlap=float(demucs_overlap))
113
- progress(0.15, desc="BPM..."); bpm = detect_bpm(sa, ssr)
114
- progress(0.25, desc="Onsets...")
115
- hits = detect_onsets(sa, ssr, mode=onset_mode, onset_delta=float(onset_delta),
116
- energy_threshold_db=float(energy_db), pre_pad=float(pre_pad),
117
- min_dur=float(min_dur), max_dur=float(max_dur), min_gap=float(min_gap))
118
  if not hits:
119
- return (audio_tuple(sa,ssr), f"**BPM: {bpm}** — No hits.", None,None,None,None,"",pd.DataFrame())
120
- progress(0.35, desc="Classify..."); hits = classify_hits(hits)
121
- progress(0.45, desc="Cluster...")
122
- cl = cluster_hits(hits, ncc_threshold=float(ncc_threshold), max_compare_ms=float(ncc_compare_ms),
123
- target_min=int(target_min), target_max=int(target_max), linkage=str(linkage))
124
- progress(0.65, desc="Select..."); select_best(cl)
125
  if do_synthesize:
126
- progress(0.7, desc="Synth...")
127
  for c in cl:
128
  if c.count>=2: c.synthesized=synthesize_from_cluster(c)
129
- progress(0.75, desc="MIDI..."); mp=tempfile.mktemp(suffix='.mid'); export_midi(cl,mp,bpm=bpm)
130
- progress(0.8, desc="Render..."); rend=render_midi_with_samples(cl,sr=ssr)
131
- progress(0.85, desc="Package...")
 
132
  sd=tempfile.mkdtemp(); sp=[]
133
  for c in sorted(cl,key=lambda x:x.count,reverse=True):
134
  p=os.path.join(sd,f"{c.label}.wav"); c.best_hit.save(p); sp.append(p)
@@ -142,24 +110,23 @@ def run_extraction(audio_in, stem_choice, demucs_model, demucs_shifts, demucs_ov
142
  'Dur':f"{b.duration*1000:.0f}ms",
143
  'First':f"{sorted(h.onset_time for h in c.hits)[0]:.2f}s"})
144
  sm=f"**BPM: {bpm}** · **{len(cl)} samples** from {len(hits)} hits\n\n"
145
- sm+=f"`{demucs_model}` · δ=`{onset_delta}` · E=`{energy_db}dB`"
146
  if int(target_min)>0 and int(target_max)>0: sm+=f" · clusters `{int(target_min)}–{int(target_max)}`"
 
147
  sm+="\n\n| Sample | Hits | MIDI |\n|---|---|---|\n"
148
  for c in sorted(cl,key=lambda x:x.count,reverse=True): sm+=f"| {c.label} | {c.count} | {c.midi_note} |\n"
149
  progress(1.0)
150
  return (audio_tuple(sa,ssr),sm,audio_tuple(rend,ssr),sp,mp,zp,"",pd.DataFrame(rows))
151
  finally: os.unlink(tmp)
152
 
153
-
154
- # ─── Evaluate ─────────────────────────────────────────────────────────────────
155
-
156
- def run_eval(pattern, bpm, bars, ncc_threshold, target_min, target_max, progress=gr.Progress()):
157
  progress(0.0); song=generate_test_song(pattern_name=pattern,bars=int(bars),bpm=float(bpm),variation='medium',seed=42)
158
  dbpm=detect_bpm(song.drums_only,song.sr); progress(0.2)
159
  hits=detect_onsets(song.drums_only,song.sr)
160
  if not hits: return None,None,None,None,"",""
161
  hits=classify_hits(hits)
162
- cl=cluster_hits(hits,ncc_threshold=float(ncc_threshold),target_min=int(target_min),target_max=int(target_max))
 
163
  select_best(cl)
164
  for c in cl:
165
  if c.count>=2: c.synthesized=synthesize_from_cluster(c)
@@ -171,14 +138,14 @@ def run_eval(pattern, bpm, bars, ncc_threshold, target_min, target_max, progress
171
  {'Metric':'Clusters','Value':str(len(cl)),'Target':str(len(gt))},
172
  {'Metric':'Score','Value':f"{r.overall_score:.1f}/100",'Target':'> 70'}]
173
  if r.unmatched_gt: s.append({'Metric':'⚠','Value':', '.join(r.unmatched_gt),'Target':'None'})
174
- m=[{'Cluster':m.cluster_label,'GT':m.gt_name,'Score':f"{m.sample_score:.1f}"} for m in r.matches]
175
  progress(1.0)
176
  return (audio_tuple(song.mix,song.sr),audio_tuple(rend,song.sr),pd.DataFrame(s),pd.DataFrame(m) if m else None,"","")
177
 
178
- def run_optimize(n_iters,config_name,author,save_hub,progress=gr.Progress()):
179
  logs=[]; progress(0.0)
180
- state=run_optimization(n_iterations=int(n_iters),config_name=config_name or "opt",
181
- author=author or "anon",save_to_hub=bool(save_hub),log_fn=lambda m:logs.append(m))
182
  progress(1.0)
183
  h=[{'Iter':r.iteration,'Score':f"{r.avg_score:.1f}"} for r in state.history]
184
  if state.history:
@@ -188,70 +155,66 @@ def run_optimize(n_iters,config_name,author,save_hub,progress=gr.Progress()):
188
  return '\n'.join(logs),pd.DataFrame(h),fig,json.dumps(state.best_config,indent=2)
189
 
190
  def refresh_lb():
191
- try:
192
- lb=get_leaderboard(); return pd.DataFrame(lb) if lb else pd.DataFrame(),""
193
  except Exception as e: return pd.DataFrame(),str(e)
194
 
195
-
196
- # ─── App ──────────────────────────────────────────────────────────────────────
197
-
198
  def build_app():
199
  with gr.Blocks(title="🎵 Sample Extractor",theme=gr.themes.Soft(),
200
- css=".gradio-container{max-width:1300px!important} .lock-row{align-items:center}") as app:
201
- gr.Markdown("# 🎵 Sample Extractor v8\n"
202
- "**Auto-Tune** finds optimal parameters for your audio. "
203
- "🔒 **Lock** any parameter to constrain the search.")
204
 
205
  with gr.Tabs():
206
  with gr.Tab("🎵 Extract"):
207
- audio_in = gr.Audio(sources=['upload'], type='numpy', label='Upload Audio')
208
 
209
- with gr.Accordion("🔧 Stem Separation", open=False):
210
  with gr.Row():
211
  dm=gr.Dropdown(DEMUCS_MODELS,value="htdemucs_ft",label="Model")
212
  st=gr.Dropdown(['drums','bass','other','vocals','all'],value='drums',label='Stem')
213
  dsh=gr.Slider(0,5,value=1,step=1,label='Shifts')
214
  dov=gr.Slider(0.0,0.5,value=0.25,step=0.05,label='Overlap')
215
 
216
- with gr.Accordion("🎯 Onset Detection", open=False):
 
217
  with gr.Row():
218
- om=gr.Dropdown(['auto','percussive','harmonic','broadband'],value='auto',label='Mode')
219
- with gr.Row(elem_classes="lock-row"):
220
  od=gr.Slider(0.01,0.5,value=0.12,step=0.01,label='Delta')
221
  lock_od=gr.Checkbox(value=False,label='🔒',scale=0)
222
- with gr.Row(elem_classes="lock-row"):
223
  ed=gr.Slider(-70,-10,value=-35,step=1,label='Energy (dB)')
224
  lock_ed=gr.Checkbox(value=False,label='🔒',scale=0)
225
- with gr.Row(elem_classes="lock-row"):
226
  mg=gr.Slider(0.005,0.2,value=0.03,step=0.005,label='Min gap (s)')
227
  lock_mg=gr.Checkbox(value=False,label='🔒',scale=0)
228
  with gr.Row():
229
- pp=gr.Slider(0.0,0.05,value=0.005,step=0.001,label='Pre-pad (s)')
230
  mnd=gr.Slider(0.005,0.2,value=0.02,step=0.005,label='Min dur (s)')
231
  mxd=gr.Slider(0.1,5.0,value=1.5,step=0.1,label='Max dur (s)')
232
 
233
- with gr.Accordion("🔗 Clustering", open=True):
234
- with gr.Row(elem_classes="lock-row"):
235
- tmin=gr.Number(value=5,label='Target min clusters',precision=0)
236
- tmax=gr.Number(value=20,label='Target max clusters',precision=0)
237
  lock_tgt=gr.Checkbox(value=True,label='🔒 Lock range',scale=0)
238
- gr.Markdown("*🔒 = auto-tune will respect this value. Unchecked = auto-tune will change it.*")
239
  with gr.Row():
240
  nt=gr.Slider(0.3,0.99,value=0.80,step=0.01,label='NCC threshold')
241
- nms=gr.Slider(0,1000,value=0,step=50,label='Compare ms (0=auto)')
242
  lnk=gr.Dropdown(['average','complete','single'],value='average',label='Linkage')
243
 
244
- with gr.Accordion("⚙️ Post-processing", open=False):
245
- syn=gr.Checkbox(value=True,label='Synthesize optimal samples')
 
 
 
246
 
247
  with gr.Row():
248
  tune_btn=gr.Button("🎛️ Auto-Tune",variant="secondary",size="lg")
249
- extract_btn=gr.Button("🔬 Extract Samples",variant="primary",size="lg")
250
-
251
- tune_summary=gr.Markdown("")
252
- tune_log=gr.Textbox(label="Auto-tune log",lines=8,max_lines=15,visible=False)
253
 
254
- summary_md=gr.Markdown("*Upload audio → Auto-Tune or Extract*")
 
255
  with gr.Row():
256
  stem_out=gr.Audio(type='numpy',label='Stem',interactive=False)
257
  rend_out=gr.Audio(type='numpy',label='🔊 Reconstruction',interactive=False)
@@ -259,59 +222,46 @@ def build_app():
259
  with gr.Row():
260
  arc=gr.File(label="📦 ZIP",interactive=False)
261
  mid=gr.File(label="🎹 MIDI",interactive=False)
262
- smp=gr.File(label="WAV samples",file_count="multiple",interactive=False)
263
- met=gr.Dataframe(label="Samples")
264
- stx=gr.Textbox(visible=False)
265
 
266
  dm.change(fn=lambda m:gr.update(choices=DEMUCS_STEMS.get(m,["drums","bass","other","vocals"])+["all"]),
267
  inputs=[dm],outputs=[st])
268
-
269
- tune_btn.click(run_auto_tune,
270
- [audio_in, st, dm, dsh, dov, om,
271
- od, ed, mg, tmin, tmax, # current values
272
- lock_od, lock_ed, lock_mg, lock_tgt], # lock flags
273
- [od, ed, mg, tmin, tmax, tune_summary, tune_log])
274
-
275
  extract_btn.click(run_extraction,
276
- [audio_in,st,dm,dsh,dov,om,od,ed,pp,mnd,mxd,mg,nt,nms,lnk,tmin,tmax,syn],
277
  [stem_out,summary_md,rend_out,smp,mid,arc,stx,met])
278
 
279
  with gr.Tab("📊 Evaluate"):
280
- gr.Markdown("Synthetic evaluation.")
281
  with gr.Row():
282
  ep=gr.Dropdown(['rock','funk','halftime'],value='rock',label='Pattern')
283
  eb=gr.Slider(80,200,value=120,step=2,label='BPM')
284
  ebs=gr.Slider(2,8,value=4,step=1,label='Bars')
285
  with gr.Row():
286
  en=gr.Slider(0.3,0.99,value=0.80,step=0.01,label='NCC')
287
- etm=gr.Number(value=0,label='Min',precision=0)
288
- etx=gr.Number(value=0,label='Max',precision=0)
289
  evb=gr.Button("🧪 Evaluate",variant="primary",size="lg")
290
  with gr.Row():
291
  evm=gr.Audio(type='numpy',label='Original',interactive=False)
292
  evr=gr.Audio(type='numpy',label='Reconstruction',interactive=False)
293
- evs=gr.Dataframe(label="Summary"); evm2=gr.Dataframe(label="Matches")
294
  es1=gr.Textbox(visible=False); es2=gr.Textbox(visible=False)
295
  evb.click(run_eval,[ep,eb,ebs,en,etm,etx],[evm,evr,evs,evm2,es1,es2])
296
 
297
  with gr.Tab("🔄 Optimize"):
298
- gr.Markdown("### Synthetic optimization")
299
  with gr.Row():
300
  on=gr.Slider(2,30,value=5,step=1,label='Iters')
301
- ocn=gr.Textbox(value="opt",label='Name')
302
- oa=gr.Textbox(value="",label='Author')
303
  osv=gr.Checkbox(value=True,label='Save')
304
  ob=gr.Button("🚀 Run",variant="primary",size="lg")
305
  ol=gr.Textbox(label="Log",lines=20,max_lines=40)
306
- oh=gr.Dataframe(label="History"); op=gr.Plot()
307
- oc=gr.Code(label="Config",language="json")
308
  ob.click(run_optimize,[on,ocn,oa,osv],[ol,oh,op,oc])
309
 
310
  with gr.Tab("🏆 Leaderboard"):
311
  lbb=gr.Button("🔄 Refresh"); lt=gr.Dataframe(); ls=gr.Textbox(visible=False)
312
  lbb.click(refresh_lb,[],[lt,ls])
313
-
314
  return app
315
 
316
- if __name__ == "__main__":
317
- build_app().launch(server_name="0.0.0.0", server_port=7860)
 
1
  """
2
+ Gradio UI — Sample Extractor v9.
3
+ SuperFlux onsets, transient NCC, mel pre-filter, MIDI quantization, param locking.
4
  """
5
 
6
  import gradio as gr
 
28
  if pk > 0: a = a / pk * 0.95
29
  return (sr, a)
30
 
 
 
 
31
  def run_auto_tune(audio_in, stem_choice, demucs_model, demucs_shifts, demucs_overlap,
32
+ onset_mode, cur_delta, cur_energy, cur_gap, cur_tmin, cur_tmax,
33
+ lock_delta, lock_energy, lock_gap, lock_targets, progress=gr.Progress()):
34
+ if audio_in is None: return [gr.update()]*5 + ["Upload audio first", ""]
 
 
 
 
 
 
 
35
  locks = {}
36
  if lock_delta: locks['onset_delta'] = float(cur_delta)
37
  if lock_energy: locks['energy_threshold_db'] = float(cur_energy)
38
  if lock_gap: locks['min_gap'] = float(cur_gap)
39
+ if lock_targets: locks['target_min']=int(cur_tmin); locks['target_max']=int(cur_tmax)
40
+ progress(0.0); sr_in,data=audio_in; data=data.astype(np.float32)
41
+ if data.ndim>1: data=data.mean(axis=1)
42
+ pk=np.abs(data).max()
43
+ if pk>0: data/=pk
44
+ with tempfile.NamedTemporaryFile(suffix='.wav',delete=False) as f:
45
+ sf.write(f.name,data,sr_in); tmp=f.name
 
 
 
 
 
 
 
46
  try:
47
+ progress(0.05,desc=f"Stem..."); sa,ssr=extract_stem(tmp,stem=stem_choice,device="cpu",
48
+ model_name=demucs_model,shifts=int(demucs_shifts),overlap=float(demucs_overlap))
49
+ ld=', '.join(f'{k}={v}' for k,v in locks.items()) if locks else 'none'
50
+ progress(0.15,desc=f"Tuning (🔒 {ld})...")
51
+ bp,bs,log=auto_tune(sa,ssr,mode=onset_mode,locks=locks)
52
+ progress(1.0)
53
+ lt='\n'.join(log[-30:])
54
+ li=f"🔒 Locked: {ld}" if locks else "All params free"
55
+ sm=f"**Score: {bs:.1f}/100** · {li}\n\nClick **Extract** to use these settings."
 
 
 
 
 
 
 
 
 
56
  return [
57
+ gr.update(value=bp['onset_delta']) if not lock_delta else gr.update(),
58
+ gr.update(value=bp['energy_threshold_db']) if not lock_energy else gr.update(),
59
+ gr.update(value=bp['min_gap']) if not lock_gap else gr.update(),
60
+ gr.update(value=bp.get('target_min',5)) if not lock_targets else gr.update(),
61
+ gr.update(value=bp.get('target_max',20)) if not lock_targets else gr.update(),
62
+ sm, lt]
63
+ finally: os.unlink(tmp)
 
 
 
 
 
 
64
 
65
  def run_extraction(audio_in, stem_choice, demucs_model, demucs_shifts, demucs_overlap,
66
  onset_mode, onset_delta, energy_db, pre_pad, min_dur, max_dur, min_gap,
67
+ ncc_threshold, attack_ms, linkage, target_min, target_max,
68
+ do_synthesize, quantize_midi, subdivision, progress=gr.Progress()):
69
  if audio_in is None: return [None]*8
70
+ progress(0.0); sr_in,data=audio_in; data=data.astype(np.float32)
71
+ if data.ndim>1: data=data.mean(axis=1)
72
+ pk=np.abs(data).max()
73
+ if pk>0: data/=pk
74
+ with tempfile.NamedTemporaryFile(suffix='.wav',delete=False) as f:
75
+ sf.write(f.name,data,sr_in); tmp=f.name
 
76
  try:
77
+ progress(0.05,desc=f"Stem ({demucs_model})...")
78
+ sa,ssr=extract_stem(tmp,stem=stem_choice,device="cpu",
79
+ model_name=demucs_model,shifts=int(demucs_shifts),overlap=float(demucs_overlap))
80
+ progress(0.15,desc="BPM..."); bpm=detect_bpm(sa,ssr)
81
+ progress(0.25,desc="Onsets (SuperFlux)...")
82
+ hits=detect_onsets(sa,ssr,mode=onset_mode,onset_delta=float(onset_delta),
83
+ energy_threshold_db=float(energy_db),pre_pad=float(pre_pad),
84
+ min_dur=float(min_dur),max_dur=float(max_dur),min_gap=float(min_gap))
85
  if not hits:
86
+ return (audio_tuple(sa,ssr),f"**BPM: {bpm}** — No hits.",None,None,None,None,"",pd.DataFrame())
87
+ progress(0.35,desc="Classify..."); hits=classify_hits(hits)
88
+ progress(0.45,desc="Cluster (transient NCC)...")
89
+ cl=cluster_hits(hits,audio=sa,sr=ssr,ncc_threshold=float(ncc_threshold),
90
+ attack_ms=float(attack_ms),target_min=int(target_min),target_max=int(target_max),linkage=str(linkage))
91
+ progress(0.65,desc="Select..."); select_best(cl)
92
  if do_synthesize:
93
+ progress(0.7,desc="Synth...")
94
  for c in cl:
95
  if c.count>=2: c.synthesized=synthesize_from_cluster(c)
96
+ progress(0.75,desc="MIDI..."); mp=tempfile.mktemp(suffix='.mid')
97
+ export_midi(cl,mp,bpm=bpm,quantize=bool(quantize_midi),subdivision=int(subdivision))
98
+ progress(0.8,desc="Render..."); rend=render_midi_with_samples(cl,sr=ssr)
99
+ progress(0.85,desc="Package...")
100
  sd=tempfile.mkdtemp(); sp=[]
101
  for c in sorted(cl,key=lambda x:x.count,reverse=True):
102
  p=os.path.join(sd,f"{c.label}.wav"); c.best_hit.save(p); sp.append(p)
 
110
  'Dur':f"{b.duration*1000:.0f}ms",
111
  'First':f"{sorted(h.onset_time for h in c.hits)[0]:.2f}s"})
112
  sm=f"**BPM: {bpm}** · **{len(cl)} samples** from {len(hits)} hits\n\n"
113
+ sm+=f"`{demucs_model}` · δ=`{onset_delta}` · E=`{energy_db}dB` · attack=`{attack_ms}ms`"
114
  if int(target_min)>0 and int(target_max)>0: sm+=f" · clusters `{int(target_min)}–{int(target_max)}`"
115
+ if quantize_midi: sm+=f" · MIDI quantized to 1/{int(subdivision)}"
116
  sm+="\n\n| Sample | Hits | MIDI |\n|---|---|---|\n"
117
  for c in sorted(cl,key=lambda x:x.count,reverse=True): sm+=f"| {c.label} | {c.count} | {c.midi_note} |\n"
118
  progress(1.0)
119
  return (audio_tuple(sa,ssr),sm,audio_tuple(rend,ssr),sp,mp,zp,"",pd.DataFrame(rows))
120
  finally: os.unlink(tmp)
121
 
122
+ def run_eval(pattern,bpm,bars,ncc_threshold,target_min,target_max,progress=gr.Progress()):
 
 
 
123
  progress(0.0); song=generate_test_song(pattern_name=pattern,bars=int(bars),bpm=float(bpm),variation='medium',seed=42)
124
  dbpm=detect_bpm(song.drums_only,song.sr); progress(0.2)
125
  hits=detect_onsets(song.drums_only,song.sr)
126
  if not hits: return None,None,None,None,"",""
127
  hits=classify_hits(hits)
128
+ cl=cluster_hits(hits,audio=song.drums_only,sr=song.sr,ncc_threshold=float(ncc_threshold),
129
+ target_min=int(target_min),target_max=int(target_max))
130
  select_best(cl)
131
  for c in cl:
132
  if c.count>=2: c.synthesized=synthesize_from_cluster(c)
 
138
  {'Metric':'Clusters','Value':str(len(cl)),'Target':str(len(gt))},
139
  {'Metric':'Score','Value':f"{r.overall_score:.1f}/100",'Target':'> 70'}]
140
  if r.unmatched_gt: s.append({'Metric':'⚠','Value':', '.join(r.unmatched_gt),'Target':'None'})
141
+ m=[{'Cluster':x.cluster_label,'GT':x.gt_name,'Score':f"{x.sample_score:.1f}"} for x in r.matches]
142
  progress(1.0)
143
  return (audio_tuple(song.mix,song.sr),audio_tuple(rend,song.sr),pd.DataFrame(s),pd.DataFrame(m) if m else None,"","")
144
 
145
+ def run_optimize(n,name,author,save,progress=gr.Progress()):
146
  logs=[]; progress(0.0)
147
+ state=run_optimization(n_iterations=int(n),config_name=name or "opt",
148
+ author=author or "anon",save_to_hub=bool(save),log_fn=lambda m:logs.append(m))
149
  progress(1.0)
150
  h=[{'Iter':r.iteration,'Score':f"{r.avg_score:.1f}"} for r in state.history]
151
  if state.history:
 
155
  return '\n'.join(logs),pd.DataFrame(h),fig,json.dumps(state.best_config,indent=2)
156
 
157
  def refresh_lb():
158
+ try: lb=get_leaderboard(); return pd.DataFrame(lb) if lb else pd.DataFrame(),""
 
159
  except Exception as e: return pd.DataFrame(),str(e)
160
 
 
 
 
161
  def build_app():
162
  with gr.Blocks(title="🎵 Sample Extractor",theme=gr.themes.Soft(),
163
+ css=".gradio-container{max-width:1300px!important}") as app:
164
+ gr.Markdown("# 🎵 Sample Extractor v9\n"
165
+ "**SuperFlux** onset detection · **Transient NCC** clustering (25ms attack matching) · "
166
+ "**Mel pre-filter** · **MIDI quantization** · **Auto-Tune** with 🔒 locks")
167
 
168
  with gr.Tabs():
169
  with gr.Tab("🎵 Extract"):
170
+ audio_in=gr.Audio(sources=['upload'],type='numpy',label='Upload Audio')
171
 
172
+ with gr.Accordion("🔧 Stem Separation",open=False):
173
  with gr.Row():
174
  dm=gr.Dropdown(DEMUCS_MODELS,value="htdemucs_ft",label="Model")
175
  st=gr.Dropdown(['drums','bass','other','vocals','all'],value='drums',label='Stem')
176
  dsh=gr.Slider(0,5,value=1,step=1,label='Shifts')
177
  dov=gr.Slider(0.0,0.5,value=0.25,step=0.05,label='Overlap')
178
 
179
+ with gr.Accordion("🎯 Onset Detection (SuperFlux)",open=False):
180
+ with gr.Row(): om=gr.Dropdown(['auto','percussive','harmonic','broadband'],value='auto',label='Mode')
181
  with gr.Row():
 
 
182
  od=gr.Slider(0.01,0.5,value=0.12,step=0.01,label='Delta')
183
  lock_od=gr.Checkbox(value=False,label='🔒',scale=0)
184
+ with gr.Row():
185
  ed=gr.Slider(-70,-10,value=-35,step=1,label='Energy (dB)')
186
  lock_ed=gr.Checkbox(value=False,label='🔒',scale=0)
187
+ with gr.Row():
188
  mg=gr.Slider(0.005,0.2,value=0.03,step=0.005,label='Min gap (s)')
189
  lock_mg=gr.Checkbox(value=False,label='🔒',scale=0)
190
  with gr.Row():
191
+ pp=gr.Slider(0.0,0.05,value=0.003,step=0.001,label='Pre-pad (s)')
192
  mnd=gr.Slider(0.005,0.2,value=0.02,step=0.005,label='Min dur (s)')
193
  mxd=gr.Slider(0.1,5.0,value=1.5,step=0.1,label='Max dur (s)')
194
 
195
+ with gr.Accordion("🔗 Clustering (Transient NCC + Mel pre-filter)",open=True):
196
+ with gr.Row():
197
+ tmin=gr.Number(value=5,label='Target min',precision=0)
198
+ tmax=gr.Number(value=20,label='Target max',precision=0)
199
  lock_tgt=gr.Checkbox(value=True,label='🔒 Lock range',scale=0)
200
+ gr.Markdown("*🔒 = auto-tune respects this value*")
201
  with gr.Row():
202
  nt=gr.Slider(0.3,0.99,value=0.80,step=0.01,label='NCC threshold')
203
+ atk=gr.Slider(10,100,value=25,step=5,label='Attack window (ms)')
204
  lnk=gr.Dropdown(['average','complete','single'],value='average',label='Linkage')
205
 
206
+ with gr.Accordion("🎹 MIDI & Post-processing",open=False):
207
+ with gr.Row():
208
+ syn=gr.Checkbox(value=True,label='Synthesize samples')
209
+ qmidi=gr.Checkbox(value=True,label='Quantize MIDI')
210
+ subdiv=gr.Dropdown([('8th',8),('16th',16),('32nd',32)],value=16,label='Grid')
211
 
212
  with gr.Row():
213
  tune_btn=gr.Button("🎛️ Auto-Tune",variant="secondary",size="lg")
214
+ extract_btn=gr.Button("🔬 Extract",variant="primary",size="lg")
 
 
 
215
 
216
+ tune_summary=gr.Markdown(""); tune_log=gr.Textbox(label="Log",lines=8,max_lines=15,visible=False)
217
+ summary_md=gr.Markdown("*Upload → Auto-Tune or Extract*")
218
  with gr.Row():
219
  stem_out=gr.Audio(type='numpy',label='Stem',interactive=False)
220
  rend_out=gr.Audio(type='numpy',label='🔊 Reconstruction',interactive=False)
 
222
  with gr.Row():
223
  arc=gr.File(label="📦 ZIP",interactive=False)
224
  mid=gr.File(label="🎹 MIDI",interactive=False)
225
+ smp=gr.File(label="WAVs",file_count="multiple",interactive=False)
226
+ met=gr.Dataframe(label="Samples"); stx=gr.Textbox(visible=False)
 
227
 
228
  dm.change(fn=lambda m:gr.update(choices=DEMUCS_STEMS.get(m,["drums","bass","other","vocals"])+["all"]),
229
  inputs=[dm],outputs=[st])
230
+ tune_btn.click(run_auto_tune,[audio_in,st,dm,dsh,dov,om,od,ed,mg,tmin,tmax,lock_od,lock_ed,lock_mg,lock_tgt],
231
+ [od,ed,mg,tmin,tmax,tune_summary,tune_log])
 
 
 
 
 
232
  extract_btn.click(run_extraction,
233
+ [audio_in,st,dm,dsh,dov,om,od,ed,pp,mnd,mxd,mg,nt,atk,lnk,tmin,tmax,syn,qmidi,subdiv],
234
  [stem_out,summary_md,rend_out,smp,mid,arc,stx,met])
235
 
236
  with gr.Tab("📊 Evaluate"):
 
237
  with gr.Row():
238
  ep=gr.Dropdown(['rock','funk','halftime'],value='rock',label='Pattern')
239
  eb=gr.Slider(80,200,value=120,step=2,label='BPM')
240
  ebs=gr.Slider(2,8,value=4,step=1,label='Bars')
241
  with gr.Row():
242
  en=gr.Slider(0.3,0.99,value=0.80,step=0.01,label='NCC')
243
+ etm=gr.Number(value=0,label='Min',precision=0); etx=gr.Number(value=0,label='Max',precision=0)
 
244
  evb=gr.Button("🧪 Evaluate",variant="primary",size="lg")
245
  with gr.Row():
246
  evm=gr.Audio(type='numpy',label='Original',interactive=False)
247
  evr=gr.Audio(type='numpy',label='Reconstruction',interactive=False)
248
+ evs=gr.Dataframe(); evm2=gr.Dataframe()
249
  es1=gr.Textbox(visible=False); es2=gr.Textbox(visible=False)
250
  evb.click(run_eval,[ep,eb,ebs,en,etm,etx],[evm,evr,evs,evm2,es1,es2])
251
 
252
  with gr.Tab("🔄 Optimize"):
 
253
  with gr.Row():
254
  on=gr.Slider(2,30,value=5,step=1,label='Iters')
255
+ ocn=gr.Textbox(value="opt",label='Name'); oa=gr.Textbox(value="",label='Author')
 
256
  osv=gr.Checkbox(value=True,label='Save')
257
  ob=gr.Button("🚀 Run",variant="primary",size="lg")
258
  ol=gr.Textbox(label="Log",lines=20,max_lines=40)
259
+ oh=gr.Dataframe(); op=gr.Plot(); oc=gr.Code(label="Config",language="json")
 
260
  ob.click(run_optimize,[on,ocn,oa,osv],[ol,oh,op,oc])
261
 
262
  with gr.Tab("🏆 Leaderboard"):
263
  lbb=gr.Button("🔄 Refresh"); lt=gr.Dataframe(); ls=gr.Textbox(visible=False)
264
  lbb.click(refresh_lb,[],[lt,ls])
 
265
  return app
266
 
267
+ if __name__=="__main__": build_app().launch(server_name="0.0.0.0",server_port=7860)