← ExpSoft Diarizer

PyTorch first, pinned above pyannote’s floor — 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 — a portable uv-managed Python, a portable FFmpeg that reads any audio or video file, the venv. The one decision that decides whether your training-grade GPU gets used or not is how the installer pins and orders PyTorch and pyannote: PyTorch first, from its CUDA wheel index, at a version that already satisfies pyannote’s torch floor, then pyannote with exact pins. Pin a PyTorch below that floor and the resolver silently replaces it with a CPU-only build. This note walks through that install-order subtlety, the three build matrix, and the offline-first model folder that keeps long runs from timing out mid-interview.

· Updated · 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 — a PyTorch above pyannote’s floor, installed first

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, cu128, cpu and others. Diarizer 1.2.0.0 uses a single cu128 set for every card from RTX 20xx to RTX 50xx (the cu124 index stops at torch 2.6, below pyannote 4’s floor), and cpu for systems without an NVIDIA 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.

The failure most people hit first is real: pin a CUDA PyTorch that is older than pyannote’s floor, 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 torch==2.6.0+cu124 does not satisfy torch>=2.8 and pulls a generic CPU-only torch from the standard PyPI index. The CUDA wheel is gone, and torch.cuda.is_available() returns False at runtime.

Earlier versions of Diarizer worked around this by installing pyannote first, then force-reinstalling PyTorch from the CUDA index. That puts a CUDA torch back on disk, but one below pyannote’s declared floor, so the environment no longer passes pip check. Version 1.2.0.0 removes the conflict at the source. It pins a PyTorch that already satisfies the floor and installs it first, from its own index. Then it installs pyannote with exact pins:

uv pip install torch==2.11.0 torchaudio==2.11.0 \
    --index-url https://download.pytorch.org/whl/cu128
uv pip install -r requirements.txt -c constraints.txt

The resolver sees that the installed torch 2.11.0 satisfies torch>=2.8 and keeps it. There is no --force-reinstall and no version conflict. Three rules keep it that way:

Diarizer’s SetupService.cs embeds this ordering in the Dependencies step of Setup, with an inline comment explaining why. The step ends with an import check of the engine, PyTorch and pyannote that logs the torch version and whether CUDA is available.

Three packages — same source, two PyTorch stacks

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. Only TargetSeries and TorchIndexUrl are #if-gated; TorchVersion is 2.11.0 in all three. On the CPU-only build, the GPU radio button is disabled and the device is hardcoded to "cpu"; on the GPU builds, the GPU & RAM step of Setup reads the card through nvidia-smi, shows its name, VRAM and the build target, and warns when the card has less than 6 GB of VRAM or the machine less than 8 GB of RAM. The 40xx and 50xx packages now install the same cu128 stack, so on any RTX 20xx to 50xx card the choice between them no longer changes the wheels.

Why not a single mega-installer with runtime probing? Two reasons. First, a GPU install downloads about 3.5 GB and a CPU install about 1 GB — pre-fetching both stacks is wasteful on slow connections, and the user typically knows what hardware they have. Second, the CUDA and CPU 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 models — and the one step that goes online

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.

The diarization engine sets the first two variables itself, before it imports anything from HuggingFace, and Diarizer’s GetIsolatedEnvVars() redirects every cache on every Python sub-process it launches:

HF_HUB_OFFLINE=1
TRANSFORMERS_OFFLINE=1
HF_HOME=cache/huggingface/
TORCH_HOME=cache/torch/
UV_CACHE_DIR=cache/uv/
UV_PYTHON_INSTALL_DIR=runtime/python/
MPLCONFIGDIR=cache/matplotlib/

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 the Models step of Setup. It runs a separate script that sets HF_HUB_OFFLINE=0 for itself and receives the user’s HF_TOKEN through the environment, never on the command line. It runs huggingface_hub.snapshot_download for each of the three repositories into plain folders under models\. The diarization runs keep the offline default. 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 Settings tab) and accept the terms.
  3. Generate an access token in their HuggingFace account settings.
  4. Paste the token into Diarizer’s Settings tab.

The Models step 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.

Long recordings — a streamed split instead of a RAM watchdog

Earlier versions relied on a RAM watchdog beside the Python sub-process. Version 1.2.0.0 removes the cause instead: the per-speaker tracks are written in a stream. The decoded source is a memory-mapped file. Blocks of about 20 seconds are masked per speaker and fed to one FFmpeg encoder per speaker, all running in parallel, so RAM stays at a few blocks whatever the length of the recording. Setup checks for 8 GB of system RAM, and Cancel stops a run cleanly, keeping the files already finished.

Batch mode and the progress protocol

The DIARIZE tab takes a queue of files and folders, audio and video. The whole batch runs in one engine process, so the pyannote pipeline is loaded only once. Each file’s .diarization.json is written last. A batch relaunched on the same folder skips the files that already have one, so it resumes where it stopped. The engine rescales each file’s progress into its share of the batch and reports the whole-batch figure in thousandths, so the progress bar advances continuously.

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

[file] 2/5 <input path>                  (a file starts)
[progress] 412/1000                      (whole-batch progress, in thousandths)
[cli] OK <first track> (12.3s, seed 0)   (a file is done)
[cli] SKIP <input path>                  (already processed)
[anything else]                          (informational log)

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 kept out of the on-screen log (they stay in the run log file). Other stderr lines are tagged by level, and only real errors get the [ERR] prefix. No JSON-RPC framing, no schema, no version contract to maintain — just text lines.

Drive-portable install — AutoPatchVenvPaths

Like the rest of the ExpSoft catalogue, Diarizer’s install is fully portable. The full chain — uv, a portable Python, the venv, FFmpeg, the three models, all caches — lives under the install folder. Nothing goes into %USERPROFILE%, the registry or PATH; the only system-wide prerequisite is the .NET 8 Desktop Runtime.

At every launch, PathService.AutoPatchVenvPaths() reads the install root recorded in pyvenv.cfg and compares it with the current folder. Only if the install has moved does it rewrite the absolute paths in the venv text files (pyvenv.cfg, the non-binary files of Scripts\, .pth files, direct_url.json in .dist-info/ folders), point the venv at the portable Python, and replace the launcher stubs with real copies of the interpreter. On a venv that has not moved it does nothing.

The repair is designed so that the install folder can move to another drive, or to another Windows machine with the .NET 8 Desktop Runtime, and be repaired at the next launch instead of reinstalled.

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 PyTorch install order matter so much?

pyannote.audio 4.x declares torch>=2.8 in its metadata, and PyTorch’s CUDA wheels live on PyTorch’s own index, not on PyPI. If the CUDA PyTorch installed first is older than that floor (say, torch==2.6.0+cu124), installing pyannote afterwards pulls a generic CPU-only torch from PyPI, and torch.cuda.is_available() returns False. Diarizer 1.2.0.0 pins torch==2.11.0 and installs it first, from the cu128 index (the cpu index for the CPU package). It then installs pyannote with exact pins and a full constraints file. The installed torch already satisfies the floor, so the resolver keeps it: no forced reinstall, no version conflict.

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 in the Models step of Setup. From that point on the user’s acceptance is recorded against their HuggingFace account.

Can Diarizer run fully offline after Setup?

Yes. The diarization engine sets HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 before it imports anything from HuggingFace. Only the Models step of Setup goes online, to download the three model repositories 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, outside Setup, is the version check against nicolas-riquier.com/ExpSoft_Versions/versions.json at startup.

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

Diarizer 1.2.0.0 installs one of two PyTorch stacks: cu128 for the RTX 50xx and RTX 40xx (and 20/30) packages, cpu for the CPU package. They are mutually exclusive; a GPU install downloads about 3.5 GB, a CPU install about 1 GB. A single installer would either have to fetch both 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 40xx and 50xx packages install the same wheels, so either works on any RTX 20xx to 50xx card. The CPU package hides the GPU choice, and Setup’s GPU & RAM step shows the detected card and warns below 6 GB of VRAM.

Does a long recording need a lot of RAM?

No. Diarizer writes the speaker tracks in a stream: the decoded source is memory-mapped and each speaker has its own FFmpeg encoder, so RAM stays at a few blocks of audio whatever the duration. Setup asks for 8 GB of system RAM. The RAM watchdog of earlier versions is gone. Cancel stops a run cleanly; files already finished are kept.

Want to use ExpSoft Diarizer?

Get it on Patreon →