fffiloni commited on
Commit
8ae40b3
·
verified ·
1 Parent(s): aa2b5b1

Upload 8 files

Browse files
Files changed (2) hide show
  1. app.py +473 -5
  2. requirements.txt +1 -0
app.py CHANGED
@@ -57,6 +57,23 @@ GPU_BASE_SECONDS = 35
57
  GPU_SECONDS_PER_CHUNK = 9
58
  GPU_DURATION_CAP = int(os.environ.get("MAX_GPU_SECONDS", "600"))
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  COLORS = (
61
  "#35c8ff",
62
  "#f6a623",
@@ -144,6 +161,8 @@ INSTRUMENT_CATEGORY_MEMBERS: tuple[tuple[str, tuple[str, ...]], ...] = (
144
 
145
  MODEL: Any | None = None
146
  MODEL_ERROR = ""
 
 
147
  NoteStartEvent: Any = None
148
  NoteEndEvent: Any = None
149
  ProgressEvent: Any = None
@@ -217,6 +236,42 @@ if os.environ.get("MUSCRIPTOR_SKIP_MODEL_LOAD") != "1":
217
  print(f"[MuScriptor Studio] Model ready in {time.perf_counter() - started:.2f}s.", flush=True)
218
 
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  def _audio_path(value: Any) -> str | None:
221
  if value is None:
222
  return None
@@ -240,6 +295,325 @@ def _audio_duration(path: str | None) -> float:
240
  return 0.0
241
 
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  def _estimate_gpu_duration(
244
  audio: Any,
245
  instruments: list[str] | None = None,
@@ -931,6 +1305,7 @@ INSTRUMENT_CATEGORY_CHOICES = _instrument_category_choices(INSTRUMENT_NAMES)
931
  KYUTAI_URL = "https://kyutai.org/"
932
  MIRELO_URL = "https://www.mirelo.ai/"
933
  MODEL_PAGE_URL = f"https://huggingface.co/{MODEL_ID}"
 
934
 
935
  HEADER = f"""
936
  <header class="workbench-nav">
@@ -969,7 +1344,7 @@ INTRO = f"""
969
  WORKBENCH_HEADING = f"""
970
  <header class="panel-heading">
971
  <div><span>New session</span><h2>Prepare the transcription</h2></div>
972
- <p>WAV, MP3, FLAC or OGG · up to {MAX_AUDIO_SECONDS / 60:.0f} minutes</p>
973
  </header>
974
  """
975
 
@@ -981,7 +1356,14 @@ RESULT_HEADING = """
981
  """
982
 
983
  SOURCE_HEADING = """
984
- <div class="control-heading"><span class="control-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 14v-4m4 8V6m4 14V4m4 13V7m4 7v-4"/></svg></span><div><strong>Source audio</strong><small>Upload once; the same Gradio player remains available for A/B comparison.</small></div></div>
 
 
 
 
 
 
 
985
  """
986
 
987
  DECODE_HEADING = """
@@ -1016,11 +1398,40 @@ with gr.Blocks(title="MuScriptor Studio") as demo:
1016
  with gr.Column(elem_id="source-panel"):
1017
  gr.HTML(SOURCE_HEADING)
1018
  audio_input = gr.Audio(
1019
- label="Drop audio here",
1020
- sources=["upload"],
1021
  type="filepath",
1022
  elem_id="audio-input",
1023
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1024
 
1025
  with gr.Row(elem_id="settings-grid", equal_height=False):
1026
  with gr.Column(scale=2, min_width=420, elem_id="decode-panel"):
@@ -1152,6 +1563,63 @@ with gr.Blocks(title="MuScriptor Studio") as demo:
1152
  api_visibility="private",
1153
  )
1154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1155
  for category_input in instrument_category_inputs:
1156
  category_input.change(
1157
  _merge_instrument_groups,
@@ -1198,7 +1666,7 @@ with gr.Blocks(title="MuScriptor Studio") as demo:
1198
  ],
1199
  api_name="transcribe",
1200
  concurrency_limit=1,
1201
- concurrency_id="muscriptor-large",
1202
  show_progress="full",
1203
  )
1204
  transcription.then(
 
57
  GPU_SECONDS_PER_CHUNK = 9
58
  GPU_DURATION_CAP = int(os.environ.get("MAX_GPU_SECONDS", "600"))
59
 
60
+ # Demucs is an optional source-preparation pass. The standard four-source
61
+ # htdemucs model is intentionally used instead of the experimental six-source
62
+ # model whose piano stem is documented as unreliable by the project.
63
+ DEMUCS_MODEL_NAME = os.environ.get("DEMUCS_MODEL_NAME", "htdemucs")
64
+ DEMUCS_SEGMENT_SECONDS = 7
65
+ DEMUCS_SHIFTS = 1
66
+ DEMUCS_OVERLAP = 0.25
67
+ DEMUCS_GPU_DURATION_CAP = int(os.environ.get("DEMUCS_MAX_GPU_SECONDS", "600"))
68
+ DEMUCS_STEM_ORDER = ("vocals", "drums", "bass", "other")
69
+ DEMUCS_STEM_LABELS = {
70
+ "full": "Full mix",
71
+ "vocals": "Vocals",
72
+ "drums": "Drums",
73
+ "bass": "Bass",
74
+ "other": "Other",
75
+ }
76
+
77
  COLORS = (
78
  "#35c8ff",
79
  "#f6a623",
 
161
 
162
  MODEL: Any | None = None
163
  MODEL_ERROR = ""
164
+ DEMUCS_SEPARATOR: Any | None = None
165
+ DEMUCS_ERROR = ""
166
  NoteStartEvent: Any = None
167
  NoteEndEvent: Any = None
168
  ProgressEvent: Any = None
 
236
  print(f"[MuScriptor Studio] Model ready in {time.perf_counter() - started:.2f}s.", flush=True)
237
 
238
 
239
+ def _load_demucs_separator() -> Any | None:
240
+ """Load the public htdemucs model once and register it with ZeroGPU."""
241
+
242
+ global DEMUCS_ERROR
243
+ try:
244
+ from demucs.api import Separator
245
+
246
+ separator = Separator(
247
+ model=DEMUCS_MODEL_NAME,
248
+ device="cuda",
249
+ shifts=DEMUCS_SHIFTS,
250
+ overlap=DEMUCS_OVERLAP,
251
+ split=True,
252
+ segment=DEMUCS_SEGMENT_SECONDS,
253
+ jobs=0,
254
+ progress=False,
255
+ )
256
+ # ZeroGPU emulates this placement at module scope and restores the
257
+ # packed model inside the decorated separation call.
258
+ separator.model.to("cuda")
259
+ return separator
260
+ except Exception as exc:
261
+ DEMUCS_ERROR = f"Demucs could not be loaded: {type(exc).__name__}: {exc}"
262
+ return None
263
+
264
+
265
+ if os.environ.get("MUSCRIPTOR_SKIP_DEMUCS_LOAD") != "1" and os.environ.get("MUSCRIPTOR_SKIP_MODEL_LOAD") != "1":
266
+ started = time.perf_counter()
267
+ print(f"[MuScriptor Studio] Loading Demucs {DEMUCS_MODEL_NAME}…", flush=True)
268
+ DEMUCS_SEPARATOR = _load_demucs_separator()
269
+ if DEMUCS_SEPARATOR is None:
270
+ print(f"[MuScriptor Studio] Demucs unavailable: {DEMUCS_ERROR}", file=sys.stderr, flush=True)
271
+ else:
272
+ print(f"[MuScriptor Studio] Demucs ready in {time.perf_counter() - started:.2f}s.", flush=True)
273
+
274
+
275
  def _audio_path(value: Any) -> str | None:
276
  if value is None:
277
  return None
 
295
  return 0.0
296
 
297
 
298
+ def _stem_source_path(session: dict[str, Any] | None, selection: str = "full") -> str | None:
299
+ session = session or {}
300
+ key = str(selection or "full")
301
+ if key == "full":
302
+ return _audio_path(session.get("original_path"))
303
+ return _audio_path((session.get("stems") or {}).get(key))
304
+
305
+
306
+ def _stem_choices(session: dict[str, Any] | None = None) -> list[tuple[str, str]]:
307
+ session = session or {}
308
+ stems = session.get("stems") or {}
309
+ choices = [(DEMUCS_STEM_LABELS["full"], "full")]
310
+ choices.extend(
311
+ (DEMUCS_STEM_LABELS[name], name)
312
+ for name in DEMUCS_STEM_ORDER
313
+ if stems.get(name)
314
+ )
315
+ return choices
316
+
317
+
318
+ def _stem_files(session: dict[str, Any] | None = None) -> list[str] | None:
319
+ session = session or {}
320
+ values = [
321
+ str(path)
322
+ for name in DEMUCS_STEM_ORDER
323
+ if (path := (session.get("stems") or {}).get(name))
324
+ ]
325
+ return values or None
326
+
327
+
328
+ def _stem_selector_update(
329
+ session: dict[str, Any] | None = None,
330
+ *,
331
+ interactive: bool | None = None,
332
+ value: str | None = None,
333
+ ) -> dict[str, Any]:
334
+ session = session or {}
335
+ choices = _stem_choices(session)
336
+ selected = value or str(session.get("selected") or "full")
337
+ if selected not in {choice_value for _, choice_value in choices}:
338
+ selected = "full"
339
+ enabled = len(choices) > 1 if interactive is None else bool(interactive)
340
+ return gr.update(choices=choices, value=selected, interactive=enabled, visible=True)
341
+
342
+
343
+ def _stem_status_html(
344
+ state: str = "waiting",
345
+ *,
346
+ audio_name: str = "",
347
+ selected: str = "full",
348
+ message: str = "",
349
+ ) -> str:
350
+ safe_name = html.escape(audio_name or "the source audio")
351
+ label = html.escape(DEMUCS_STEM_LABELS.get(selected, _display_name(selected)))
352
+ copy: dict[str, tuple[str, str]] = {
353
+ "waiting": (
354
+ "Optional stem separation",
355
+ "Upload or record audio, then isolate vocals, drums, bass and accompaniment.",
356
+ ),
357
+ "ready": (
358
+ "Ready for source separation",
359
+ f"{safe_name} stays available as the full mix. Demucs runs only when requested.",
360
+ ),
361
+ "working": (
362
+ "Separating four audio stems…",
363
+ "ZeroGPU is isolating vocals, drums, bass and the remaining accompaniment.",
364
+ ),
365
+ "complete": (
366
+ "Four stems ready",
367
+ "Full mix is still loaded. Choose a source below before starting transcription.",
368
+ ),
369
+ "selected": (
370
+ f"{label} loaded in the audio player",
371
+ "This is now the source MuScriptor will transcribe.",
372
+ ),
373
+ "unavailable": (
374
+ "Demucs unavailable",
375
+ "The audio can still be transcribed as a full mix on this runtime.",
376
+ ),
377
+ "error": (
378
+ "Stem separation stopped",
379
+ html.escape(message or "Demucs could not process this audio source."),
380
+ ),
381
+ }
382
+ title, detail = copy.get(state, copy["waiting"])
383
+ return (
384
+ f'<div class="stem-status" data-state="{html.escape(state)}"><i aria-hidden="true"></i><div>'
385
+ f"<strong>{title}</strong><span>{detail}</span>"
386
+ f'</div><b>{html.escape(DEMUCS_MODEL_NAME)} · 4 stems</b></div>'
387
+ )
388
+
389
+
390
+ def register_source_audio(audio: Any) -> tuple[dict[str, Any], dict[str, Any], str, dict[str, Any], None]:
391
+ """Reset stale stems only when the user uploads or records a new source."""
392
+
393
+ path = _audio_path(audio)
394
+ if not path:
395
+ return (
396
+ {},
397
+ _stem_selector_update({}, interactive=False),
398
+ _stem_status_html(),
399
+ gr.update(interactive=False, value="Separate into stems"),
400
+ None,
401
+ )
402
+ session = {
403
+ "original_path": path,
404
+ "original_name": Path(path).name,
405
+ "selected": "full",
406
+ "stems": {},
407
+ "model": DEMUCS_MODEL_NAME,
408
+ }
409
+ available = DEMUCS_SEPARATOR is not None
410
+ status = _stem_status_html(
411
+ "ready" if available else "unavailable",
412
+ audio_name=Path(path).name,
413
+ )
414
+ return (
415
+ session,
416
+ _stem_selector_update(session, interactive=False),
417
+ status,
418
+ gr.update(
419
+ interactive=available,
420
+ value="Separate into stems" if available else "Demucs unavailable",
421
+ ),
422
+ None,
423
+ )
424
+
425
+
426
+ def clear_source_audio() -> tuple[dict[str, Any], dict[str, Any], str, dict[str, Any], None]:
427
+ return register_source_audio(None)
428
+
429
+
430
+ def begin_stem_separation(
431
+ audio: Any,
432
+ session: dict[str, Any] | None,
433
+ ) -> tuple[str, dict[str, Any], dict[str, Any]]:
434
+ path = _stem_source_path(session, "full") or _audio_path(audio)
435
+ if not path:
436
+ return (
437
+ _stem_status_html("error", message="Add or record audio before running Demucs."),
438
+ gr.update(interactive=False, value="Separate into stems"),
439
+ _stem_selector_update(session, interactive=False),
440
+ )
441
+ return (
442
+ _stem_status_html("working", audio_name=Path(path).name),
443
+ gr.update(interactive=False, value="Separating stems…"),
444
+ _stem_selector_update(session, interactive=False),
445
+ )
446
+
447
+
448
+ def _estimate_demucs_gpu_duration(
449
+ audio: Any,
450
+ session: dict[str, Any] | None = None,
451
+ *args: Any,
452
+ **kwargs: Any,
453
+ ) -> int:
454
+ path = _stem_source_path(session, "full") or _audio_path(audio)
455
+ duration = _audio_duration(path) or 15.0
456
+ return min(DEMUCS_GPU_DURATION_CAP, max(60, math.ceil(35 + duration * 0.85)))
457
+
458
+
459
+ @spaces.GPU(duration=_estimate_demucs_gpu_duration)
460
+ def separate_audio_stems(
461
+ audio: Any,
462
+ session: dict[str, Any] | None,
463
+ progress: gr.Progress = gr.Progress(),
464
+ ) -> tuple[dict[str, Any], dict[str, Any], str, dict[str, Any], list[str] | None]:
465
+ """Run htdemucs once and retain the full mix plus four selectable WAV stems."""
466
+
467
+ previous = dict(session or {})
468
+ path = _stem_source_path(previous, "full") or _audio_path(audio)
469
+ audio_name = str(previous.get("original_name") or (Path(path).name if path else ""))
470
+ retry_update = gr.update(interactive=DEMUCS_SEPARATOR is not None, value="Retry separation")
471
+ if not path:
472
+ return (
473
+ previous,
474
+ _stem_selector_update(previous),
475
+ _stem_status_html("error", message="Add or record audio before running Demucs."),
476
+ gr.update(interactive=False, value="Separate into stems"),
477
+ _stem_files(previous),
478
+ )
479
+ duration = _audio_duration(path)
480
+ if duration <= 0:
481
+ return (
482
+ previous,
483
+ _stem_selector_update(previous),
484
+ _stem_status_html("error", audio_name=audio_name, message="The audio duration could not be read."),
485
+ retry_update,
486
+ _stem_files(previous),
487
+ )
488
+ if duration > MAX_AUDIO_SECONDS:
489
+ return (
490
+ previous,
491
+ _stem_selector_update(previous),
492
+ _stem_status_html(
493
+ "error",
494
+ audio_name=audio_name,
495
+ message=f"This Space accepts audio up to {MAX_AUDIO_SECONDS / 60:.1f} minutes.",
496
+ ),
497
+ retry_update,
498
+ _stem_files(previous),
499
+ )
500
+ if DEMUCS_SEPARATOR is None:
501
+ return (
502
+ previous,
503
+ _stem_selector_update(previous),
504
+ _stem_status_html("unavailable", audio_name=audio_name),
505
+ gr.update(interactive=False, value="Demucs unavailable"),
506
+ _stem_files(previous),
507
+ )
508
+
509
+ started = time.perf_counter()
510
+ separator = DEMUCS_SEPARATOR
511
+ segment_frames = int(separator.samplerate * DEMUCS_SEGMENT_SECONDS)
512
+
513
+ def on_progress(info: dict[str, Any]) -> None:
514
+ if info.get("state") != "end":
515
+ return
516
+ audio_length = max(1, int(info.get("audio_length") or 1))
517
+ segment_end = min(audio_length, int(info.get("segment_offset") or 0) + segment_frames)
518
+ model_count = max(1, int(info.get("models") or 1))
519
+ model_index = max(0, int(info.get("model_idx_in_bag") or 0))
520
+ shift_index = max(0, int(info.get("shift_idx") or 0))
521
+ shift_count = max(1, DEMUCS_SHIFTS)
522
+ unit = (model_index * shift_count + shift_index + segment_end / audio_length) / (
523
+ model_count * shift_count
524
+ )
525
+ progress(min(0.92, 0.04 + 0.86 * unit), desc="Separating audio with Demucs")
526
+
527
+ try:
528
+ from demucs.api import save_audio
529
+
530
+ progress(0.02, desc="Preparing the full mix")
531
+ separator.update_parameter(callback=on_progress, callback_arg={"audio_name": audio_name})
532
+ _, separated = separator.separate_audio_file(Path(path))
533
+ missing = [name for name in DEMUCS_STEM_ORDER if name not in separated]
534
+ if missing:
535
+ raise RuntimeError(f"Demucs did not return: {', '.join(missing)}")
536
+
537
+ progress(0.94, desc="Writing four WAV stems")
538
+ export_stem = safe_filename_stem(Path(audio_name).stem, "demucs-source")
539
+ output_dir = Path(tempfile.mkdtemp(prefix="muscriptor-demucs-"))
540
+ stem_paths: dict[str, str] = {}
541
+ for name in DEMUCS_STEM_ORDER:
542
+ stem_path = output_dir / f"{export_stem}-{name}-stem.wav"
543
+ save_audio(
544
+ separated[name].detach().cpu(),
545
+ stem_path,
546
+ samplerate=separator.samplerate,
547
+ clip="rescale",
548
+ bits_per_sample=16,
549
+ as_float=False,
550
+ )
551
+ stem_paths[name] = str(stem_path)
552
+
553
+ result = {
554
+ "original_path": path,
555
+ "original_name": audio_name,
556
+ "selected": "full",
557
+ "stems": stem_paths,
558
+ "model": DEMUCS_MODEL_NAME,
559
+ "duration": duration,
560
+ "elapsed": time.perf_counter() - started,
561
+ }
562
+ progress(1, desc="Four stems ready")
563
+ print(
564
+ f"[MuScriptor Studio] Demucs separated {audio_name} "
565
+ f"({duration:.1f}s) in {result['elapsed']:.1f}s.",
566
+ flush=True,
567
+ )
568
+ return (
569
+ result,
570
+ _stem_selector_update(result, interactive=True, value="full"),
571
+ _stem_status_html("complete", audio_name=audio_name),
572
+ gr.update(interactive=True, value="Separate original again"),
573
+ _stem_files(result),
574
+ )
575
+ except Exception as exc:
576
+ message = f"{type(exc).__name__}: {exc}"
577
+ print(f"[MuScriptor Studio] Demucs stopped: {message}", file=sys.stderr, flush=True)
578
+ return (
579
+ previous,
580
+ _stem_selector_update(previous),
581
+ _stem_status_html("error", audio_name=audio_name, message=message),
582
+ retry_update,
583
+ _stem_files(previous),
584
+ )
585
+ finally:
586
+ try:
587
+ separator.update_parameter(callback=None, callback_arg=None)
588
+ except Exception:
589
+ pass
590
+
591
+
592
+ def select_stem_source(
593
+ selection: str | None,
594
+ session: dict[str, Any] | None,
595
+ ) -> tuple[Any, dict[str, Any], str]:
596
+ updated = dict(session or {})
597
+ key = str(selection or "full")
598
+ path = _stem_source_path(updated, key)
599
+ if not path or not Path(path).exists():
600
+ return (
601
+ gr.update(),
602
+ updated,
603
+ _stem_status_html("error", message=f"The {DEMUCS_STEM_LABELS.get(key, key)} source is unavailable."),
604
+ )
605
+ updated["selected"] = key
606
+ return (
607
+ path,
608
+ updated,
609
+ _stem_status_html(
610
+ "selected",
611
+ audio_name=str(updated.get("original_name") or Path(path).name),
612
+ selected=key,
613
+ ),
614
+ )
615
+
616
+
617
  def _estimate_gpu_duration(
618
  audio: Any,
619
  instruments: list[str] | None = None,
 
1305
  KYUTAI_URL = "https://kyutai.org/"
1306
  MIRELO_URL = "https://www.mirelo.ai/"
1307
  MODEL_PAGE_URL = f"https://huggingface.co/{MODEL_ID}"
1308
+ DEMUCS_URL = "https://github.com/adefossez/demucs"
1309
 
1310
  HEADER = f"""
1311
  <header class="workbench-nav">
 
1344
  WORKBENCH_HEADING = f"""
1345
  <header class="panel-heading">
1346
  <div><span>New session</span><h2>Prepare the transcription</h2></div>
1347
+ <p>Upload or microphone · WAV, MP3, FLAC or OGG · up to {MAX_AUDIO_SECONDS / 60:.0f} minutes</p>
1348
  </header>
1349
  """
1350
 
 
1356
  """
1357
 
1358
  SOURCE_HEADING = """
1359
+ <div class="control-heading"><span class="control-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 14v-4m4 8V6m4 14V4m4 13V7m4 7v-4"/></svg></span><div><strong>Source audio</strong><small>Upload or record audio, then keep the full mix or load an isolated stem into this player.</small></div></div>
1360
+ """
1361
+
1362
+ STEM_WORKBENCH_HEADING = f"""
1363
+ <div class="stem-workbench-heading">
1364
+ <div><strong>Prepare the transcription source</strong><small>Optional separation into four broad audio stems before MuScriptor.</small></div>
1365
+ <a href="{DEMUCS_URL}" target="_blank" rel="noreferrer">Demucs v4 <span aria-hidden="true">↗</span></a>
1366
+ </div>
1367
  """
1368
 
1369
  DECODE_HEADING = """
 
1398
  with gr.Column(elem_id="source-panel"):
1399
  gr.HTML(SOURCE_HEADING)
1400
  audio_input = gr.Audio(
1401
+ label="Upload or record audio",
1402
+ sources=["upload", "microphone"],
1403
  type="filepath",
1404
  elem_id="audio-input",
1405
  )
1406
+ stem_session_state = gr.State(value={})
1407
+ with gr.Column(elem_id="stem-workbench"):
1408
+ gr.HTML(STEM_WORKBENCH_HEADING, elem_id="stem-workbench-heading")
1409
+ with gr.Row(elem_id="stem-separation-row"):
1410
+ stem_status_output = gr.HTML(
1411
+ value=_stem_status_html(),
1412
+ elem_id="stem-status-output",
1413
+ )
1414
+ separate_stems_button = gr.Button(
1415
+ "Separate into stems",
1416
+ variant="secondary",
1417
+ interactive=False,
1418
+ elem_id="separate-stems-button",
1419
+ )
1420
+ stem_source_input = gr.Radio(
1421
+ choices=[(DEMUCS_STEM_LABELS["full"], "full")],
1422
+ value="full",
1423
+ label="Source to transcribe",
1424
+ info="Selecting a source loads it into the Gradio player above. Then press Transcribe audio.",
1425
+ interactive=False,
1426
+ elem_id="stem-source-selector",
1427
+ )
1428
+ # Retain every generated stem in Gradio's managed cache;
1429
+ # user-facing selection happens through the source cards.
1430
+ stem_files_output = gr.File(
1431
+ label="Demucs stems",
1432
+ file_count="multiple",
1433
+ visible=False,
1434
+ )
1435
 
1436
  with gr.Row(elem_id="settings-grid", equal_height=False):
1437
  with gr.Column(scale=2, min_width=420, elem_id="decode-panel"):
 
1563
  api_visibility="private",
1564
  )
1565
 
1566
+ audio_input.input(
1567
+ register_source_audio,
1568
+ inputs=[audio_input],
1569
+ outputs=[
1570
+ stem_session_state,
1571
+ stem_source_input,
1572
+ stem_status_output,
1573
+ separate_stems_button,
1574
+ stem_files_output,
1575
+ ],
1576
+ queue=False,
1577
+ api_visibility="private",
1578
+ trigger_mode="always_last",
1579
+ )
1580
+ audio_input.clear(
1581
+ clear_source_audio,
1582
+ inputs=[],
1583
+ outputs=[
1584
+ stem_session_state,
1585
+ stem_source_input,
1586
+ stem_status_output,
1587
+ separate_stems_button,
1588
+ stem_files_output,
1589
+ ],
1590
+ queue=False,
1591
+ api_visibility="private",
1592
+ )
1593
+ stem_preflight = separate_stems_button.click(
1594
+ begin_stem_separation,
1595
+ inputs=[audio_input, stem_session_state],
1596
+ outputs=[stem_status_output, separate_stems_button, stem_source_input],
1597
+ queue=False,
1598
+ show_progress="hidden",
1599
+ )
1600
+ stem_preflight.then(
1601
+ separate_audio_stems,
1602
+ inputs=[audio_input, stem_session_state],
1603
+ outputs=[
1604
+ stem_session_state,
1605
+ stem_source_input,
1606
+ stem_status_output,
1607
+ separate_stems_button,
1608
+ stem_files_output,
1609
+ ],
1610
+ api_name="separate_stems",
1611
+ concurrency_limit=1,
1612
+ concurrency_id="muscriptor-gpu",
1613
+ show_progress="full",
1614
+ )
1615
+ stem_source_input.input(
1616
+ select_stem_source,
1617
+ inputs=[stem_source_input, stem_session_state],
1618
+ outputs=[audio_input, stem_session_state, stem_status_output],
1619
+ queue=False,
1620
+ api_visibility="private",
1621
+ )
1622
+
1623
  for category_input in instrument_category_inputs:
1624
  category_input.change(
1625
  _merge_instrument_groups,
 
1666
  ],
1667
  api_name="transcribe",
1668
  concurrency_limit=1,
1669
+ concurrency_id="muscriptor-gpu",
1670
  show_progress="full",
1671
  )
1672
  transcription.then(
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  gradio==6.20.0
2
  spaces==0.51.0
3
  muscriptor==0.2.1
 
4
  numpy==1.26.4
5
  einops==0.8.2
6
  mido==1.3.3
 
1
  gradio==6.20.0
2
  spaces==0.51.0
3
  muscriptor==0.2.1
4
+ demucs==4.1.0
5
  numpy==1.26.4
6
  einops==0.8.2
7
  mido==1.3.3