Who spoke when — on your Mac.
NVIDIA's Nemotron 3 Diarization running natively on Apple Silicon with MLX. Up to 8 speakers, streaming or offline, recordings of any length. No PyTorch, no CUDA, no cloud.
$ python -m nemotron_diar_mlx meeting.wav
0.35 9.31 speaker_0
9.15 13.81 speaker_1
13.55 18.32 speaker_2
18.38 23.59 speaker_3
23.77 28.10 speaker_4
27.80 32.69 speaker_5
...
[ 0.2s] speaker 0: To celebrate the release of Nemotron 3 Diarization, we invited seven more open text-to-speech
voices to the party. I'm Chris, the Nemotron voice from NVIDIA.
[ 9.4s] speaker 1: Hi, everybody. Thanks for having me. I'm Jane, a pocket voice from QTI.
[13.8s] speaker 2: Nice to meet you, Chris and Jen. I'm Serena, the Quinn3 Voice.
[18.4s] speaker 3: Hey, I'm Adam from Kokoro. Just 82 million parameters, and I still made the guest list.
[23.7s] speaker 4: Hello, hello. Chatterbox here from Resemble AI. I brought the expressive energy.
[28.1s] speaker 5: Hi, I'm John, a Dio Voice from Nari Labs. Great crowd, we've got here already.
Real output on NVIDIA's 8-voice demo clip, transcribed with parakeet-mlx. See Who said what.
mlx, numpy and huggingface_hub.Install · Quick start · Who said what · Streaming · How it works · Performance · Accuracy · API · Weights · Limitations · Development · Credits
pip install git+https://github.com/9clg6/nemotron-diar-mlx
macOS on Apple Silicon, Python 3.10–3.14. The model (~400 MB) is downloaded on first use. MLX also runs on Linux
(pip install "mlx[cpu]"), which is handy for tests but slow.
Command line. Input is 16 kHz audio (ffmpeg -i input.m4a -ar 16000 -ac 1 meeting.wav):
python -m nemotron_diar_mlx meeting.wav # start end speaker_N
python -m nemotron_diar_mlx meeting.wav --json # {"num_speakers": 6, "segments": [...]}
python -m nemotron_diar_mlx meeting.wav --rttm # for DER scoring tools
python -m nemotron_diar_mlx meeting.wav --preset low_latency --min-duration-off 0.3
Python:
import mlx.core as mx
from nemotron_diar_mlx import Model
model = Model.from_pretrained("nvidia/Nemotron-3-Diarization", dtype=mx.bfloat16)
for seg in model.diarize(audio): # mono float32 NumPy array at 16 kHz
print(f"{seg.start:6.2f}–{seg.end:6.2f} speaker_{seg.speaker}")
probs = model(audio)["preds"] # (frames, 8): P(speaker active), one frame every 10 ms
Speakers are numbered in order of first appearance: speaker_0 speaks first.
Diarization tells you when each person talks; an ASR tells you what was said. words_to_turns joins the two: each
word goes to the speaker most active during that word, and consecutive words of one speaker become a turn. It works
with any ASR that returns word or token timestamps. Here with parakeet-mlx,
which is MLX-native too:
import mlx.core as mx
from parakeet_mlx import from_pretrained
from nemotron_diar_mlx import Model, tokens_to_words, words_to_turns
diar = Model.from_pretrained(dtype=mx.bfloat16)
probs = diar(audio)["preds"]
asr = from_pretrained("mlx-community/parakeet-tdt-0.6b-v3").transcribe("meeting.wav")
words = tokens_to_words(tok for sentence in asr.sentences for tok in sentence.tokens)
for turn in words_to_turns(words, probs, diar.frame_duration):
print(f"[{turn.start:5.1f}s] speaker {turn.speaker}: {turn.text}")
With another ASR, build Word(start, end, text) objects yourself, keeping a leading space at word boundaries.
The model processes audio in chunks. Each chunk is encoded together with a compact speaker cache (the most representative frames of every speaker heard so far) and a short FIFO of recent context. Memory stays constant, speaker IDs stay consistent for hours, and you choose the latency:
| Preset | Latency¹ | Use it for |
|---|---|---|
offline (default) | 30.4 s | recordings: best accuracy |
low_latency | 1.04 s | live captions |
very_low_latency | 0.64 s | live, tighter |
ultra_low_latency | 0.32 s | real-time voice agents |
¹ Input buffer latency = (chunk + look-ahead) × 80 ms, as on the
model card; compute time not included. The exact parameters
are in nemotron_diar_mlx.STREAMING_PRESETS.
import queue
import sounddevice as sd # pip install sounddevice
from nemotron_diar_mlx import Model, StreamingDiarizer
model = Model.from_pretrained(streaming="low_latency")
diar = StreamingDiarizer(model)
blocks = queue.Queue()
# Keep the audio callback light: just hand the samples over.
with sd.InputStream(samplerate=16000, channels=1, dtype="float32",
callback=lambda data, *_: blocks.put(data[:, 0].copy())):
while True:
out = diar.feed(blocks.get()) # any block size
for i, frame in enumerate(out.preds): # frames finalized since the last call, 10 ms each
t = (out.start_frame + i) * diar.frame_duration
speaking = [k for k, p in enumerate(frame) if p > 0.5]
...
Feeding a recording in pieces gives the same result as processing it at once (bit-identical on CPU; within ~1e-6 on the GPU, whose kernels round slightly differently with the block size).
Need diarization from another process or language? A small WebSocket server ships with the package:
pip install "nemotron-diar-mlx[server]"
python -m nemotron_diar_mlx.server --port 8001 --preset low_latency
GET /health answers 200 once the model is loaded and warmed up. Each connection to ws://127.0.0.1:8001/v1/diarize
is one session:
{"type": "flush"} at the end;{"type": "ready", "frame_duration": 0.01, "num_speakers": 8, "latency": 1.04} on connect,
{"type": "frames", "start_frame": n, "probs": [[...], ...]} as frames are finalized (one row of per-speaker
probabilities per 10 ms, counted from the first sample sent), and {"type": "done", "num_frames": n} after the flush.The server queues audio while a chunk is computing and processes the backlog in one go, so a slow step delays the
labels but never drops audio. --parent-pid makes it exit together with the app that launched it.
flowchart LR
A["16 kHz audio"] --> B["128-band log-mel<br/>(10 ms frames)"]
B --> C["stack 8 frames<br/>→ 80 ms tokens"]
C --> D["31 × Transformer block<br/>pre-norm · RoPE · GELU<br/>d=512 · 8 heads"]
D --> E["project 512→192<br/>sub-pixel upsample ×8"]
E --> F["8 sigmoid outputs<br/>P(speaker k active)<br/>every 10 ms"]
In streaming, each step encodes [speaker cache | FIFO | new chunk] with full attention, then updates the state:
flowchart LR
S["speaker cache<br/>≤ 264 frames"] --> E["encoder + heads"]
Q["FIFO<br/>recent frames"] --> E
N["new chunk"] --> E
E --> O["probabilities<br/>for the chunk"]
E -- "frames leaving the FIFO" --> U["cache compression:<br/>keep each speaker's most<br/>confident, non-overlapped frames"]
U --> S
The cache is ordered by each speaker's first arrival (the Arrival-Order Speaker Cache of Streaming Sortformer), so output channel k always means the k-th person to speak: no clustering, no permutation step.
| File | Contents |
|---|---|
model.py | encoder, Sortformer heads, loading, offline inference |
streaming.py | speaker cache, FIFO, compression, StreamingDiarizer |
features.py | log-mel features, one-shot and incremental |
segments.py | probabilities → segments (NeMo's post-processing) |
align.py | words → speaker turns |
convert.py | Hugging Face / .nemo → MLX weights |
Measured by the benchmark workflow on GitHub's macos-14 runner, a virtualized
base Apple M1, with 10 minutes of real multi-speaker speech fed in 0.5 s blocks:
| Preset | dtype | Speed | 10 min of audio in | Peak memory |
|---|---|---|---|---|
offline | bfloat16 | 120× real time | 5.0 s | 0.52 GB |
offline | float32 | 69× real time | 8.7 s | 1.03 GB |
low_latency | bfloat16 | 2.5× real time | 242 s | 0.54 GB |
low_latency | float32 | 2.9× real time | 209 s | 0.88 GB |
An hour-long meeting takes about 30 seconds to diarize on the slowest Apple Silicon chip, in about half a gigabyte.
Low-latency streaming keeps ahead of real time on average (median feed of 0.5 s of audio: ~0.1 s, p95 ~0.4 s). On
this shared virtual machine, however, about 1 % of feed calls took over a second and a handful took 5–8 s, spread
over the whole run, so measure on your hardware before relying on it for live captions.
Newer chips (M2–M4, Pro/Max) are faster.
Run it on your own Mac with python scripts/benchmark.py (needs soundfile and scipy).
The port is validated against NVIDIA's own implementation (NeMo, PyTorch) on the real checkpoint with
scripts/check_parity.py. Results on 98 s of real 8-voice audio, offline preset, float32:
| Check | Result |
|---|---|
Final segments vs NeMo's diarize() | identical: 29 segments, 6 speakers, 0.000 s boundary shift |
| Speaker probabilities, streaming | max error 9e-6; 0 of 78,080 speaker/frame decisions differ |
| Speaker probabilities, offline | max error 4e-6 |
| Speaker-cache compression | bit-identical, including exactly tied scores |
Weights from Hugging Face vs .nemo | 355 / 355 tensors bit-identical |
In bfloat16 the same audio gives the same speakers with segment boundaries within 30 ms. Unit tests run in CI on Linux (CPU) and on Apple Silicon (GPU).
SortformerEncLabelModel.diarize.A note on ties. Cache compression keeps the highest-scoring frames with torch.topk, which does not specify which
of several equal values it keeps. This port breaks ties deterministically (oldest frame first), and the parity script
imposes the same rule on NeMo. Two implementations can still round a near-tie differently and drift slightly afterwards:
on synthetic audio with the training-time configuration we saw 3e-4 on one chunk. NeMo itself is not guaranteed to
agree with itself across CPU and GPU there.
Model.from_pretrained(source="nvidia/Nemotron-3-Diarization", dtype=mx.float32, streaming="offline", cache_dir=None) | Load from a Hugging Face repo id (repo@revision works), a local Hugging Face folder, a .nemo file or a converted folder. |
model(audio) -> {"preds", "activity"?} | Speaker probabilities (frames, 8), one frame every model.frame_duration seconds. |
model.diarize(audio, onset=0.5, offset=0.5, pad_onset=0, pad_offset=0, min_duration_on=0, min_duration_off=0) | list[Segment(start, end, speaker)], using NeMo's post-processing. |
model.set_streaming(preset=None, **params) | Switch preset, or set chunk_len, chunk_right_context, fifo_len, spkcache_len, spkcache_update_period. |
model.forward_offline(audio) | Full attention over the whole input. Short clips only: memory grows with length². |
StreamingDiarizer(model).feed(block) / .flush() | Incremental diarization, returning StreamingOutput(preds, activity, start_frame); .latency is the buffer latency in seconds. |
speaker_segments(probs, frame_duration, ...) | Probabilities → segments. |
tokens_to_words(tokens), words_to_turns(words, probs, frame_duration) | Who said what. |
Model.from_pretrained() downloads model.safetensors, config.json and processor_config.json from
nvidia/Nemotron-3-Diarization and caches converted MLX weights
in ~/.cache/nemotron-diar-mlx (override with cache_dir= or NEMOTRON_DIAR_MLX_CACHE). To convert explicitly, for
example to ship weights with an app:
python -m nemotron_diar_mlx.convert nvidia/Nemotron-3-Diarization ./nemotron3-mlx --dtype bfloat16
The NeMo checkpoint is supported as well (requires PyTorch):
pip install "nemotron-diar-mlx[nemo] @ git+https://github.com/9clg6/nemotron-diar-mlx"
python -m nemotron_diar_mlx.convert Nemotron-3-Diarization.nemo ./nemotron3-mlx
Both sources give bit-identical network weights. The .nemo file also contains an auxiliary activity head
(silence / one speaker / overlap, returned as out["activity"]), and stores its mel filterbank in bfloat16, whereas
the Hugging Face path rebuilds it exactly, as used in training.
forward_offline attends over the whole input at once; use the default streaming path for anything long.git clone https://github.com/9clg6/nemotron-diar-mlx && cd nemotron-diar-mlx
pip install -e ".[test]" && pytest # fast, no weights needed (on Linux: pip install "mlx[cpu]")
# Parity with NeMo on the real checkpoint (needs nemo_toolkit[asr]; CPU is fine):
python scripts/check_parity.py Nemotron-3-Diarization.nemo audio_16k.wav --preset offline
# ...or on random weights with the same architecture, without downloading anything:
python scripts/make_random_nemo.py <NeMo>/examples/speaker_tasks/diarization/conf/neural_diarizer/sortformer_streaming_8spk.yaml random.nemo
python scripts/check_parity.py random.nemo
Issues and pull requests are welcome.
Code licensed under Apache-2.0; see NOTICE. This is an independent project, not affiliated with NVIDIA or Apple.
Who spoke when — on your Mac.
NVIDIA's Nemotron 3 Diarization running natively on Apple Silicon with MLX. Up to 8 speakers, streaming or offline, recordings of any length. No PyTorch, no CUDA, no cloud.
$ python -m nemotron_diar_mlx meeting.wav
0.35 9.31 speaker_0
9.15 13.81 speaker_1
13.55 18.32 speaker_2
18.38 23.59 speaker_3
23.77 28.10 speaker_4
27.80 32.69 speaker_5
...
[ 0.2s] speaker 0: To celebrate the release of Nemotron 3 Diarization, we invited seven more open text-to-speech
voices to the party. I'm Chris, the Nemotron voice from NVIDIA.
[ 9.4s] speaker 1: Hi, everybody. Thanks for having me. I'm Jane, a pocket voice from QTI.
[13.8s] speaker 2: Nice to meet you, Chris and Jen. I'm Serena, the Quinn3 Voice.
[18.4s] speaker 3: Hey, I'm Adam from Kokoro. Just 82 million parameters, and I still made the guest list.
[23.7s] speaker 4: Hello, hello. Chatterbox here from Resemble AI. I brought the expressive energy.
[28.1s] speaker 5: Hi, I'm John, a Dio Voice from Nari Labs. Great crowd, we've got here already.
Real output on NVIDIA's 8-voice demo clip, transcribed with parakeet-mlx. See Who said what.
mlx, numpy and huggingface_hub.Install · Quick start · Who said what · Streaming · How it works · Performance · Accuracy · API · Weights · Limitations · Development · Credits
pip install git+https://github.com/9clg6/nemotron-diar-mlx
macOS on Apple Silicon, Python 3.10–3.14. The model (~400 MB) is downloaded on first use. MLX also runs on Linux
(pip install "mlx[cpu]"), which is handy for tests but slow.
Command line. Input is 16 kHz audio (ffmpeg -i input.m4a -ar 16000 -ac 1 meeting.wav):
python -m nemotron_diar_mlx meeting.wav # start end speaker_N
python -m nemotron_diar_mlx meeting.wav --json # {"num_speakers": 6, "segments": [...]}
python -m nemotron_diar_mlx meeting.wav --rttm # for DER scoring tools
python -m nemotron_diar_mlx meeting.wav --preset low_latency --min-duration-off 0.3
Python:
import mlx.core as mx
from nemotron_diar_mlx import Model
model = Model.from_pretrained("nvidia/Nemotron-3-Diarization", dtype=mx.bfloat16)
for seg in model.diarize(audio): # mono float32 NumPy array at 16 kHz
print(f"{seg.start:6.2f}–{seg.end:6.2f} speaker_{seg.speaker}")
probs = model(audio)["preds"] # (frames, 8): P(speaker active), one frame every 10 ms
Speakers are numbered in order of first appearance: speaker_0 speaks first.
Diarization tells you when each person talks; an ASR tells you what was said. words_to_turns joins the two: each
word goes to the speaker most active during that word, and consecutive words of one speaker become a turn. It works
with any ASR that returns word or token timestamps. Here with parakeet-mlx,
which is MLX-native too:
import mlx.core as mx
from parakeet_mlx import from_pretrained
from nemotron_diar_mlx import Model, tokens_to_words, words_to_turns
diar = Model.from_pretrained(dtype=mx.bfloat16)
probs = diar(audio)["preds"]
asr = from_pretrained("mlx-community/parakeet-tdt-0.6b-v3").transcribe("meeting.wav")
words = tokens_to_words(tok for sentence in asr.sentences for tok in sentence.tokens)
for turn in words_to_turns(words, probs, diar.frame_duration):
print(f"[{turn.start:5.1f}s] speaker {turn.speaker}: {turn.text}")
With another ASR, build Word(start, end, text) objects yourself, keeping a leading space at word boundaries.
The model processes audio in chunks. Each chunk is encoded together with a compact speaker cache (the most representative frames of every speaker heard so far) and a short FIFO of recent context. Memory stays constant, speaker IDs stay consistent for hours, and you choose the latency:
| Preset | Latency¹ | Use it for |
|---|---|---|
offline (default) | 30.4 s | recordings: best accuracy |
low_latency | 1.04 s | live captions |
very_low_latency | 0.64 s | live, tighter |
ultra_low_latency | 0.32 s | real-time voice agents |
¹ Input buffer latency = (chunk + look-ahead) × 80 ms, as on the
model card; compute time not included. The exact parameters
are in nemotron_diar_mlx.STREAMING_PRESETS.
import queue
import sounddevice as sd # pip install sounddevice
from nemotron_diar_mlx import Model, StreamingDiarizer
model = Model.from_pretrained(streaming="low_latency")
diar = StreamingDiarizer(model)
blocks = queue.Queue()
# Keep the audio callback light: just hand the samples over.
with sd.InputStream(samplerate=16000, channels=1, dtype="float32",
callback=lambda data, *_: blocks.put(data[:, 0].copy())):
while True:
out = diar.feed(blocks.get()) # any block size
for i, frame in enumerate(out.preds): # frames finalized since the last call, 10 ms each
t = (out.start_frame + i) * diar.frame_duration
speaking = [k for k, p in enumerate(frame) if p > 0.5]
...
Feeding a recording in pieces gives the same result as processing it at once (bit-identical on CPU; within ~1e-6 on the GPU, whose kernels round slightly differently with the block size).
Need diarization from another process or language? A small WebSocket server ships with the package:
pip install "nemotron-diar-mlx[server]"
python -m nemotron_diar_mlx.server --port 8001 --preset low_latency
GET /health answers 200 once the model is loaded and warmed up. Each connection to ws://127.0.0.1:8001/v1/diarize
is one session:
{"type": "flush"} at the end;{"type": "ready", "frame_duration": 0.01, "num_speakers": 8, "latency": 1.04} on connect,
{"type": "frames", "start_frame": n, "probs": [[...], ...]} as frames are finalized (one row of per-speaker
probabilities per 10 ms, counted from the first sample sent), and {"type": "done", "num_frames": n} after the flush.The server queues audio while a chunk is computing and processes the backlog in one go, so a slow step delays the
labels but never drops audio. --parent-pid makes it exit together with the app that launched it.
flowchart LR
A["16 kHz audio"] --> B["128-band log-mel<br/>(10 ms frames)"]
B --> C["stack 8 frames<br/>→ 80 ms tokens"]
C --> D["31 × Transformer block<br/>pre-norm · RoPE · GELU<br/>d=512 · 8 heads"]
D --> E["project 512→192<br/>sub-pixel upsample ×8"]
E --> F["8 sigmoid outputs<br/>P(speaker k active)<br/>every 10 ms"]
In streaming, each step encodes [speaker cache | FIFO | new chunk] with full attention, then updates the state:
flowchart LR
S["speaker cache<br/>≤ 264 frames"] --> E["encoder + heads"]
Q["FIFO<br/>recent frames"] --> E
N["new chunk"] --> E
E --> O["probabilities<br/>for the chunk"]
E -- "frames leaving the FIFO" --> U["cache compression:<br/>keep each speaker's most<br/>confident, non-overlapped frames"]
U --> S
The cache is ordered by each speaker's first arrival (the Arrival-Order Speaker Cache of Streaming Sortformer), so output channel k always means the k-th person to speak: no clustering, no permutation step.
| File | Contents |
|---|---|
model.py | encoder, Sortformer heads, loading, offline inference |
streaming.py | speaker cache, FIFO, compression, StreamingDiarizer |
features.py | log-mel features, one-shot and incremental |
segments.py | probabilities → segments (NeMo's post-processing) |
align.py | words → speaker turns |
convert.py | Hugging Face / .nemo → MLX weights |
Measured by the benchmark workflow on GitHub's macos-14 runner, a virtualized
base Apple M1, with 10 minutes of real multi-speaker speech fed in 0.5 s blocks:
| Preset | dtype | Speed | 10 min of audio in | Peak memory |
|---|---|---|---|---|
offline | bfloat16 | 120× real time | 5.0 s | 0.52 GB |
offline | float32 | 69× real time | 8.7 s | 1.03 GB |
low_latency | bfloat16 | 2.5× real time | 242 s | 0.54 GB |
low_latency | float32 | 2.9× real time | 209 s | 0.88 GB |
An hour-long meeting takes about 30 seconds to diarize on the slowest Apple Silicon chip, in about half a gigabyte.
Low-latency streaming keeps ahead of real time on average (median feed of 0.5 s of audio: ~0.1 s, p95 ~0.4 s). On
this shared virtual machine, however, about 1 % of feed calls took over a second and a handful took 5–8 s, spread
over the whole run, so measure on your hardware before relying on it for live captions.
Newer chips (M2–M4, Pro/Max) are faster.
Run it on your own Mac with python scripts/benchmark.py (needs soundfile and scipy).
The port is validated against NVIDIA's own implementation (NeMo, PyTorch) on the real checkpoint with
scripts/check_parity.py. Results on 98 s of real 8-voice audio, offline preset, float32:
| Check | Result |
|---|---|
Final segments vs NeMo's diarize() | identical: 29 segments, 6 speakers, 0.000 s boundary shift |
| Speaker probabilities, streaming | max error 9e-6; 0 of 78,080 speaker/frame decisions differ |
| Speaker probabilities, offline | max error 4e-6 |
| Speaker-cache compression | bit-identical, including exactly tied scores |
Weights from Hugging Face vs .nemo | 355 / 355 tensors bit-identical |
In bfloat16 the same audio gives the same speakers with segment boundaries within 30 ms. Unit tests run in CI on Linux (CPU) and on Apple Silicon (GPU).
SortformerEncLabelModel.diarize.A note on ties. Cache compression keeps the highest-scoring frames with torch.topk, which does not specify which
of several equal values it keeps. This port breaks ties deterministically (oldest frame first), and the parity script
imposes the same rule on NeMo. Two implementations can still round a near-tie differently and drift slightly afterwards:
on synthetic audio with the training-time configuration we saw 3e-4 on one chunk. NeMo itself is not guaranteed to
agree with itself across CPU and GPU there.
Model.from_pretrained(source="nvidia/Nemotron-3-Diarization", dtype=mx.float32, streaming="offline", cache_dir=None) | Load from a Hugging Face repo id (repo@revision works), a local Hugging Face folder, a .nemo file or a converted folder. |
model(audio) -> {"preds", "activity"?} | Speaker probabilities (frames, 8), one frame every model.frame_duration seconds. |
model.diarize(audio, onset=0.5, offset=0.5, pad_onset=0, pad_offset=0, min_duration_on=0, min_duration_off=0) | list[Segment(start, end, speaker)], using NeMo's post-processing. |
model.set_streaming(preset=None, **params) | Switch preset, or set chunk_len, chunk_right_context, fifo_len, spkcache_len, spkcache_update_period. |
model.forward_offline(audio) | Full attention over the whole input. Short clips only: memory grows with length². |
StreamingDiarizer(model).feed(block) / .flush() | Incremental diarization, returning StreamingOutput(preds, activity, start_frame); .latency is the buffer latency in seconds. |
speaker_segments(probs, frame_duration, ...) | Probabilities → segments. |
tokens_to_words(tokens), words_to_turns(words, probs, frame_duration) | Who said what. |
Model.from_pretrained() downloads model.safetensors, config.json and processor_config.json from
nvidia/Nemotron-3-Diarization and caches converted MLX weights
in ~/.cache/nemotron-diar-mlx (override with cache_dir= or NEMOTRON_DIAR_MLX_CACHE). To convert explicitly, for
example to ship weights with an app:
python -m nemotron_diar_mlx.convert nvidia/Nemotron-3-Diarization ./nemotron3-mlx --dtype bfloat16
The NeMo checkpoint is supported as well (requires PyTorch):
pip install "nemotron-diar-mlx[nemo] @ git+https://github.com/9clg6/nemotron-diar-mlx"
python -m nemotron_diar_mlx.convert Nemotron-3-Diarization.nemo ./nemotron3-mlx
Both sources give bit-identical network weights. The .nemo file also contains an auxiliary activity head
(silence / one speaker / overlap, returned as out["activity"]), and stores its mel filterbank in bfloat16, whereas
the Hugging Face path rebuilds it exactly, as used in training.
forward_offline attends over the whole input at once; use the default streaming path for anything long.git clone https://github.com/9clg6/nemotron-diar-mlx && cd nemotron-diar-mlx
pip install -e ".[test]" && pytest # fast, no weights needed (on Linux: pip install "mlx[cpu]")
# Parity with NeMo on the real checkpoint (needs nemo_toolkit[asr]; CPU is fine):
python scripts/check_parity.py Nemotron-3-Diarization.nemo audio_16k.wav --preset offline
# ...or on random weights with the same architecture, without downloading anything:
python scripts/make_random_nemo.py <NeMo>/examples/speaker_tasks/diarization/conf/neural_diarizer/sortformer_streaming_8spk.yaml random.nemo
python scripts/check_parity.py random.nemo
Issues and pull requests are welcome.
Code licensed under Apache-2.0; see NOTICE. This is an independent project, not affiliated with NVIDIA or Apple.