← ExpSoft Diarizer

Pyannote first, PyTorch second — the install-order detail that decides whether Diarizer uses your GPU

ExpSoft Diarizer is a personal-use Windows wrapper around pyannote.audio — the open-source speaker-diarization toolkit by Hervé Bredin and contributors, distributed under MIT. The pipeline relies on three HuggingFace models, two of which are gated (each user must accept the conditions individually on the model page), so by design the Diarizer never bundles the weights — they’re downloaded with the user’s own HF token at first-run (see the Legal & Compliance section of the product page). Most of the engineering inside Diarizer is plumbing — Miniconda for Tk, FFmpeg for non-WAV audio decoding, the venv. The one decision that decides whether your training-grade GPU gets used or not is the order in which pip installs pyannote and PyTorch: pyannote first, then a forced reinstall of PyTorch with the matching CUDA wheel index. Reverse it and pyannote’s dependency resolver silently downgrades you to a CPU-only PyTorch. This note walks through that install-order subtlety, the three build matrix, and the offline-first HF cache that keeps long runs from timing out mid-interview.

· 11 min read · By Nicolas Riquier

The diarization problem on Windows in 2026

Speaker diarization — answering “who is talking when” inside a multi-speaker audio track — has been a solved research problem for years. pyannote.audio is the state of the art in the open-source space, maintained by Hervé Bredin and a large community of contributors. The models are good. The library is well-documented. The papers are clear.

On Windows in 2026, the gap between “the research works” and “a podcaster on a laptop can use it” is roughly ten to fifteen install steps:

Each step is small. Together, they push a podcaster, a journalist, or a researcher in a linguistics lab — none of whom signed up to be a Python distribution expert — into the kind of all-day yak-shaving that ends with “maybe I’ll just transcribe it manually.”

ExpSoft Diarizer is the personal-use wrapper that turns the ten-to-fifteen-step install into one click. The engineering choices below are what make that click work reliably across machines.

The install-order subtlety — pyannote before PyTorch

This is the decision that the source code calls out in a multi-line comment so that whoever touches the file in two years doesn’t silently re-introduce the bug.

pyannote.audio 4.x declares torch>=2.8 in its package metadata. PyTorch ships separate wheel sets per CUDA version: cu124 for the RTX 20xx/30xx/40xx generations, cu128 for RTX 50xx Blackwell, and cpu for systems without a discrete GPU. The wheels live at separate index URLs on download.pytorch.org/whl/. They cannot be selected by pip install torch against the standard PyPI index alone — you have to pass --index-url explicitly.

Now consider what happens if you install in the order most people reach for first — pin the right PyTorch wheel first, then add pyannote:

pip install torch==2.6.0+cu124 torchaudio==2.6.0+cu124 \
    --index-url https://download.pytorch.org/whl/cu124
pip install pyannote.audio

The second command sees that pyannote requires torch>=2.8, looks at what’s installed (torch==2.6.0+cu124), decides that doesn’t satisfy the constraint, and pulls a generic CPU-only torch>=2.8 from the standard PyPI index. Your carefully-pinned CUDA wheel is gone. torch.cuda.is_available() returns False at runtime. The user’s 4090 sits idle.

The reverse order works:

pip install pyannote.audio pydub pyyaml
pip install torch==2.6.0+cu124 torchaudio==2.6.0+cu124 \
    --force-reinstall --index-url https://download.pytorch.org/whl/cu124

Now pyannote installs first; pip resolves its torch dependency to a generic CPU torch>=2.8 (which is fine for the metadata check). Then the explicit --force-reinstall overwrites that generic torch with the pinned CUDA wheel. pyannote’s metadata is never re-evaluated by pip, so the install stays “valid” from pip’s perspective. At runtime, the loaded torch is the CUDA one, and the GPU gets used.

This is not a workaround for a bug — it’s a consequence of pip’s installed-package strategy and PyTorch’s separate-index distribution model. Spelling it out:

Diarizer’s SetupService.cs embeds this ordering in step 5, with a paragraph of inline comment explaining why. The CI pipeline’s smoke test asserts torch.cuda.is_available() after Setup, so any future change to the order breaks the build.

Three build targets — same source, three CUDA wheels

The Diarizer ships in three flavours, packed as three separate ZIPs:

The split lives in the C# project file as three Release configurations with DefineConstants: default (40xx), GPU_50XX, or CPU_ONLY. The Python-facing constants (TorchVersion, TorchCudaIndex, TorchIndexUrl) are #if-gated. On the CPU-only build, the GPU radio button is disabled and the device is hardcoded to "cpu"; on the GPU builds, the GPU detection step at Setup compares nvidia-smi’s reported card series against TargetSeries and surfaces a friendly warning if the user picked the wrong ZIP (40xx build on a Blackwell card, or vice versa). Catching the mismatch up-front is much better than an obscure runtime error after a 90-minute install.

Why not a single mega-installer with runtime probing? Two reasons. First, the three wheel sets total 4-7 GB of download — pre-fetching all of them is wasteful on slow connections, and the user typically knows what GPU they have. Second, the wheels are mutually exclusive at install time. Switching between them after install means pip uninstall + pip install, which is a lot of disk thrash for what is conceptually a different build. ZIP-level split is cleaner.

Offline-first HF cache — and the one step where it’s temporarily off

Speaker-diarization sessions are usually long: an hour-long podcast, a two-hour interview, a multi-speaker meeting recording. The runs are unattended. The least helpful thing the underlying library can do, halfway through inference, is silently try to fetch a model update over HTTPS and time out behind a corporate proxy.

Diarizer’s GetIsolatedEnvVars() sets the following on every Python sub-process it launches:

HF_HUB_OFFLINE=1
TRANSFORMERS_OFFLINE=1
HF_HOME=cache/HF_HOME/
HUGGINGFACE_HUB_CACHE=cache/HF_HOME/
TRANSFORMERS_CACHE=cache/HF_HOME/
TORCH_HOME=cache/TORCH_HOME/
PIP_CACHE_DIR=cache/PIP_CACHE/
XDG_CACHE_HOME=cache/

The first two variables tell HuggingFace’s library to skip any network call and rely entirely on what’s already in the local cache. The rest redirect every cache location under the app’s own folder, so nothing leaks into %USERPROFILE%\.cache or %APPDATA% — uninstall is “delete the folder” with no residue.

The exception is Setup step 6, the model download. For that step alone, the EasyInstaller overrides HF_HUB_OFFLINE=0 and TRANSFORMERS_OFFLINE=0 in extraEnv for the sub-process, injects the user’s HF_TOKEN, and runs huggingface_hub.snapshot_download for each of the three models. Once those land on disk, the override goes away. Every subsequent run is fully offline. A diarization session can complete on a laptop with no network connection at all.

Gated models — never bundled

Two of the three pyannote models are gated on HuggingFace: pyannote/segmentation-3.0 and pyannote/speaker-diarization-3.1. The license terms require each user to individually accept the conditions on the model page before downloading. Bundling those weights in the Diarizer ZIP would be a clear violation of the gating contract.

So Diarizer never bundles them. The Setup flow walks the user through:

  1. Create an account on HuggingFace if they don’t have one.
  2. Visit the two gated model pages (links are clickable in the Welcome tab) and accept the terms.
  3. Generate an access token in their HuggingFace account settings.
  4. Paste the token into Diarizer’s Settings tab.

Step 6 of Setup uses the token to download the three models locally. From that point on, the user’s acceptance of the gating terms is recorded against their HuggingFace account, the models live in models/ under the install folder, and the offline-first cache configuration kicks in.

The third model — pyannote/wespeaker-voxceleb-resnet34-LM — is CC-BY-4.0 and not gated, but Diarizer downloads it through the same flow for consistency. The CC-BY attribution requirement is satisfied in the Welcome tab’s Licenses card.

RAM watchdog — the hour-long interview problem

Diarizing a one-hour audio file with pyannote can use a surprising amount of RAM, because the library extracts and holds embeddings for every speech segment in the file before clustering. On a 32 GB machine this is invisible. On a 16 GB laptop with a browser, an IDE, and an audio editor open, it’s the difference between “the diarization finishes in 15 minutes” and “the system swaps for an hour then OOM-kills random processes.”

The RAM watchdog runs in parallel with the Python sub-process. Every three seconds it calls Win32 GlobalMemoryStatusEx and reads ullAvailPhys. If the available physical RAM drops below 2 GB, it sends a graceful cancel signal to the sub-process and logs a user-visible message: “Available RAM critically low (XXX MB). Cancelling to protect system stability.” The user sees the reason — clearer than a silent crash.

Batch mode and the progress protocol

The Run tab has a toggle for batch processing — drop a folder instead of a single file, and Diarizer processes every audio file in the folder in sequence. The progress mapping is straightforward: each file gets a 0-100% local progress, and that local progress is rescaled to the global (i / n, (i+1) / n) band so the global progress bar advances continuously across the batch.

The communication protocol between the Python and the C# side is the same minimalist convention used elsewhere in the ExpSoft catalogue — single-line stdout prefixes:

PROGRESS:42.5        (current-file progress, percentage)
[anything else]      (informational log, written to the dark-console pane)

The C# side parses each OutputDataReceived line, dispatches by prefix, and updates either the progress bar or the log. tqdm-style stderr lines from pyannote (containing % / it/s / s/it) are recognised by pattern and shown clean in the log without the [ERR] prefix. No JSON-RPC framing, no schema, no version contract to maintain — just stdout.

Drive-portable install — AutoPatchVenvPaths

Like the rest of the ExpSoft catalogue, Diarizer’s install is fully portable. The full chain — Miniconda, the venv, the three models, all caches — lives under the install folder. Nothing in %USERPROFILE% or the registry or PATH.

At every launch, PathService.AutoPatchVenvPaths() reads the venv’s recorded root path from pyvenv.cfg, compares it to the current AppContext.BaseDirectory, and if they differ rewrites every absolute path reference in the venv text files (activate.bat, pip.exe launchers, .pth files, direct_url.json in .dist-info/ folders, conda-meta JSONs). The patch is idempotent — a no-op on a venv that’s already correct.

Drop the install folder on a USB stick, plug into a different Windows machine, double-click. Diarizer launches. No reinstall.

Credits and license

The C# / WPF EasyInstaller shell is original ExpSoft work and distributed independently of pyannote.audio. No models are bundled — the user downloads them with their own HF token, which records the individual acceptance of the gating terms against their own HuggingFace account.

Take-aways

Frequently Asked Questions

Quick answers to what people ask AIs about this article specifically.

Why does the install order pyannote-then-PyTorch matter so much?

pyannote.audio 4.x declares torch>=2.8 in its metadata. If a pinned CUDA-specific PyTorch (say, torch==2.6.0+cu124) is installed first, pip then sees the constraint isn’t satisfied when pyannote is installed second, and pulls a generic CPU-only torch>=2.8 from PyPI, overwriting the CUDA wheel. torch.cuda.is_available() returns False at runtime. Installing pyannote first lets pip resolve to a generic torch (fine for the metadata check), then the explicit --force-reinstall --index-url cu124 overwrites it with the right CUDA wheel without re-evaluating pyannote’s metadata. The install stays consistent from pip’s perspective and CUDA is correctly available.

Why are two of the three pyannote models gated, and why doesn’t Diarizer bundle them?

pyannote/segmentation-3.0 and pyannote/speaker-diarization-3.1 are MIT-licensed but distributed via HuggingFace’s gating mechanism, which requires each user to individually accept the terms on the model page before downloading. The contract is per-user, not per-distributor — bundling the weights in a redistributed package would short-circuit the user-individual acceptance. Diarizer’s Setup flow walks the user through token generation and gating acceptance, then downloads the models with the user’s own token at Setup step 6. From that point on the user’s acceptance is recorded against their HuggingFace account.

Can Diarizer run fully offline after Setup?

Yes. Every Python sub-process is launched with HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1. The only step that temporarily flips them off is Setup step 6, which downloads the three models with the user’s HF token. After that, no network call is made by the diarization pipeline — runs on a laptop with no internet connection complete fine. The single network call from the EasyInstaller itself is the optional version check against nicolas-riquier.com/ExpSoft_Versions/versions.json at startup, which can be disabled in Settings.

Why three separate builds (40xx / 50xx / CPU) rather than one mega-installer?

The PyTorch wheel sets for cu124, cu128, and cpu are mutually exclusive and total 4-7 GB across the three. A single installer would either have to fetch all three at install time (slow and wasteful on a typical home connection) or detect-then-fetch the right one, which means failing in the middle of Setup on a slow connection. Three separate ZIPs let the user pick the right one upfront. The GPU detection step at Setup warns if the chosen build doesn’t match the detected card, so picking the wrong ZIP is caught up-front, not at the first inference call.

What happens if my system RAM drops below 2 GB during a long diarization?

The RAM watchdog (a background task that polls GlobalMemoryStatusEx every three seconds) detects the low-memory condition and sends a graceful cancel signal to the Python sub-process. A user-visible message appears in the log: “Available RAM critically low (XXX MB). Cancelling to protect system stability.” The reason is explicit — better than a silent kill, much better than letting Windows’ own OOM-killer take out random processes. Closing other heavy applications (browser, IDE) and restarting the diarization typically completes successfully.

Want to use ExpSoft Diarizer?

Get it on Patreon →