voicegrab/GPU-upgrade.md

12 KiB
Raw Blame History

GPU / AI-Separator Upgrade — Planning Notes (for me, the assistant)

Status: planned, not built. Last updated: 2026-08-23. Context: VoiceGrab core (voicegrab.py) currently isolates voice with the noisereduce STFT pipeline (DSP). This note covers adding a deep-learning separator (Demucs / UVR / RoFormer family) as an optional "heavy isolation" mode for clips with music or competing speech.


1. Problem the DSP path doesn't solve

noisereduce (spectral gating) is great for stationary / broadband noise: fan, hum, room tone, light traffic. It cannot separate:

  • background music (guitars, drums, other instruments overlap speech band)
  • a second speaker talking
  • intermittent noise bursts that contain voice-like formant structure

Spectral gate will either leave the music or carve holes in the target voice (pumping artifacts). For AI-voice-training samples, a source separator (speech vs. non-speech) is the right tool.

2. Candidate models (ranked for our use case)

Model Size (ckpt) What it does Notes
Demucs htdemucs_ft ~92 MB 4-stem (vocals/drums/bass/other) MIT code + weights, maintained, CLI-ready, best "default choice". htdemucs (80 MB) is the original. Fine-tuned _ft preserves voice quality better — prefer it.
BS-RoFormer / Mel-Band RoFormer (vocal separation & speech enhancement) ~50100 MB SOTA speech/music separation quality Checkpoint licensing is research-leaning — must verify before bundling/redistributing.
UVR5 MDX-Net / VR Arch checkpoints ~70100 MB each Community vocal-sep models, many flavors The app is a GUI wrapper; the checkpoints are community-made with mixed licenses — risky to ship. Good for reference only.
Demucs htdemucs_6s ~100 MB 6-stem incl. a dedicated "speech" stem Heavier; only worth it if 4-stem vocals bleed.

Decision (revisit at build time): htdemucs_ft as the default AI mode — permissive license, single dependency (demucs pulls torch), and its "vocals" stem is exactly what we want. Keep an eye on RoFormer-family license clarity; if permissive weights appear, they become the recommended quality option.

License gate: never bundle a checkpoint we can't point to a permissive license for. Demucs/htdemucs(_ft) = MIT. Everything else: verify first.

3. Hardware & performance reality check

Local machine (voicegrab2 host): no NVIDIA GPU (no nvidia-smi), 8 cores, 14 GB RAM. So:

  • Development/testing here will be CPU-only. That's fine for correctness and pipeline integration; all real speed numbers below for "what the user experiences" assume an RTX 30/40/50-class GPU.
  • CPU fallback must still work (Windows users without GPU, Macs).

Estimates (5 s clip → 60 s clip):

Backend 5 s 60 s VRAM Notes
DSP (current) <1 s ~1020 s ~200 MB RAM Always works, CPU
Demucs CPU ~1025 s ~26 min ~12 GB RAM usable for short samples
Demucs CUDA (46 GB VRAM) ~13 s ~1540 s ~2 GB peak comfortable at 6 GB+
6-stem / RoFormer similar +3050% similar +3050% ~24 GB longer context = more VRAM

Practical guidance to surface in UI copy:

  • Target clip length for AI voice samples is 30120 s → fine on 4 GB VRAM.
  • First run downloads model (~92 MB) to user data dir. Show this explicitly.
  • CPU mode: set expectations ("a few minutes for a 60 s clip").

4. Architecture plan

4.1 Backend abstraction (replace direct VoiceReducer calls)

# voicegrab.py (sketch)
class Separator:
    name: str                 # "None" | "Noise reduction (fast)" | "AI separation (Demucs)"
    def available(self) -> tuple[bool, str]: ...   # (ok, reason)
    def separate(self, y: np.ndarray, sr: int, progress) -> np.ndarray: ...

class DSSeparator(Separator):      # current noisereduce path, unchanged behavior
    ...

class DemucsSeparator(Separator):  # lazy import; GPU-aware
    def __init__(self):
        import torch, demucs.apply, demucs.pretrained
        ...
    def separate(self, y, sr, progress):
        wav = y[None, :, :]                 # demucs expects (1, C, T) float32
        model = self._model()               # cached singleton
        sources = demucs.apply.apply_model(model, wav, device=self.device, shifts=1)
        vocals = sources[model.sources.index("vocals")]
        return vocals[0, 0].cpu().numpy()   # mono speech

Pipeline order becomes: extract range → optional separation (DSP or AI) → loudnorm → MP3 320k. The AI path replaces DSP, not stacks on it — running noisereduce after Demucs is usually counterproductive (double-processing artifacts). Keep DSP as an independent mode, not a post-filter.

4.2 Device selection

def pick_device():
    if torch.cuda.is_available():
        return "cuda"
    if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
        return "mps"          # Mac bonus, free
    return "cpu"

Surface the active device in status text ("Using GPU (CUDA)" / "CPU mode — will be slower").

4.3 GUI changes

  • Replace the single "Isolate voice" checkbox with a mode selector:
    1. No processing (just loudness-normalize — current non-isolate path)
    2. Noise reduction (fast) — current DSP, default, no downloads
    3. AI voice separation (Demucs) — shows a note: "First use downloads ~92 MB of models. GPU strongly recommended."
  • Progress: demucs.apply.apply_model supports a callback; route it into the existing Worker.log signal. On first use, pretrained.get_model("htdemucs_ft") download should report progress too (it goes through torch.hub/urllib — wrap or at least show "Downloading model… (x MB)" in the log line).
  • Model cache: appdata_dir()/models/ (already the pattern for the old profile idea; reuse appdata_dir()).

4.4 Threading / UX

  • Keep the existing Worker(QThread); add a busy spinner + cancel button. Demucs on CPU can run minutes — subprocess-style cancellation isn't available for in-process torch, so implement a soft cancel: worker checks a flag between apply_model shifts (pass shifts=2, check between) — or accept no-cancel for v1 and say so.
  • Disable the Export button while running (already the pattern).

5. Packaging — the hard part (be honest about it)

torch + demucs is the weight problem. Options, ranked:

A. "AI Pack" as a separate installer component (recommended). Core app stays small & instant-start (PyInstaller onefile, ~4080 MB with PySide6+ffmpeg). Ship a second Inno Setup component ("AI voice separation (Demucs), ~1.52.5 GB after install") that drops pre-wheeled torch+demucs wheels into %APPDATA%\VoiceGrab\ai\wheels. On first AI-mode use, the app builds a venv from those wheels (python -m venv ~/.local/share/VoiceGrab/ai_venv) and imports from it (or runs separation as a sidecar process — see B). Pros: core users pay nothing; opt-in size; works offline after download. Cons: most moving parts; needs careful Windows testing.

B. Separation as a sidecar executable (cleanest runtime boundary). Build separate.pydemucs-cli.exe with PyInstaller (--collect-all torch → big, but it's an isolated binary, e.g. VoiceGrab-AI.exe next to the core app). Core app shells out: VoiceGrab-AI.exe --in clip.wav --out vocals.wav --device cuda. Pros: crash/firewall/GPU driver issues can't take down the GUI; no venv bootstrapping; trivial to update independently; the demucs CLI already does exactly this job. Cons: one more build artifact. → I'd pick B for Windows, A-or-B for Linux. In practice B generalizes: it is the demucs CLI wrapped in a branded exe.

C. Single onefile with torch bundled. Don't. 8 GB+ onefile, 60120 s extraction on every start, memory-mapped junk in %TEMP%. Only acceptable as a "portable full build" niche option.

Linux specifics

  • AppImage with torch = same bloat problem → ship VoiceGrab (core, as today)
    • VoiceGrab-AI (sidecar binary, ~2 GB) or a pip install -e .[ai] path.
  • A second .deb component with the wheels in /usr/share/voicegrab-ai is also reasonable.

6. Correctness / QA plan

  1. Unit: separate() returns float32 mono, shape (T,), dtype invariance, no NaN, SR unchanged (Demucs resamples to 44.1 k internally — re-resample back to 48 k before loudnorm, or just feed 44.1 through to MP3; decide and document. Voice training tools accept 44.1k fine → simplest: feed Demucs output at 44.1k straight to MP3, skip the 48k round-trip).
  2. Regression: existing kage.mp4 16 s test must produce a valid 320 kbps MP3 in both DSP and AI modes; assert ffprobe duration ≈ 5.0 s ± 0.15 s (MP3 encoder padding).
  3. A/B listening test: pick 3 clips (clean speech; speech+fan; speech+music) — currently only have kage.mp4; need 2 more fixtures. Record: artifacts? "chipmunk" consonants (classic Demucs failure)? residual music?
  4. GPU smoke: on a CUDA machine: torch.cuda.is_available(), model load, 60 s clip timing, VRAM peak via torch.cuda.max_memory_allocated(). Acceptance: < 2× realtime on ≥ 6 GB VRAM.
  5. CPU timeout sanity: 60 s clip on 8-core CPU must finish < 8 min; surface estimated time in UI ("≈ 24 min on CPU").
  6. No-GPU Windows box (test VM): CPU path + model download + offline second run (no re-download).

7. Risks / open questions

  • Model redistribution license — only ship MIT (Demucs). Re-check checkpoint license at build time; put a THIRD_PARTY.md in the installer.
  • Torch × Python 3.14 — current host is 3.14; verify wheel availability at target versions (torch tracks newest Pythons well, but pin in CI).
  • CUDA wheel size on Windowstorch with cu126 wheels ≈ 2.5 GB (nvidia-cublas etc. are separate wheels). Sidecar build must --collect-all torch nvidia.*.
  • AMD/Intel users — ROCm wheels exist (Windows + Linux) but are finicky; IPEX for Intel Arc. v1: NVIDIA + CPU only; document the rest as "may work".
  • Demucs driftdemucs is PyTorch-Lightning-free now but its API (apply_model, pretrained.get_model) has shifted across versions. Pin demucs==4.1.x in the AI-pack wheels.
  • Voice fidelity tradeoff: aggressive separation can thin out sibilance / breath — exactly what TTS trainers want to keep. htdemucs_ft is the gentler choice; also consider post-gain before loudnorm (separated vocals sit ~610 dB lower than the mix).
  • Cancel semantics for CPU-mode runs (see 4.4) — decide before build.

8. Suggested build order (if I do this)

  1. SeparationBackend interface + move existing DSP behind it (small, no deps) — safe, half a day.
  2. Add demucs to a dev-only venv extra; wire DemucsSeparator with lazy import + pick_device(); test end-to-end on this CPU box with kage.mp4 (5 s, expect ~1030 s).
  3. UI: mode selector + progress + device line. (Half a day.)
  4. Packaging: prototype B (sidecar) with PyInstaller on this box (CPU build) to shake out --collect-all issues; finalize Windows build script.
  5. QA pass from §6; write THIRD_PARTY.md; bump version to 1.1.0.

Rough total: 24 focused days, packaging being the long pole.

9. Files this would touch

  • voicegrab.py — backend interface, DemucsSeparator, UI mode selector
  • installer/build_win.ps1 — AI sidecar build step
  • installer/VoiceGrab.iss — new [Components]/[Files] entries
  • installer/build_linux.sh — sidecar variant
  • requirements-ai.txt (new) — demucs==4.1.*, torch==2.*
  • README.md — AI mode docs + GPU guidance
  • tests/test_separators.py (new) — §6 automation

Reminder: the "big model" option is a product feature behind a checkbox, never a hard dependency. Core app must stay <100 MB and start in <2 s.