vocal_helper.growing module
Diariser le tampon entier, périodiquement : la qualité du différé, en direct.
Le raisonnement
Deux mesures se combinent mieux qu’il n’y paraît. D’une part l’identité voyage très bien par recouvrement temporel entre deux diarisations qui partagent de l’audio. D’autre part le diariseur différé coûte RTF 0,039 : une réunion d’une heure se diarise en deux minutes.
Un découpage en morceaux a besoin d’un amorçage synthétique pour transporter l’identité, parce qu’un locuteur peut être absent de la zone commune aux deux diarisations. Si la seconde passe porte sur tout l’audio depuis le début, ce cas disparaît : quiconque a parlé est dans la zone commune, qui couvre alors la quasi-totalité du signal. L’appariement devient presque exact, aucun son synthétique n’est nécessaire, et surtout la dernière passe est une diarisation du fichier entier — donc la qualité du différé, atteinte en direct.
Le coût se calcule et se mesure. Rafraîchir aux instants t, r·t,
r²·t… diarise au total un petit multiple de la durée, donc le coût reste
proportionnel à la séance et non à son carré : 2,6 à 3,3 fois l’audio mesuré,
pour un facteur temps réel de 0,10 à 0,16.
Ce que ça donne
Sur les huit réunions d’AMI dev-slice, le résultat final est exactement celui du différé pleine fenêtre, à la troisième décimale — taux d’erreur médian 0,176, le bon nombre de locuteurs, et le meilleur accord longue portée mesuré. Pour comparaison sur le même corpus : l’étage en ligne glouton donne 0,369 avec soixante-cinq étiquettes pour quatre locuteurs, et le regroupement incrémental 0,254 avec vingt-quatre.
Ce que ça coûte
Pas la qualité, la fraîcheur : la parole postérieure à la dernière passe
n’a pas encore d’étiquette faisant autorité. Un espacement géométrique rend ce
retard proportionnel à la séance — 0,375 fois sa durée à ratio=1.6 — ce qui
va pour vingt minutes et non pour deux heures ; max_period_s le borne au
prix d’un facteur temps réel plus élevé. Et les étiquettes se révisent :
c’est le prix de l’exactitude, et LabelRevision le dit à
l’appelant plutôt que de le cacher.
- class vocal_helper.growing.GrowingBufferDiarStage(backend='auto', policy=<factory>, offline=None, online=None, on_revision=None)[source]
Bases:
objectDiarisation en direct par re-diarisation périodique du tampon entier.
L’étage accumule l’audio et, aux instants que la politique fixe, le fait diariser en entier par un moteur différé. Les étiquettes de chaque passe sont rattachées à celles de la précédente par recouvrement temporel, de sorte que le lecteur voie des identités stables ; les changements sont rendus comme des révisions explicites.
- Parameters:
backend (str) – Moteur différé, passé à
OfflineDiarStage."auto"prend le meilleur installé.policy (GrowingBufferPolicy, optional) – Le calendrier des passes.
offline (object, optional) – Un diariseur déjà construit, qui doit exposer
diarize(pcm, sr) -> [(t0, t1, étiquette), …]. Sert aux tests et permet de brancher un autre moteur sans toucher à cette classe.online (object, optional) – L’étage en ligne qui donne l’étiquette provisoire, en une demi- seconde, le temps que la première passe arrive.
Nonene publie rien avant la première passe.on_revision (callable, optional) – Appelée pour chaque étiquette démentie par une passe. Sans elle, les corrections sont calculées et perdues.
Examples
Un diariseur de pacotille suffit à montrer le mécanisme : les étiquettes qu’il rend changent de nom à chaque passe, et l’étage les stabilise.
>>> import numpy as np >>> class Flighty: ... def __init__(self): self.calls = 0 ... def diarize(self, pcm, sr, **kw): ... self.calls += 1 ... tag = f"local{self.calls}" ... return [(0.0, len(pcm) / sr, tag)] >>> stage = GrowingBufferDiarStage( ... offline=Flighty(), policy=GrowingBufferPolicy(first_s=1.0, ratio=2.0) ... ) >>> segments = stage.diarize(np.zeros(16000 * 4, dtype=np.float32), 16000) >>> [label for _, _, label in segments] ['P0'] >>> stage.speaker_labels ['P0']
Trois passes ont eu lieu, et le locuteur n’a été découvert qu’une fois.
>>> stage.passes 3
- diarize(pcm, sr, *, num_speakers=None, min_speakers=None, max_speakers=None)[source]
Dérouler un tampon comme un direct, et rendre l’état final.
Utile pour mesurer : la dernière passe voit tout, donc le résultat est celui du différé pleine fenêtre, obtenu en ayant payé le prix du direct. Pour un vrai flux, appeler
refresh()aux instants queGrowingBufferPolicy.times()indique et lire les révisions.- Parameters:
- Returns:
[(t0, t1, locuteur), …], la même forme que les autres étages.- Return type:
- policy: GrowingBufferPolicy
- refresh(pcm, sr, **hints)[source]
Rediariser tout le tampon, et rendre ce que ça change.
- Parameters:
pcm (NDArray[np.float32]) – Tout l’audio depuis le début, pas seulement le nouveau.
sr (int) – Sa fréquence.
**hints – Transmis au moteur différé, par exemple
max_speakers.
- Returns:
Les passages dont l’étiquette change par rapport à ce qui avait été publié. Vide à la première passe, puisque rien n’avait été dit.
- Return type:
- async run(inbox, outbox)[source]
Consommer des segments voisés, publier aussitôt, corriger ensuite.
Même contrat que l’étage en ligne, donc c’est un remplacement direct : un consommateur qui lit
outboxn’a rien à changer. Ce qui est nouveau, c’est qu’une étiquette déjà publiée peut être démentie, eton_revisionle dit — un affichage qui ignore les révisions montre des étiquettes dont on lui a signalé qu’elles sont fausses.L’étage garde tout l’audio voisé, puisque chaque passe rediarise depuis le début : à 16 kHz en flottant simple cela fait 64 ko par seconde de parole, soit environ 230 Mo pour une heure de réunion. C’est le prix de l’exactitude, et il est borné par la durée de la séance.
- Parameters:
inbox – Files d’attente de segments voisés puis diarisés, comme pour les autres étages.
outbox – Files d’attente de segments voisés puis diarisés, comme pour les autres étages.
- Return type:
None
- class vocal_helper.growing.GrowingBufferPolicy(first_s=60.0, ratio=1.6, max_period_s=None, min_speaker_s=0.0, gap_s=0.8)[source]
Bases:
objectQuand rafraîchir, et jusqu’où laisser le retard grandir.
- Parameters:
- first_s
Instant de la première passe. En dessous, le diariseur n’a pas de quoi travailler et rendrait un seul locuteur.
- Type:
- ratio
Facteur d’espacement. 1,6 mesure un coût de 2,6 à 3,3 fois l’audio pour un retard valant 0,375 fois la séance ; descendre vers 1,2 raccourcit le retard et monte le coût.
- Type:
- max_period_s
Attente maximale entre deux passes.
Nonelaisse le retard croître avec la séance, ce qui va pour une réunion et non pour une journée.- Type:
float, optional
- min_speaker_s
Temps de parole minimal pour qu’une étiquette soit publiée. Écarte les locuteurs d’une seconde que le diariseur invente sur un bruit.
- Type:
- gap_s
Silence considéré comme une séparation de tours, pour récolter le bruit de fond qui reconstitue les creux entre segments voisés.
- Type:
- vocal_helper.growing.map_by_overlap(left, right)[source]
La correspondance un-pour-un entre deux diarisations, par temps partagé.
Les noms de grappes sont arbitraires et interchangeables ; ce qui doit être conservé d’une passe à l’autre, c’est qui est qui. C’est un appariement bijectif, donc résolu par algorithme hongrois et non par un argmax par étiquette : un argmax peut envoyer deux étiquettes de gauche sur la même à droite, ce qui fusionnerait deux locuteurs sans le dire.
- Parameters:
- Returns:
{étiquette de gauche: étiquette de droite}, seulement pour les paires qui partagent effectivement du temps.- Return type:
Examples
>>> map_by_overlap([(0.0, 2.0, "a"), (2.0, 4.0, "b")], ... [(0.0, 2.0, "y"), (2.0, 4.0, "x")]) {'a': 'y', 'b': 'x'}
Rien de partagé, rien d’apparié — plutôt qu’un rapprochement inventé.
>>> map_by_overlap([(0.0, 1.0, "a")], [(9.0, 10.0, "z")]) {}
- vocal_helper.growing.refresh_times(duration_s, *, first_s, ratio, max_period_s=None)[source]
Les instants de rafraîchissement, espacés géométriquement.
Un pas constant ferait croître le coût comme le carré de la durée, puisque chaque passe rediarise tout depuis le début. Un pas géométrique le garde proportionnel : la somme des préfixes reste un petit multiple de la durée totale.
max_period_sborne l’attente au prix d’un coût plus élevé, pour une séance longue où un retard proportionnel serait inacceptable.Examples
>>> refresh_times(100.0, first_s=10.0, ratio=2.0) [10.0, 20.0, 40.0, 80.0, 100.0]
Plus court que la première passe : une seule, à la fin.
>>> refresh_times(8.0, first_s=10.0, ratio=2.0) [8.0]
Avec un plafond, l’attente ne grandit plus indéfiniment.
>>> refresh_times(100.0, first_s=10.0, ratio=2.0, max_period_s=25.0) [10.0, 20.0, 40.0, 65.0, 90.0, 100.0]