vocal_helper.nemotron module

Diarisation en flux par Nemotron 3, sans torch : onnxruntime et numpy.

Pourquoi ce module

Tout ce que ce dépôt a construit pour la diarisation en direct gratuite repose sur un regroupement d’empreintes orchestré autour d’un moteur différé. Le meilleur résultat obtenu est la qualité du différé atteinte en direct — taux d’erreur médian 0,176 sur AMI dev-slice — et deux limites qu’aucune orchestration ne lève : la fraîcheur, puisqu’une étiquette définitive arrive à la passe suivante, et le plafond du moteur lui-même.

Nemotron 3 Diarization n’a ni l’une ni l’autre. C’est un modèle de bout en bout qui tient son propre cache de locuteurs par ordre d’arrivée, donc il nomme à l’instant et ne révise rien. Mesuré ici sous le protocole du dépôt, sur un mixage dont la vérité est exacte par construction, il donne 0,090 de taux d’erreur avec une confusion de locuteurs de 0,000 et quatre étiquettes pour quatre locuteurs, là où le différé pyannote fait 0,299 et le meilleur étage en flux maison 0,171.

Pourquoi par ONNX plutôt que par la bibliothèque d’origine

La voie officielle passe par transformers et torch, et appelle un dépôt distant à l’exécution. Ici le graphe est un fichier du bundle de poids que le projet sert déjà, et il tourne sur onnxruntime, qui est déjà une dépendance de base par sherpa. Trois conséquences : aucun jeton ni appel réseau, pas de torch sur le chemin de décision, et le même fichier tourne depuis Rust par la caisse ort — ce que torch ne permet pas.

Une précision utile : onnxruntime n’a pas de moteur Metal. Sur Apple Silicon l’accélération passerait par CoreML, pas par MPS — mais la mesure la déconseille, CoreML ne prenant que 1193 nœuds sur 2827 et les découpant en 316 partitions, ce qui le rend 2,4 fois plus lent que le CPU seul et le place au-dessus du temps réel. Le défaut est donc le CPU, qui tient 1,9 fois le temps réel sur un portable.

Ce que ce module doit écrire lui-même

Le graphe est un pas de flux, pas un diariseur de fichier : il reçoit [cache | file | morceau] et rend des probabilités. La tenue du cache et de la file — quelles trames y entrent, lesquelles en sortent, comment le cache se comprime quand il déborde — vit autour du graphe, et c’est le portage de SortformerModules.streaming_update de NeMo. Tout ce portage est du numpy pur, donc il se vérifie sans modèle et sans audio.

class vocal_helper.nemotron.MelSettings(sample_rate=16000, n_fft=512, win_length=400, hop_length=160, n_mels=128, preemphasis=0.97, log_guard=5.960464477539063e-08)[source]

Bases: object

Les paramètres du front-end, tels que la configuration du modèle les donne.

Parameters:
sample_rate

16 kHz, imposé par le modèle.

Type:

int

n_fft

Taille de la transformée.

Type:

int

win_length

Longueur de la fenêtre d’analyse, plus courte que la transformée.

Type:

int

hop_length

Avance entre deux trames : 160 échantillons, soit 10 ms.

Type:

int

n_mels

Nombre de bandes.

Type:

int

preemphasis

Pré-accentuation appliquée avant tout découpage.

Type:

float

log_guard

Plancher ajouté avant le logarithme. 2**-24 est la valeur que l’extracteur officiel emploie, retrouvée en comparant les sorties.

Type:

float

hop_length: int = 160
log_guard: float = 5.960464477539063e-08
n_fft: int = 512
n_mels: int = 128
preemphasis: float = 0.97
sample_rate: int = 16000
win_length: int = 400
class vocal_helper.nemotron.NemotronOnnxDiar(graph=None, *, latency_s=1.04, mode=None, providers=None, threads=None, threshold=0.5)[source]

Bases: object

Diarisation en flux par le graphe Nemotron, sans torch.

Parameters:
  • graph (str or Path, optional) – Le fichier .onnx. None le cherche dans le bundle de poids, puis le télécharge une fois seul (resolve_graph()), sans jeton.

  • latency_s (float, optional) – Budget de latence, en secondes. C’est le seul réglage à connaître : le calendrier s’en déduit, en prenant le morceau le plus long que ce budget et le graphe permettent. Le défaut est DEFAULT_LATENCY_S, c’est-à-dire le calendrier publié à basse latence, parce qu’il est nettement meilleur dès qu’il y a de la réverbération — et une salle de réunion en a. None prend le plus long contexte viable, 6,4 s, qui coûte un tiers de calcul en moins et n’est à égalité qu’en salle anéchoïque.

  • mode (str, optional) – Un calendrier nommé de STREAMING_MODES, qui l’emporte sur latency_s quand il est donné. Utile pour reproduire exactement une configuration publiée. Ce n’est jamais celui que le graphe porte dans ses métadonnées : ce dernier est le calendrier hors ligne et il dépasse l’encodage positionnel tracé, voir MAX_SEQUENCE_FRAMES.

  • providers (list of str, optional) – Les moteurs d’exécution, par ordre de préférence. None prend CoreML s’il est disponible — c’est la voie du GPU et du moteur neuronal sur Apple Silicon, onnxruntime n’ayant pas de moteur Metal — puis le CPU, qui suffit de toute façon.

  • threads (int, optional) – Fils pour le moteur CPU. None — le défaut — laisse onnxruntime choisir, et c’est le meilleur choix : forcer huit fils sur une machine à huit cœurs performants mesure 0,953 de facteur temps réel contre 0,528 en laissant faire, soit presque deux fois pire. L’argument existe pour brider la consommation, pas pour accélérer.

  • threshold (float) – Seuil de parole appliqué aux probabilités à 10 ms.

diarize(pcm, sr, *, num_speakers=None, min_speakers=None, max_speakers=None)[source]

Diariser un tampon, en le faisant défiler comme un flux.

Les trois arguments de compte sont acceptés pour la parité d’interface avec les autres étages du paquet, et ignorés : ce modèle ordonne ses huit voies par ordre d’arrivée et n’a pas de compte à recevoir.

Parameters:
  • pcm (NDArray[np.float32]) – Tampon mono.

  • sr (int) – Sa fréquence, qui doit être 16 kHz.

  • num_speakers (int | None)

  • min_speakers (int | None)

  • max_speakers (int | None)

Returns:

[(t0, t1, locuteur), …] trié par début, la même forme que les autres étages de diarisation.

Return type:

list of tuple

load()[source]

Ouvrir la session tout de suite, comme les autres moteurs de diarisation.

vocal_helper.diar.OfflineDiarStage appelle load() sur chaque moteur avant le premier tampon. Sans cette méthode, backend="nemotron" (donc "auto" dès que le graphe est dans le bundle) levait AttributeError à la première diarisation hors ligne.

Return type:

None

locate()[source]

Le chemin du graphe : celui donné, sinon resolve_graph().

Raises:

FileNotFoundError – Si ni le bundle ni le fichier unique ne fournissent le moteur.

Return type:

Path

probabilities(pcm, sr)[source]

Les probabilités d’activité par locuteur, (T, 8) à 10 ms.

Ce que diarize() seuille. Leur maximum par trame est une détection de parole, mesurée contre Silero dans studies/vad_nemotron_vs_silero.py.

Parameters:
  • pcm (NDArray[np.float32]) – Tampon mono.

  • sr (int) – Sa fréquence, qui doit être 16 kHz.

Returns:

Une ligne par trame de 10 ms, une colonne par voie de locuteur.

Return type:

NDArray[np.float32]

property providers: list[str]

Les moteurs d’exécution que la session a réellement retenus.

async run(inbox, outbox)[source]

Consommer des segments voisés, les étiqueter, les republier.

Même contrat que les autres étages : un segment voisé entre, le même segment ressort avec son locuteur et son PCM, parce que la transcription en aval a besoin de l’audio. Le modèle n’est donc pas utilisé pour découper la parole — le détecteur d’activité l’a déjà fait — mais pour dire qui parle.

Le modèle est un modèle de flux : il veut de l’audio continu avec une horloge juste. Or la file ne porte que les segments voisés, donc les creux sont absents. L’étage reconstitue donc la ligne de temps en comblant les silences, sans quoi le cache de locuteurs verrait un montage et non une séance.

Un segment n’est publié que lorsque le modèle a dépassé sa fin : c’est la latence du calendrier choisi — 1,04 s par défaut — et elle est le prix de l’identité, pas un défaut d’implémentation.

Parameters:
  • inbox – Files de segments voisés puis diarisés.

  • outbox – Files de segments voisés puis diarisés.

Return type:

None

property schedule: NemotronSchedule

Le calendrier effectif, celui du graphe ou celui demandé.

class vocal_helper.nemotron.NemotronSchedule(num_speakers=8, emb_dim=512, n_mels=128, subsampling=8, upsample_factor=8, spkcache_len=264, fifo_len=40, chunk_len=340, chunk_right_context=40, spkcache_update_period=300, sil_threshold=0.2, pred_score_threshold=0.25, strong_boost_rate=0.75, weak_boost_rate=1.5, min_pos_scores_rate=0.5, scores_boost_latest=0.05, spkcache_sil_frames_per_spk=1)[source]

Bases: object

Le calendrier de flux, en trames de 80 ms.

Les valeurs vivent dans les métadonnées du graphe, donc elles ne sont pas recopiées ici : un moteur mis à jour apporte les siennes, et un écart entre le code et le fichier est impossible par construction.

Parameters:
  • num_speakers (int)

  • emb_dim (int)

  • n_mels (int)

  • subsampling (int)

  • upsample_factor (int)

  • spkcache_len (int)

  • fifo_len (int)

  • chunk_len (int)

  • chunk_right_context (int)

  • spkcache_update_period (int)

  • sil_threshold (float)

  • pred_score_threshold (float)

  • strong_boost_rate (float)

  • weak_boost_rate (float)

  • min_pos_scores_rate (float)

  • scores_boost_latest (float)

  • spkcache_sil_frames_per_spk (int)

num_speakers

Nombre maximal de locuteurs que le modèle peut distinguer.

Type:

int

emb_dim

Dimension des empreintes du cache et de la file.

Type:

int

n_mels

Bandes attendues en entrée.

Type:

int

subsampling

Rapport entre les trames de 10 ms et celles de 80 ms.

Type:

int

upsample_factor

Rapport inverse, pour les probabilités à 10 ms.

Type:

int

spkcache_len, fifo_len, chunk_len, chunk_right_context

Longueurs du cache, de la file, du morceau et de son contexte droit.

Type:

int

spkcache_update_period

Fréquence de rafraîchissement du cache.

Type:

int

sil_threshold, pred_score_threshold

Seuils de silence et de score.

Type:

float

strong_boost_rate, weak_boost_rate, min_pos_scores_rate

Les taux qui équilibrent le cache entre locuteurs.

Type:

float

scores_boost_latest

Prime donnée aux trames les plus récentes.

Type:

float

spkcache_sil_frames_per_spk

Créneaux de silence réservés par locuteur dans le cache.

Type:

int

chunk_len: int = 340
chunk_right_context: int = 40
emb_dim: int = 512
fifo_len: int = 40
classmethod from_metadata(metadata)[source]

Lire le calendrier des métadonnées du graphe.

Les clés inconnues sont ignorées et les absentes gardent leur défaut, pour qu’un export enrichi n’empêche pas le chargement.

Examples

>>> NemotronSchedule.from_metadata(
...     {"vernacula.diar.fifo_len": "64", "vernacula.diar.inconnu": "x"}
... ).fifo_len
64
Parameters:

metadata (dict[str, str])

Return type:

NemotronSchedule

property latency_s: float

Audio à attendre avant de traiter un morceau, en secondes.

Examples

>>> round(NemotronSchedule(chunk_len=9, chunk_right_context=4).latency_s, 3)
1.04
min_pos_scores_rate: float = 0.5
n_mels: int = 128
num_speakers: int = 8
pred_score_threshold: float = 0.25
scores_boost_latest: float = 0.05
sil_threshold: float = 0.2
spkcache_len: int = 264
spkcache_sil_frames_per_spk: int = 1
spkcache_update_period: int = 300
strong_boost_rate: float = 0.75
subsampling: int = 8
upsample_factor: int = 8
weak_boost_rate: float = 1.5
class vocal_helper.nemotron.StreamingState(spkcache, fifo, spkcache_preds=None, fifo_preds=None)[source]

Bases: object

Le cache de locuteurs et la file, entre deux pas de flux.

Parameters:
  • spkcache (ndarray[tuple[int, ...], dtype[float32]])

  • fifo (ndarray[tuple[int, ...], dtype[float32]])

  • spkcache_preds (ndarray[tuple[int, ...], dtype[float32]] | None)

  • fifo_preds (ndarray[tuple[int, ...], dtype[float32]])

spkcache

(T_cache, emb_dim) — ce que le flux a retenu des locuteurs.

Type:

NDArray[np.float32]

spkcache_preds

Les probabilités du cache, None tant qu’il n’a jamais été rempli.

Type:

NDArray[np.float32] or None

fifo

(T_fifo, emb_dim) — le contexte récent.

Type:

NDArray[np.float32]

fifo_preds

Ses probabilités.

Type:

NDArray[np.float32]

classmethod empty(schedule)[source]

Un état de départ : tout vide, rien de retenu.

Examples

>>> state = StreamingState.empty(NemotronSchedule())
>>> state.spkcache.shape, state.fifo.shape
((0, 512), (0, 512))
Parameters:

schedule (NemotronSchedule)

Return type:

StreamingState

fifo: ndarray[tuple[int, ...], dtype[float32]]
fifo_preds: ndarray[tuple[int, ...], dtype[float32]] = None
spkcache: ndarray[tuple[int, ...], dtype[float32]]
spkcache_preds: ndarray[tuple[int, ...], dtype[float32]] | None = None
vocal_helper.nemotron.boost_top_scores(scores, count, *, scale, offset=0.5)[source]

Relever les count meilleurs scores de chaque locuteur.

Appliqué deux fois par la compression, avec deux intensités : une forte, pour garantir à chaque locuteur un minimum de place dans le cache, et une faible, pour empêcher un bavard d’occuper tout l’espace. Les trames déjà écartées restent à moins l’infini.

Examples

>>> import numpy as np
>>> scores = np.array([[0.0], [1.0], [-np.inf]])
>>> boost_top_scores(scores, 1, scale=1.0)[:, 0].round(3).tolist()
[0.0, 1.693, -inf]
Parameters:
Return type:

ndarray[tuple[int, …], dtype[float64]]

vocal_helper.nemotron.compress_cache(embeddings, predictions, schedule, silence)[source]

Ramener un cache débordant à sa taille, en gardant ce qui compte.

On garde les trames dont les scores disent qu’elles portent de la parole propre, en réservant à chaque locuteur sa part et en laissant quelques créneaux de silence par locuteur. Les créneaux vides reçoivent l’empreinte de silence apprise que l’export transporte, et non la moyenne des trames silencieuses — c’est l’un des deux écarts de ce point de contrôle par rapport à Streaming Sortformer 2.x.

Parameters:
  • embeddings (NDArray[np.float32]) – (n, emb_dim), plus long que le cache.

  • predictions (NDArray[np.float32]) – (n, num_speakers), les probabilités correspondantes.

  • schedule (NemotronSchedule) – Les longueurs et les taux.

  • silence (NDArray[np.float32]) – L’empreinte de silence apprise, (emb_dim,).

Returns:

Le cache et ses probabilités, à la longueur voulue.

Return type:

tuple

Examples

Un cache de quatre trames ramené à deux garde les deux voix, une chacune, plutôt que les deux trames du locuteur le plus bavard.

>>> import numpy as np
>>> schedule = NemotronSchedule(num_speakers=2, emb_dim=3, spkcache_len=2,
...                             spkcache_sil_frames_per_spk=0)
>>> embs = np.array([[1., 0, 0], [1., 0, 0], [1., 0, 0], [0, 1., 0]], np.float32)
>>> preds = np.array([[0.9, 0.0], [0.9, 0.0], [0.9, 0.0], [0.0, 0.9]], np.float32)
>>> cache, kept = compress_cache(embs, preds, schedule, np.zeros(3, np.float32))
>>> cache.shape, [int(np.argmax(row)) for row in kept]
((2, 3), [0, 1])
vocal_helper.nemotron.disable_low_scores(predictions, scores, *, min_positive)[source]

Écarter du cache ce qui n’est pas de la parole propre.

Deux coups de balai. Ce qui n’est pas de la parole pour ce locuteur est écarté définitivement. Et ce qui a un score non positif — en général du recouvrement — l’est aussi, à condition que le locuteur dispose déjà d’assez de trames franches : sans cette condition, un locuteur qui ne parle jamais seul disparaîtrait du cache.

Examples

>>> import numpy as np
>>> preds = np.array([[0.9, 0.1], [0.4, 0.1]], dtype=np.float32)
>>> scores = np.zeros((2, 2))
>>> out = disable_low_scores(preds, scores, min_positive=99)
>>> bool(np.isneginf(out[1, 0])), bool(np.isneginf(out[0, 1]))
(True, True)
Parameters:
  • predictions (ndarray[tuple[int, ...], dtype[float32]])

  • scores (ndarray[tuple[int, ...], dtype[float64]])

  • min_positive (int)

Return type:

ndarray[tuple[int, …], dtype[float64]]

vocal_helper.nemotron.log_mel(audio, sample_rate, settings=None)[source]

Les trames log-mel que le graphe attend, (T, n_mels).

Deux conventions ont été retrouvées en comparant à l’extracteur officiel plutôt que devinées, et elles comptent toutes les deux.

La fenêtre d’analyse de 400 points est centrée dans la transformée de 512, et non collée au début. Mettre la fenêtre au début multiplie par seize l’écart aux sorties officielles — 0,530 au lieu de 0,033 — parce que le décalage de phase que cela introduit change le spectre.

Le plancher du logarithme est 2**-24, retrouvé en lisant le minimum des sorties officielles : −16,36, c’est-à-dire exactement son logarithme.

Au régime établi l’écart moyen aux sorties officielles est de 0,006 pour des valeurs d’écart-type 1,3, soit un demi pour cent. Les trames de bord diffèrent : l’extracteur officiel remplit sa dernière trame d’une valeur sentinelle, ce qu’il n’y a pas lieu de reproduire.

Parameters:
  • audio (NDArray[np.float32]) – Signal mono.

  • sample_rate (int) – Sa fréquence, qui doit être celle du modèle.

  • settings (MelSettings, optional) – Les paramètres, ceux du modèle par défaut.

Returns:

(T, n_mels), une trame toutes les 10 ms.

Return type:

NDArray[np.float32]

Raises:

ValueError – Si la fréquence d’échantillonnage n’est pas celle du modèle.

Examples

>>> import numpy as np
>>> frames = log_mel(np.zeros(16000, dtype=np.float32), 16000)
>>> frames.shape
(101, 128)

Le silence tombe sur le plancher du logarithme, partout pareil.

>>> bool(np.allclose(frames, np.log(2.0**-24)))
True
vocal_helper.nemotron.log_pred_scores(predictions, *, floor)[source]

Le score d’une trame pour chaque locuteur, à partir des probabilités.

Le score est conçu pour être élevé quand un seul locuteur parle avec confiance : il récompense la probabilité du locuteur considéré et pénalise celle des autres. C’est ce qui fait qu’un cache rempli par ces scores contient de la parole propre et non du recouvrement.

Examples

Un locuteur seul et sûr obtient un score positif ; deux locuteurs simultanés, des scores plus faibles.

>>> import numpy as np
>>> alone = log_pred_scores(np.array([[0.9, 0.05]], dtype=np.float32), floor=0.25)
>>> both = log_pred_scores(np.array([[0.9, 0.9]], dtype=np.float32), floor=0.25)
>>> bool(alone[0, 0] > both[0, 0])
True
Parameters:
  • predictions (ndarray[tuple[int, ...], dtype[float32]])

  • floor (float)

Return type:

ndarray[tuple[int, …], dtype[float64]]

vocal_helper.nemotron.probabilities_to_segments(probabilities, *, frame_s=0.01, threshold=0.5, bridge_s=0.5)[source]

Seuiller les probabilités par trame et recoller les miettes.

Deux locuteurs simultanés donnent deux segments qui se recouvrent, ce qui est voulu : le modèle sait le dire, et l’écraser perdrait l’information. Les segments d’un même locuteur séparés de moins de bridge_s sont réunis, parce qu’un silence de respiration n’est pas un changement de tour et qu’une transcription n’a rien à faire de fragments de trois dixièmes.

Parameters:
  • probabilities (NDArray[np.float32]) – (T, num_speakers) à une trame par frame_s.

  • frame_s (float) – Durée d’une trame. 10 ms pour la sortie haute résolution du graphe.

  • threshold (float) – Probabilité au-dessus de laquelle la trame compte comme parole.

  • bridge_s (float) – Silence en deçà duquel deux segments d’un même locuteur sont réunis.

Returns:

[(t0, t1, locuteur), …] trié par début.

Return type:

list of tuple

Examples

>>> import numpy as np
>>> probs = np.array([[0.9], [0.9], [0.1], [0.9]], dtype=np.float32)
>>> probabilities_to_segments(probs, bridge_s=0.0)
[(0.0, 0.02, 'S0'), (0.03, 0.04, 'S0')]

Le même signal, en recollant les silences courts :

>>> probabilities_to_segments(probs, bridge_s=0.5)
[(0.0, 0.04, 'S0')]
vocal_helper.nemotron.resolve_graph()[source]

Le graphe de diarisation : le bundle s’il le porte, sinon un fichier unique.

Ordre : nemotron3/ dans le bundle de poids, puis le cache $VH_CACHE_DIR/nemotron-onnx (celui du graphe ASR), rempli une fois depuis $VH_NEMOTRON_DIAR_URL ou DEFAULT_BASE_URL. Le téléchargement passe par os-helper : reprise, nouvelles tentatives, écriture atomique, et vérification de GRAPH_SHA256 sur l’adresse par défaut.

Raises:

FileNotFoundError – Si ni le bundle ni le fichier unique ne fournissent le graphe.

Return type:

Path

vocal_helper.nemotron.streaming_update(state, chunk_embeddings, predictions, schedule, silence)[source]

Faire entrer un morceau dans la file, et la file dans le cache.

Le portage de SortformerModules.streaming_update, pour un seul flux. Les probabilités reçues couvrent [cache | file | morceau] : on en découpe la part de la file et celle du morceau, le morceau rejoint la file, et ce que la file pousse dehors rejoint le cache — qu’on comprime s’il déborde.

Parameters:
  • state (StreamingState) – L’état précédent.

  • chunk_embeddings (NDArray[np.float32]) – (T_chunk, emb_dim) rendues par le graphe pour ce morceau, contexte droit déjà retiré.

  • predictions (NDArray[np.float32]) – (T_cache + T_fifo + T_chunk, num_speakers).

  • schedule (NemotronSchedule) – Les longueurs.

  • silence (NDArray[np.float32]) – L’empreinte de silence apprise.

Returns:

Le nouvel état, et les probabilités du seul morceau.

Return type:

tuple

Examples

La file se remplit d’abord sans rien pousser vers le cache.

>>> import numpy as np
>>> schedule = NemotronSchedule(num_speakers=2, emb_dim=2, spkcache_len=4, fifo_len=4)
>>> state = StreamingState.empty(schedule)
>>> embs = np.ones((2, 2), dtype=np.float32)
>>> preds = np.full((2, 2), 0.9, dtype=np.float32)
>>> state, chunk_preds = streaming_update(state, embs, preds, schedule,
...                                       np.zeros(2, np.float32))
>>> state.fifo.shape, state.spkcache.shape, chunk_preds.shape
((2, 2), (0, 2), (2, 2))