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:
objectLes paramètres du front-end, tels que la configuration du modèle les donne.
- Parameters:
- log_guard
Plancher ajouté avant le logarithme.
2**-24est la valeur que l’extracteur officiel emploie, retrouvée en comparant les sorties.- Type:
- class vocal_helper.nemotron.NemotronOnnxDiar(graph=None, *, latency_s=1.04, mode=None, providers=None, threads=None, threshold=0.5)[source]
Bases:
objectDiarisation en flux par le graphe Nemotron, sans torch.
- Parameters:
graph (str or Path, optional) – Le fichier
.onnx.Nonele 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.Noneprend 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 surlatency_squand 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é, voirMAX_SEQUENCE_FRAMES.providers (list of str, optional) – Les moteurs d’exécution, par ordre de préférence.
Noneprend 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:
- Returns:
[(t0, t1, locuteur), …]trié par début, la même forme que les autres étages de diarisation.- Return type:
- load()[source]
Ouvrir la session tout de suite, comme les autres moteurs de diarisation.
vocal_helper.diar.OfflineDiarStageappelleload()sur chaque moteur avant le premier tampon. Sans cette méthode,backend="nemotron"(donc"auto"dès que le graphe est dans le bundle) levaitAttributeErrorà 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:
- 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 dansstudies/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]
- 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:
objectLe 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)
- spkcache_len, fifo_len, chunk_len, chunk_right_context
Longueurs du cache, de la file, du morceau et de son contexte droit.
- Type:
- sil_threshold, pred_score_threshold
Seuils de silence et de score.
- Type:
- strong_boost_rate, weak_boost_rate, min_pos_scores_rate
Les taux qui équilibrent le cache entre locuteurs.
- Type:
- 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:
- Return type:
- class vocal_helper.nemotron.StreamingState(spkcache, fifo, spkcache_preds=None, fifo_preds=None)[source]
Bases:
objectLe cache de locuteurs et la file, entre deux pas de flux.
- Parameters:
- spkcache
(T_cache, emb_dim)— ce que le flux a retenu des locuteurs.- Type:
NDArray[np.float32]
- spkcache_preds
Les probabilités du cache,
Nonetant 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:
- vocal_helper.nemotron.boost_top_scores(scores, count, *, scale, offset=0.5)[source]
Relever les
countmeilleurs 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]
- 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:
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)
- 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
- 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_ssont 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 parframe_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:
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_URLouDEFAULT_BASE_URL. Le téléchargement passe par os-helper : reprise, nouvelles tentatives, écriture atomique, et vérification deGRAPH_SHA256sur l’adresse par défaut.- Raises:
FileNotFoundError – Si ni le bundle ni le fichier unique ne fournissent le graphe.
- Return type:
- 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:
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))