CreativeCoder77 commited on
Commit
21b2ec8
·
verified ·
1 Parent(s): ff10a4a

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +625 -0
  2. requirements.txt +10 -0
app.py ADDED
@@ -0,0 +1,625 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import textwrap
8
+ import uuid
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Iterable
12
+
13
+ import gradio as gr
14
+ import numpy as np
15
+ import soundfile as sf
16
+
17
+ try:
18
+ import librosa
19
+ except Exception:
20
+ librosa = None
21
+
22
+ APP_DIR = Path(__file__).resolve().parent
23
+ OUTPUT_DIR = APP_DIR / "outputs"
24
+ OUTPUT_DIR.mkdir(exist_ok=True)
25
+
26
+
27
+ @dataclass
28
+ class GeneratedAsset:
29
+ label: str
30
+ path: Path
31
+
32
+
33
+ def _summarize_runtime_error(message: str) -> str:
34
+ lowered = message.lower()
35
+
36
+ if "save_with_torchcodec" in lowered or "torchcodec is required" in lowered:
37
+ return "A library tried to save audio through TorchCodec. Restart the app and run the separation again after this update."
38
+ if "no module named 'demucs'" in lowered:
39
+ return "Demucs is not installed. Run: pip install demucs"
40
+ if "pyannote.audio is not installed" in lowered:
41
+ return "Speaker separation is turned on, but `pyannote.audio` is not installed."
42
+ if "speaker separation needs a hugging face access token" in lowered:
43
+ return "Speaker separation is turned on, but no Hugging Face token was provided."
44
+ if "noisereduce is not installed" in lowered:
45
+ return "Noise estimation is turned on, but `noisereduce` is not installed."
46
+ if "librosa is not installed correctly" in lowered:
47
+ return "The audio-processing stack is incomplete because `librosa` is missing or broken."
48
+
49
+ lines = [line.strip() for line in message.splitlines() if line.strip()]
50
+ if not lines:
51
+ return "An unknown error occurred."
52
+ return lines[-1]
53
+
54
+
55
+ def _run_command(command: list[str], cwd: Path | None = None) -> None:
56
+ completed = subprocess.run(
57
+ command,
58
+ cwd=str(cwd) if cwd else None,
59
+ capture_output=True,
60
+ text=True,
61
+ check=False,
62
+ )
63
+ if completed.returncode != 0:
64
+ stderr = completed.stderr.strip() or completed.stdout.strip() or "Unknown error"
65
+ raise RuntimeError(f"Command failed: {' '.join(command)}\n{stderr}")
66
+
67
+
68
+ def _copy_uploaded_file(audio_path: str | Path, run_dir: Path) -> Path:
69
+ source = Path(audio_path)
70
+ target = run_dir / source.name
71
+ shutil.copy2(source, target)
72
+ return target
73
+
74
+
75
+ def _safe_stem(name: str) -> str:
76
+ return "".join(ch if ch.isalnum() or ch in {"-", "_"} else "_" for ch in name).strip("_") or "audio"
77
+
78
+
79
+ def _latest_demucs_output(base_dir: Path, audio_stem: str) -> Path:
80
+ matches = sorted(base_dir.rglob(f"{audio_stem}.wav"))
81
+ if not matches:
82
+ raise RuntimeError("Demucs finished without producing any WAV files.")
83
+ return matches[0].parent
84
+
85
+
86
+ def _require_demucs_save_stack() -> None:
87
+ try:
88
+ import demucs # noqa: F401
89
+ import torch # noqa: F401
90
+ except Exception as exc:
91
+ raise RuntimeError("Demucs or PyTorch is not installed in the current Python environment.") from exc
92
+
93
+ # Demucs 4.x uses torchaudio / soundfile for I/O — torchcodec is NOT required.
94
+
95
+
96
+ def _prepare_demucs_audio(
97
+ signal: np.ndarray,
98
+ sample_rate: int,
99
+ target_sample_rate: int,
100
+ target_channels: int,
101
+ ) -> np.ndarray:
102
+ if signal.ndim == 1:
103
+ signal = np.expand_dims(signal, axis=0)
104
+
105
+ if sample_rate != target_sample_rate:
106
+ signal = np.stack(
107
+ [
108
+ librosa.resample(channel, orig_sr=sample_rate, target_sr=target_sample_rate)
109
+ for channel in signal
110
+ ],
111
+ axis=0,
112
+ )
113
+
114
+ if signal.shape[0] == target_channels:
115
+ return signal.astype(np.float32)
116
+ if target_channels == 1:
117
+ return np.mean(signal, axis=0, keepdims=True).astype(np.float32)
118
+ if signal.shape[0] == 1:
119
+ return np.repeat(signal, target_channels, axis=0).astype(np.float32)
120
+ if signal.shape[0] > target_channels:
121
+ return signal[:target_channels].astype(np.float32)
122
+ raise RuntimeError("The input audio channel layout is not supported for the selected Demucs model.")
123
+
124
+
125
+ def _patch_torchaudio_save() -> None:
126
+ try:
127
+ import torchaudio
128
+ except Exception:
129
+ return
130
+
131
+ def _soundfile_save(filepath, src, sample_rate, *args, **kwargs):
132
+ audio = src.detach().cpu().numpy()
133
+ if audio.ndim == 2:
134
+ audio = audio.T
135
+ sf.write(filepath, audio, sample_rate)
136
+
137
+ torchaudio.save = _soundfile_save
138
+
139
+
140
+ def separate_with_demucs(audio_path: Path, run_dir: Path, model_name: str) -> list[GeneratedAsset]:
141
+ _require_demucs_save_stack()
142
+ _require_audio_stack()
143
+ _patch_torchaudio_save()
144
+ import torch
145
+ from demucs.apply import apply_model
146
+ from demucs.pretrained import get_model
147
+
148
+ model = get_model(name=model_name)
149
+ model.cpu()
150
+ model.eval()
151
+
152
+ signal, sample_rate = librosa.load(str(audio_path), sr=None, mono=False)
153
+ prepared_signal = _prepare_demucs_audio(signal, sample_rate, model.samplerate, model.audio_channels)
154
+ wav = torch.tensor(prepared_signal, dtype=torch.float32)
155
+
156
+ ref = wav.mean(0)
157
+ wav = wav - ref.mean()
158
+ ref_std = ref.std()
159
+ if float(ref_std) > 0:
160
+ wav = wav / ref_std
161
+
162
+ sources = apply_model(
163
+ model,
164
+ wav[None],
165
+ device="cpu",
166
+ shifts=1,
167
+ split=True,
168
+ overlap=0.25,
169
+ progress=False,
170
+ )[0]
171
+
172
+ if float(ref_std) > 0:
173
+ sources = sources * ref_std
174
+ sources = sources + ref.mean()
175
+
176
+ stem_dir = run_dir / "demucs" / model_name / audio_path.stem
177
+ stem_dir.mkdir(parents=True, exist_ok=True)
178
+ assets: list[GeneratedAsset] = []
179
+ for source_tensor, source_name in zip(sources, model.sources):
180
+ stem_path = stem_dir / f"{source_name}.wav"
181
+ stem_audio = source_tensor.detach().cpu().numpy().T
182
+ sf.write(stem_path, stem_audio, model.samplerate)
183
+ assets.append(GeneratedAsset(label=f"Stem: {source_name}", path=stem_path))
184
+ return assets
185
+
186
+
187
+ def _mix_audio_files(paths: list[Path], output_path: Path) -> Path | None:
188
+ if not paths:
189
+ return None
190
+
191
+ mixed_audio = None
192
+ sample_rate = None
193
+
194
+ for path in paths:
195
+ audio, current_sr = sf.read(path, always_2d=True)
196
+ if mixed_audio is None:
197
+ mixed_audio = np.zeros_like(audio, dtype=np.float32)
198
+ sample_rate = current_sr
199
+ elif current_sr != sample_rate or audio.shape != mixed_audio.shape:
200
+ raise RuntimeError("Audio stems do not share the same shape/sample rate, so they cannot be mixed.")
201
+
202
+ mixed_audio += audio.astype(np.float32)
203
+
204
+ mixed_audio = np.clip(mixed_audio, -1.0, 1.0)
205
+ sf.write(output_path, mixed_audio, sample_rate)
206
+ return output_path
207
+
208
+
209
+ def _zip_directory(directory: Path) -> Path | None:
210
+ if not directory.exists() or not any(directory.rglob("*")):
211
+ return None
212
+ archive_base = directory.parent / directory.name
213
+ archive_path = shutil.make_archive(str(archive_base), "zip", root_dir=directory)
214
+ return Path(archive_path)
215
+
216
+
217
+ def build_background_music_asset(assets: list[GeneratedAsset], run_dir: Path) -> GeneratedAsset | None:
218
+ stem_paths = {asset.path.stem.lower(): asset.path for asset in assets if asset.label.startswith("Stem:")}
219
+ music_parts = [stem_paths[name] for name in ("bass", "drums", "other", "guitar", "piano") if name in stem_paths]
220
+ if not music_parts:
221
+ return None
222
+
223
+ background_dir = run_dir / "background"
224
+ background_dir.mkdir(parents=True, exist_ok=True)
225
+ output_path = background_dir / "background_music_only.wav"
226
+ mixed_path = _mix_audio_files(music_parts, output_path)
227
+ if mixed_path is None:
228
+ return None
229
+ return GeneratedAsset(label="Background music only", path=mixed_path)
230
+
231
+
232
+ def _require_audio_stack() -> None:
233
+ if librosa is None:
234
+ raise RuntimeError(
235
+ "librosa is not installed correctly. Reinstall dependencies before using speaker or noise extraction."
236
+ )
237
+
238
+
239
+ def export_speaker_tracks(
240
+ audio_path: Path,
241
+ run_dir: Path,
242
+ hf_token: str,
243
+ min_segment_length: float,
244
+ ) -> list[GeneratedAsset]:
245
+ _require_audio_stack()
246
+ try:
247
+ from pyannote.audio import Pipeline
248
+ except Exception as exc:
249
+ raise RuntimeError(
250
+ "pyannote.audio is not installed. Speaker separation needs the optional diarization stack."
251
+ ) from exc
252
+
253
+ if not hf_token.strip():
254
+ raise RuntimeError(
255
+ "Speaker separation needs a Hugging Face access token for the pyannote diarization model."
256
+ )
257
+
258
+ pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1", use_auth_token=hf_token.strip())
259
+ diarization = pipeline(str(audio_path))
260
+
261
+ signal, sample_rate = librosa.load(str(audio_path), sr=None, mono=False)
262
+ if signal.ndim == 1:
263
+ signal = np.expand_dims(signal, axis=0)
264
+
265
+ speakers_dir = run_dir / "speakers"
266
+ speakers_dir.mkdir(parents=True, exist_ok=True)
267
+
268
+ speaker_buffers: dict[str, np.ndarray] = {}
269
+ speaker_clip_counts: dict[str, int] = {}
270
+ speaker_durations: dict[str, float] = {}
271
+ assets: list[GeneratedAsset] = []
272
+
273
+ for segment, _, speaker in diarization.itertracks(yield_label=True):
274
+ duration = float(segment.end - segment.start)
275
+ if duration < min_segment_length:
276
+ continue
277
+
278
+ start = max(0, int(segment.start * sample_rate))
279
+ end = min(signal.shape[-1], int(segment.end * sample_rate))
280
+ if end <= start:
281
+ continue
282
+
283
+ if speaker not in speaker_buffers:
284
+ speaker_buffers[speaker] = np.zeros_like(signal)
285
+ speaker_clip_counts[speaker] = 0
286
+ speaker_durations[speaker] = 0.0
287
+
288
+ speaker_buffers[speaker][:, start:end] = signal[:, start:end]
289
+ speaker_clip_counts[speaker] += 1
290
+ speaker_durations[speaker] += duration
291
+
292
+ clip_path = speakers_dir / f"{speaker}_clip_{speaker_clip_counts[speaker]:03d}.wav"
293
+ sf.write(clip_path, signal[:, start:end].T, sample_rate)
294
+ assets.append(GeneratedAsset(label=f"{speaker} clip {speaker_clip_counts[speaker]}", path=clip_path))
295
+
296
+ for speaker, buffer in speaker_buffers.items():
297
+ speaker_path = speakers_dir / f"{speaker}_timeline.wav"
298
+ sf.write(speaker_path, buffer.T, sample_rate)
299
+ assets.insert(0, GeneratedAsset(label=f"{speaker} isolated timeline", path=speaker_path))
300
+
301
+ # Treat the longest-running speaker as the main voice and fold the rest
302
+ # together into a single "background voices" track.
303
+ if len(speaker_buffers) >= 2:
304
+ primary_speaker = max(speaker_durations, key=speaker_durations.get)
305
+ background_mix = np.zeros_like(signal)
306
+ for speaker, buffer in speaker_buffers.items():
307
+ if speaker != primary_speaker:
308
+ background_mix += buffer
309
+
310
+ background_voice_path = speakers_dir / "background_voices_only.wav"
311
+ sf.write(background_voice_path, np.clip(background_mix.T, -1.0, 1.0), sample_rate)
312
+ assets.insert(0, GeneratedAsset(label="Background voices only", path=background_voice_path))
313
+
314
+ if not assets:
315
+ raise RuntimeError("No speaker segments were produced. Try lowering the minimum segment length.")
316
+
317
+ return assets
318
+
319
+
320
+ def export_noise_estimates(audio_path: Path, run_dir: Path, prop_decrease: float) -> list[GeneratedAsset]:
321
+ _require_audio_stack()
322
+ try:
323
+ import noisereduce as nr
324
+ except Exception as exc:
325
+ raise RuntimeError(
326
+ "noisereduce is not installed. Noise extraction needs the optional denoising stack."
327
+ ) from exc
328
+
329
+ signal, sample_rate = librosa.load(str(audio_path), sr=None, mono=False)
330
+ cleaned = nr.reduce_noise(y=signal, sr=sample_rate, prop_decrease=prop_decrease)
331
+ estimated_noise = signal - cleaned
332
+
333
+ noise_dir = run_dir / "noise"
334
+ noise_dir.mkdir(parents=True, exist_ok=True)
335
+
336
+ cleaned_path = noise_dir / "denoised.wav"
337
+ noise_path = noise_dir / "estimated_noise.wav"
338
+ sf.write(cleaned_path, cleaned.T if cleaned.ndim > 1 else cleaned, sample_rate)
339
+ sf.write(noise_path, estimated_noise.T if estimated_noise.ndim > 1 else estimated_noise, sample_rate)
340
+
341
+ return [
342
+ GeneratedAsset(label="Denoised audio", path=cleaned_path),
343
+ GeneratedAsset(label="Estimated noise residue", path=noise_path),
344
+ ]
345
+
346
+
347
+ def _build_summary(run_dir: Path, steps: Iterable[str], warnings: Iterable[str], assets: list[GeneratedAsset]) -> str:
348
+ file_lines = [f"- {asset.label}: `{asset.path.name}`" for asset in assets]
349
+ warning_lines = [f"- {item}" for item in warnings if item]
350
+ return textwrap.dedent(
351
+ f"""
352
+ Processing finished.
353
+
354
+ Output folder:
355
+ `{run_dir}`
356
+
357
+ Completed steps:
358
+ {os.linesep.join(f"- {step}" for step in steps)}
359
+
360
+ Files:
361
+ {os.linesep.join(file_lines) if file_lines else "- No files were generated."}
362
+
363
+ Notes:
364
+ {os.linesep.join(warning_lines) if warning_lines else "- None"}
365
+ """
366
+ ).strip()
367
+
368
+
369
+ def _build_failure_summary(run_dir: Path, warnings: Iterable[str]) -> str:
370
+ warning_lines = [f"- {item}" for item in warnings if item]
371
+ return textwrap.dedent(
372
+ f"""
373
+ Processing could not generate any separated tracks.
374
+
375
+ Output folder:
376
+ `{run_dir}`
377
+
378
+ Most likely causes:
379
+ - One or more required packages are not installed correctly.
380
+ - Speaker separation was enabled without a valid Hugging Face token.
381
+
382
+ Detailed errors:
383
+ {os.linesep.join(warning_lines) if warning_lines else "- No extra details were captured."}
384
+ """
385
+ ).strip()
386
+
387
+
388
+ def process_audio(
389
+ audio_file: str | None,
390
+ video_file: str | None,
391
+ demucs_model: str,
392
+ run_source_separation: bool,
393
+ run_speaker_diarization: bool,
394
+ run_noise_estimation: bool,
395
+ hf_token: str,
396
+ min_segment_length: float,
397
+ prop_decrease: float,
398
+ ):
399
+ if not audio_file and not video_file:
400
+ raise gr.Error("Upload an audio or video file first.")
401
+
402
+ input_file = video_file if video_file else audio_file
403
+ original_path = Path(input_file)
404
+ run_id = f"{_safe_stem(original_path.stem)}_{uuid.uuid4().hex[:8]}"
405
+ run_dir = OUTPUT_DIR / run_id
406
+ run_dir.mkdir(parents=True, exist_ok=True)
407
+
408
+ steps: list[str] = []
409
+ warnings: list[str] = []
410
+ assets: list[GeneratedAsset] = []
411
+
412
+ if video_file:
413
+ uploaded_copy = run_dir / f"{original_path.stem}.wav"
414
+ try:
415
+ _run_command([
416
+ "ffmpeg", "-i", str(original_path),
417
+ "-vn", "-acodec", "pcm_s16le", "-ar", "44100", "-ac", "2", "-y",
418
+ str(uploaded_copy)
419
+ ])
420
+ assets.append(GeneratedAsset(label="Extracted audio from video", path=uploaded_copy))
421
+ steps.append("Extracted audio from video")
422
+ except Exception as exc:
423
+ raise gr.Error(f"Failed to extract audio from video: {exc}")
424
+ else:
425
+ uploaded_copy = _copy_uploaded_file(input_file, run_dir)
426
+ assets.append(GeneratedAsset(label="Original upload", path=uploaded_copy))
427
+
428
+ # Each processing path is optional on purpose. If one model is missing,
429
+ # the app still returns anything that did succeed instead of failing hard.
430
+ if run_source_separation:
431
+ try:
432
+ stem_assets = separate_with_demucs(uploaded_copy, run_dir, demucs_model)
433
+ assets.extend(stem_assets)
434
+ background_music_asset = build_background_music_asset(stem_assets, run_dir)
435
+ if background_music_asset is not None:
436
+ assets.append(background_music_asset)
437
+ demucs_archive = _zip_directory(run_dir / "demucs")
438
+ if demucs_archive is not None:
439
+ assets.append(GeneratedAsset(label="Demucs folder archive", path=demucs_archive))
440
+ steps.append(f"Source separation with Demucs ({demucs_model})")
441
+ except Exception as exc:
442
+ warnings.append(_summarize_runtime_error(str(exc)))
443
+
444
+ if run_speaker_diarization:
445
+ try:
446
+ assets.extend(export_speaker_tracks(uploaded_copy, run_dir, hf_token, min_segment_length))
447
+ steps.append("Speaker diarization and speaker track export")
448
+ except Exception as exc:
449
+ warnings.append(_summarize_runtime_error(str(exc)))
450
+
451
+ if run_noise_estimation:
452
+ try:
453
+ assets.extend(export_noise_estimates(uploaded_copy, run_dir, prop_decrease))
454
+ noise_archive = _zip_directory(run_dir / "noise")
455
+ if noise_archive is not None:
456
+ assets.append(GeneratedAsset(label="Noise folder archive", path=noise_archive))
457
+ steps.append("Noise reduction and residual-noise estimation")
458
+ except Exception as exc:
459
+ warnings.append(_summarize_runtime_error(str(exc)))
460
+
461
+ if len(assets) == 1:
462
+ summary = _build_failure_summary(run_dir, warnings)
463
+ return summary, [], []
464
+
465
+ downloadable_files = [str(asset.path) for asset in assets]
466
+ summary = _build_summary(run_dir, steps or ["No optional processing step succeeded."], warnings, assets)
467
+ audio_list = [{"label": asset.label, "path": str(asset.path)} for asset in assets if str(asset.path).lower().endswith(".wav")]
468
+ return summary, downloadable_files, audio_list
469
+
470
+
471
+ def build_demo() -> gr.Blocks:
472
+ # The UI stays intentionally simple: upload first, then tune the optional
473
+ # processing steps only if the recording needs them.
474
+ intro = """
475
+ # Audio Separator Studio
476
+
477
+ Upload one audio file and the app will try to split it into separate outputs:
478
+ vocals, accompaniment, drums/bass/other stems, individual speaker tracks, and an estimated noise layer.
479
+
480
+ Perfectly separating every sound in a messy real-world recording is not possible yet, but this app gets you
481
+ several practical layers from the same file.
482
+ """
483
+
484
+ guide = """
485
+ ## What each option means
486
+
487
+ **Upload audio or video**
488
+ Add the song, podcast, interview, or video recording you want to split. If you upload a video, the audio will be automatically extracted.
489
+
490
+ **Separation model**
491
+ This controls how Demucs separates music parts.
492
+
493
+ - `mdx_extra_q`: fastest and lightest. Good when you want quicker results.
494
+ - `htdemucs`: balanced option. Better for most people and the default choice.
495
+ - `htdemucs_ft`: slowest and heaviest. Usually the best quality when you want the strongest separation.
496
+ - `htdemucs_6s`: adds extra `guitar` and `piano` stems on top of the usual stems. `[Default Selected]`
497
+
498
+ **Separate music stems**
499
+ Creates files such as vocals, drums, bass, other, and also a combined `background music only` file with vocals removed.
500
+
501
+ **Separate speakers**
502
+ Tries to detect different human speakers in the recording and exports separate files for each speaker.
503
+ If there are multiple speakers, it also creates `background voices only`, which keeps the secondary speakers and removes the main speaker.
504
+
505
+ **Estimate background noise**
506
+ Creates a cleaner version of the audio and also a file containing the estimated leftover noise.
507
+
508
+ **Hugging Face token for speaker diarization**
509
+ This is only needed for `Separate speakers`.
510
+ A Hugging Face token is a private access key from Hugging Face.
511
+ `Speaker diarization` means detecting who spoke when, so the app can split one recording into speaker-wise tracks.
512
+ If you are not using speaker separation, you can leave this box empty.
513
+
514
+ **Minimum speaker segment length**
515
+ Very short speech fragments can create messy results. Increase this to ignore tiny fragments. Lower it if you want more detailed speaker clips.
516
+
517
+ **Noise reduction strength**
518
+ Higher values remove more noise, but can also affect the original sound more strongly.
519
+ """
520
+
521
+ css = """
522
+ .instructions-panel {
523
+ padding: 14px !important;
524
+ background: #2b2b31 !important;
525
+ border: 2px dotted rgba(255, 255, 255, 0.8) !important;
526
+ border-radius: 10px !important;
527
+ box-shadow: none !important;
528
+ }
529
+
530
+ .instructions-panel > div {
531
+ border: none !important;
532
+ box-shadow: none !important;
533
+ background: transparent !important;
534
+ padding: 0 !important;
535
+ }
536
+ """
537
+
538
+ with gr.Blocks(title="Audio Separator Studio", css=css) as demo:
539
+ gr.Markdown(intro)
540
+ with gr.Group(elem_classes=["instructions-panel"]):
541
+ gr.Markdown(guide)
542
+ with gr.Row():
543
+ with gr.Column(scale=2):
544
+ audio_input = gr.Audio(type="filepath", sources=["upload"], label="Upload audio")
545
+ video_input = gr.Video(sources=["upload"], label="Or upload video (MP4)", include_audio=True)
546
+ demucs_model = gr.Dropdown(
547
+ choices=["htdemucs", "htdemucs_ft", "htdemucs_6s", "mdx_extra_q"],
548
+ value="htdemucs_6s",
549
+ label="Source separation model",
550
+ info="Choose speed vs quality: mdx_extra_q = good and fast, htdemucs = better balanced, htdemucs_ft = best quality but slower, htdemucs_6s = adds guitar and piano stems.",
551
+ )
552
+ with gr.Row():
553
+ run_source_separation = gr.Checkbox(
554
+ value=True,
555
+ label="Separate music stems",
556
+ info="Splits the audio into stems like vocals, drums, bass, other, and background-music-only.",
557
+ )
558
+ run_speaker_diarization = gr.Checkbox(
559
+ value=True,
560
+ label="Separate speakers",
561
+ info="Splits different human speakers into separate files. Needs a Hugging Face token.",
562
+ )
563
+ run_noise_estimation = gr.Checkbox(
564
+ value=True,
565
+ label="Estimate background noise",
566
+ info="Creates a denoised version and a separate noise-only estimate.",
567
+ )
568
+ hf_token = gr.Textbox(
569
+ value=os.environ.get("HF_TOKEN", ""),
570
+ type="password",
571
+ label="Hugging Face token for speaker diarization",
572
+ info="Only needed for speaker separation. It is your Hugging Face access key used to load the speaker-diarization model.",
573
+ )
574
+ min_segment_length = gr.Slider(
575
+ minimum=0.25,
576
+ maximum=3.0,
577
+ value=0.75,
578
+ step=0.05,
579
+ label="Minimum speaker segment length (seconds)",
580
+ info="Higher values ignore tiny speech fragments. Lower values keep more short speech clips.",
581
+ )
582
+ prop_decrease = gr.Slider(
583
+ minimum=0.1,
584
+ maximum=1.0,
585
+ value=0.9,
586
+ step=0.05,
587
+ label="Noise reduction strength",
588
+ info="Higher values remove more noise, but can also remove more detail from the original audio.",
589
+ )
590
+ run_button = gr.Button("Process Audio", variant="primary")
591
+ with gr.Column(scale=3):
592
+ summary = gr.Markdown(label="Summary")
593
+ downloads = gr.File(label="Generated files", file_count="multiple")
594
+
595
+ audio_players_state = gr.State([])
596
+
597
+ @gr.render(inputs=audio_players_state)
598
+ def render_audio_players(audio_list):
599
+ if audio_list:
600
+ gr.Markdown("### Listen to Generated Audio")
601
+ for audio_info in audio_list:
602
+ gr.Audio(value=audio_info["path"], label=audio_info["label"])
603
+
604
+ run_button.click(
605
+ fn=process_audio,
606
+ inputs=[
607
+ audio_input,
608
+ video_input,
609
+ demucs_model,
610
+ run_source_separation,
611
+ run_speaker_diarization,
612
+ run_noise_estimation,
613
+ hf_token,
614
+ min_segment_length,
615
+ prop_decrease,
616
+ ],
617
+ outputs=[summary, downloads, audio_players_state],
618
+ )
619
+
620
+ return demo
621
+
622
+
623
+ if __name__ == "__main__":
624
+ demo = build_demo()
625
+ demo.queue().launch()
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Best on Python 3.10 or 3.11.
2
+ gradio>=5.25.0
3
+ numpy>=1.26.0
4
+ soundfile>=0.12.1
5
+ librosa>=0.10.2
6
+ noisereduce>=3.0.2
7
+ demucs>=4.0.1
8
+ torch>=2.2.0
9
+ torchaudio>=2.2.0
10
+ pyannote.audio>=3.3.2