| Title: | Speaker Diarization with 'Nemotron 3 Diarization' in R 'torch' |
|---|---|
| Description: | Determines "who spoke when" in recorded or streaming audio with a native R 'torch' implementation of the 'NVIDIA' 'Nemotron 3 Diarization' model <https://huggingface.co/nvidia/Nemotron-3-Diarization>, a Streaming 'Sortformer' that tracks up to eight speakers in order of first arrival. Log-mel feature extraction, the transformer encoder and the arrival-order speaker cache are implemented in R; weights are downloaded from 'Hugging Face' with the 'hfhub' package on request. |
| Authors: | Troy Hernandez [aut, cre] (ORCID: <https://orcid.org/0009-0005-4248-604X>), cornball.ai [cph], The HuggingFace Team [cph] (portions ported from the transformers library (Apache-2.0); see inst/COPYRIGHTS) |
| Maintainer: | Troy Hernandez <[email protected]> |
| License: | Apache License (>= 2) |
| Version: | 0.1.0 |
| Built: | 2026-10-14 19:01:01 UTC |
| Source: | https://github.com/cran/n3d |
Finds who spoke when in a recording. Speakers are numbered from 1 in order of first arrival; up to eight are tracked.
diarize( audio, model = NULL, threshold = 0.5, probs = FALSE, sample_rate = 16000L )diarize( audio, model = NULL, threshold = 0.5, probs = FALSE, sample_rate = 16000L )
audio |
Path to an audio or video file, or a numeric vector of 16 kHz mono samples. |
model |
A model from |
threshold |
Speaker probability above which a frame counts as speech. |
probs |
Whether to also return the per-frame probabilities. |
sample_rate |
Sampling rate of numeric |
The recording is processed offline in 27.2 s chunks with 3.2 s of look-ahead, carrying the arrival-order speaker cache between chunks, so its length is not limited.
A data frame of segments with columns start, end
(seconds) and speaker. With probs = TRUE, a list of
segments and probs, a frames x 8 matrix with one frame per
10 ms.
if (n3d_exists()) { model <- load_n3d() mp3 <- system.file("samples", "Synapsis-Wonderland.mp3", package = "av") diarize(mp3, model) }if (n3d_exists()) { model <- load_n3d() mp3 <- system.file("samples", "Synapsis-Wonderland.mp3", package = "av") diarize(mp3, model) }
Downloads the Nemotron 3 Diarization weights (about 400 MB) into the Hugging Face cache with hfhub. In interactive sessions it asks for consent first. The weights are published by NVIDIA under the OpenMDW License 1.1.
download_n3d(revision = n3d_revision(), force = FALSE)download_n3d(revision = n3d_revision(), force = FALSE)
revision |
Hugging Face commit to download. |
force |
Download again even when the weights are cached. |
The path of the cached model.safetensors, invisibly.
if (interactive()) { download_n3d() }if (interactive()) { download_n3d() }
Decodes an audio or video file with av, downmixed to mono and resampled to 16 kHz.
load_audio(file)load_audio(file)
file |
Path to an audio or video file in any format FFmpeg reads. |
Numeric vector of samples in [-1, 1].
mp3 <- system.file("samples", "Synapsis-Wonderland.mp3", package = "av") samples <- load_audio(mp3) length(samples) / 16000 # secondsmp3 <- system.file("samples", "Synapsis-Wonderland.mp3", package = "av") samples <- load_audio(mp3) length(samples) / 16000 # seconds
Builds the network and loads the Nemotron 3 Diarization weights from the Hugging Face cache.
load_n3d( device = default_device(), download = FALSE, revision = n3d_revision() )load_n3d( device = default_device(), download = FALSE, revision = n3d_revision() )
device |
Torch device, |
download |
Whether to download the weights when they are not cached.
Defaults to |
revision |
Hugging Face commit to load. Defaults to the commit this package was validated against. |
On CUDA each encoder layer runs as a traced TorchScript graph,
which frees intermediate tensors as it goes (peak device memory about
0.45 GiB instead of about 2 GiB) and removes most per-operation R
overhead. Tracing and warming it up take about a second, once per
model. Set options(n3d.jit = FALSE) to run the plain R functions
instead. The CPU always runs them.
An n3d_model object: a list holding the network and its
configuration.
if (n3d_exists()) { model <- load_n3d(device = "cpu") }if (n3d_exists()) { model <- load_n3d(device = "cpu") }
Check Whether the Weights Are Cached
n3d_exists(revision = n3d_revision())n3d_exists(revision = n3d_revision())
revision |
Hugging Face commit to look for. |
TRUE if the model weights are in the Hugging Face cache.
n3d_exists()n3d_exists()
The Hugging Face commit of nvidia/Nemotron-3-Diarization that
load_n3d and download_n3d use by default.
n3d_revision()n3d_revision()
A 40-character commit hash.
n3d_revision()n3d_revision()
Opens a streaming session: push audio as it arrives and get speaker probabilities for each completed chunk. The speaker cache keeps speaker numbering consistent across the whole stream.
n3d_stream(model, mode = "low_latency")n3d_stream(model, mode = "low_latency")
model |
A model from |
mode |
Streaming mode: |
Latency is the audio a chunk waits for, its frames plus look-ahead:
1.04 s for "low_latency", 0.64 s for "very_low_latency" and
0.32 s for "ultra_low_latency", excluding compute time.
Every step re-encodes the speaker cache and FIFO queue (about 540 encoder frames) to score one chunk, so keeping up with live audio needs a GPU; on a CPU a step takes seconds.
An n3d_stream session.
n3d_stream_push, n3d_stream_finish
if (n3d_exists()) { model <- load_n3d() mp3 <- system.file("samples", "Synapsis-Wonderland.mp3", package = "av") samples <- load_audio(mp3) s <- n3d_stream(model) probs <- list() for (start in seq(1, length(samples), by = 8000)) { piece <- samples[start:min(start + 7999, length(samples))] probs[[length(probs) + 1]] <- n3d_stream_push(s, piece) } probs[[length(probs) + 1]] <- n3d_stream_finish(s) probs_to_segments(do.call(rbind, probs)) }if (n3d_exists()) { model <- load_n3d() mp3 <- system.file("samples", "Synapsis-Wonderland.mp3", package = "av") samples <- load_audio(mp3) s <- n3d_stream(model) probs <- list() for (start in seq(1, length(samples), by = 8000)) { piece <- samples[start:min(start + 7999, length(samples))] probs[[length(probs) + 1]] <- n3d_stream_push(s, piece) } probs[[length(probs) + 1]] <- n3d_stream_finish(s) probs_to_segments(do.call(rbind, probs)) }
Scores the remaining buffered audio, including the look-ahead frames of the previous chunk, and closes the session.
n3d_stream_finish(stream)n3d_stream_finish(stream)
stream |
A session from |
A matrix of speaker probabilities for the remaining frames.
n3d_stream for a complete example.
if (n3d_exists()) { s <- n3d_stream(load_n3d()) n3d_stream_push(s, numeric(32000)) dim(n3d_stream_finish(s)) }if (n3d_exists()) { s <- n3d_stream(load_n3d()) n3d_stream_push(s, numeric(32000)) dim(n3d_stream_finish(s)) }
Push Audio to a Stream
n3d_stream_push(stream, samples)n3d_stream_push(stream, samples)
stream |
A session from |
samples |
Numeric vector of 16 kHz mono samples. |
A matrix of speaker probabilities, frames x 8, for the frames completed by this push (possibly zero rows), one frame per 10 ms.
n3d_stream for a complete example.
if (n3d_exists()) { s <- n3d_stream(load_n3d()) p <- n3d_stream_push(s, numeric(32000)) # 2 s of silence dim(p) }if (n3d_exists()) { s <- n3d_stream(load_n3d()) p <- n3d_stream_push(s, numeric(32000)) # 2 s of silence dim(p) }
Thresholds per-frame speaker probabilities and returns one row per contiguous run of activity. Overlapping speech gives overlapping segments.
probs_to_segments(probs, threshold = 0.5, frame_duration = 0.01)probs_to_segments(probs, threshold = 0.5, frame_duration = 0.01)
probs |
Numeric matrix, frames x speakers, of speaker activity
probabilities (one frame per 10 ms), as returned by
|
threshold |
Probability above which a frame counts as speech. |
frame_duration |
Seconds per frame. |
A data frame with columns start and end (seconds)
and speaker (integer, numbered from 1 in order of first
arrival), sorted by start time then speaker.
p <- matrix(0, 300, 8) p[1:120, 1] <- 0.9 p[100:300, 2] <- 0.8 probs_to_segments(p)p <- matrix(0, 300, 8) p[1:120, 1] <- 0.9 p[100:300, 2] <- 0.8 probs_to_segments(p)