HPR4746: Monotone

Bron: Hacker Public RadioOpenen bij originele bron ↗
Door Lee12/10/2026 às 00:0012 weergaven
Foto: CC BY-SA / Hacker Public Radio
🎙 Lee · Hacker Public Radio

Deze uitzending is door de uitzending als "Clean" geclassificeerd.

Python en zijn bibliotheken

1. librosa — laden en herhalen van audio

```
def load_audio(path):
 y, sr = librosa.load(path, sr=SR, mono=True)
 return y, sr
```

functie load_audio(pad):
 y, sr = librosa.load(pad, sr=SR, mono=True)
 return y, sr

Elke aflevering, ongeacht van de oorspronkelijke sample rate of het aantal kanalen, wordt geladen als een enkele mono stream met een frequentie van 16 kHz (SR = 16000). Dit is één regel van librosa, resampling en het omzetten van stereo naar mono. Hierdoor konden alle daaropvolgende tools (VAD, pitch tracking, MFCC extractie) consistent en voorspelbaar werken, ongeacht welke MP3 encoder of podcast host oorspronkelijk was gebruikt.

2. parselmouth (Praat bindings) — spraak- en stemkwaliteit volgen

def pitch_en_hnr(y, sr):
 snd = parselmouth.Sound(y, sampling_frequency=sr)
 pitch = snd.to_pitch(time_step=PITCH_TIME_STEP, pitch_floor=PITCH_FLOOR, pitch_ceiling=PITCH_CEILING)
 f0 = pitch.selected_array["frequency"]
 times = pitch.xs()
 try:
 harmonicity = snd.to_harmonicity_cc(time_step=PITCH_TIME_STEP)
 hnr = np.array([harmonicity.get_value(t) for t in times])
 hnr = np.nan_to_num(hnr, nan=-999.0)
 exceptException:
 hnr = np.full_like(f0, 999.0) # faalt open als HNR extractie fouten
 return f0, hnr, times

parselmouth is een Python-wrapper rondom Praat, het standaard hulpmiddel in de fonetische onderzoek. Dit is het hart van de hele meting van monotone stem: to_pitch() geeft de fundamentele frequentie (F0, ongeveer "toonhoogte") elke 20 ms, en to_harmonicity_cc() geeft het verhouding van harmonie tot ruis (HNR) op dezelfde tijdstippen — een signaalkwaliteitsscore die later wordt gebruikt om toonhoogte-schattingen uit ruis of slecht gecomprimeerde audio te verwijderen, in plaats van slechte audio als "platte" toonhoogte te laten lijken.

3. webrtcvad — bepalen welke frames daadwerkelijk spraak bevatten

def vad_speech_mask(y, sr, agressiviteit=2):
 vad = webrtcvad.Vad(agressiviteit)
 pcm16 = (np.clip(y, -1.0, 1.0) * 32767).astype(np.int16)
 n_frames = len(pcm16) // VAD_FRAME_LEN
 mask = np.zeros(n_frames, dtype=bool)
 for i in range(n_frames):
 chunk = pcm16[i * VAD_FRAME_LEN:(i + 1) * VAD_FRAME_LEN]
 raw = struct.pack("%dh" % len(chunk), *chunk)
 try:
 mask[i] = vad.is_speech(raw, sr)
 except Exception:
 mask[i] = False
 return mask, times

webrtcvad is de spraakdetectie-module uit het WebRTC-project van Google — een frame-per-frame classifier ("is deze 30ms chunk spraak of niet"). Het heeft rauwe 16-bit PCM-bytes nodig, niet floats, daarom worden de conversies met `np.clip` en `struct.pack` uitgevoerd.

4. librosa.feature — muziek onderscheiden van spraak

flatness = librosa.feature.spectral_flatness(y=y, hop_length=hop)[0]
chroma = librosa.feature.chroma_stft(y=y, sr=sr, hop_length=hop)
onset_env = librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop)
...
chroma_diff = np.mean(np.linalg.norm(np.diff(chroma_win, axis=1), axis=0))
chroma_stability = 1.0 / (1.0 + chroma_diff)
...
scores[w] = 0.4 * chroma_stability + 0.35 * perioditeit + 0.25 * min(flat_mean * 3, 1.0)

Podcast intro's/andere zijn vaak muziek, en muziek heeft een heel ander frequentieprofiel dan spraak — het opnemen ervan zou de meting van "monotone" beïnvloeden. In plaats van een classifier te trainen, werden drie librosa-kenmerken gecombineerd met handmatig afgestelde gewichten: spectrale vlakheid (muziek heeft doorgaans meer tonale structuur, minder ruisachtig spectrum), chroma-stabiliteit (aanhoudende harmonie versus

(constantly-changing speech formants) en onset-periodicitet (een regelmatige ritme versus de onregelmatige ritme van spraak).

5. librosa.feature.mfcc + sklearn.cluster.KMeans + sklearn.metrics.silhouette_score — een poging tot onafhankelijke telling van sprekers

mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13, hop_length=hop)
 ...
 chunk_vecs.append(np.concatenate([seg.mean(axis=1), seg.std(axis=1)]))
 ...
 X = np.array(chunk_vecs)
 X = (X - X.mean(axis=0)) / (X.std(axis=0) + 1e-9)
 
 best_k, best_score = 1, -1.0
 for k in range(2, min(MAX_SPEAKERS, len(X) - 1) + 1):
 labels = KMeans(n_clusters=k, n_init=10, random_state=0).fit_predict(X)
 if len(set(labels)) < 2:
 continue
 score = silhouette_score(X, labels)
 if score > best_score:
 best_k, best_score = k, score
 
 estimated = best_k if best_score >= SILHOUETTE_MIN else 1

MFCC's (Mel-frequentie cepstraal coëfficiënten) geven een samenvatting van de vorm van het spectrum van een korte audiogedeelte — een ruwe "stemafdruk" die veel wordt gebruikt in spraaktaken. Het idee: elke episode in 2-seconden segmenten verdelen, elk segment omzetten in een afdrukvector, de kenmerken standaardiseren (met behulp van een z-score), en vervolgens KMeans gebruiken om te zien of de segmenten van nature in 2+ clusters (verschillende stemmen) worden verdeeld, in plaats van 1 (één stem doorheen). silhouette_score meet hoe goed de clusters gescheiden zijn, en kiest de beste k — of keert terug naar "1 spreker" als geen clustering een minimale kwaliteitstreshold haalt. Dit deel van het onderzoek werkte niet, omdat het ruis als een andere spreker oppikte.

6. scipy.stats.mannwhitneyu + een handgeschreven Cliff's delta — vergelijken van twee groepen zonder aanname van een normale verdeling

def cliffs_delta(a, b):
 a, b = np.asarray(a), np.asarray(b)
 gt =sum((x > y) for x in a for y in b)
 lt =sum((x < y) for x in a for y in b)
 return (gt - lt) / (len(a) *len(b))
 

 def compare_group(df, metric, label):
 hpr = df[df.corpus =="hpr"][metric].dropna()
 cmp= df[df.corpus =="comparison"][metric].dropna()
 u, p = stats.mannwhitneyu(hpr, cmp, alternative="two-sided")
 delta = cliffs_delta(hpr.values, cmp.values) iflen(hpr) *len(cmp) <200000elseNone

scipy.stats.mannwhitneyu is een statistische test: het vergelijkt twee groepen getallen (HPR vs. vergelijkingspodcasts, per metric) zonder aan te nemen dat ze normaal verdeeld zijn — geschikt voor scheve, real-world data zoals metingen van pitch-bereik. SciPy levert geen maat voor de effectgrootte, dus Cliff's delta — "van alle mogelijke (HPR-aflevering, vergelijkingsaflevering) paren, hoe vaak is de waarde van HPR kleiner?" Dit paar (een significantietest plus een maat voor de effectgrootte) werd gedurende het hele proces gebruikt, omdat een lage p-waarde alleen niet zegt of het verschil daadwerkelijk groot genoeg is om relevant te zijn.

7. pandas — groeperen, stratificeren en kruistabelen van resultaten

print(df.groupby("corpus")["retained_fraction"].describe())
 ...
 print(pd.crosstab(df["corpus"], df["register"]))

Zodra de data/results.jsonl (één JSON-object per geanalyseerde aflevering) is geladen in een pandas.DataFrame, beantwoorden twee regels twee verschillende vragen die anders aangepaste code vereisten: .groupby(...).describe() geeft een samenvattende beschrijving (aantal, gemiddelde, standaarddeviatie, kwartielen) van de audio-kwaliteit per corpus, in één keer, en pd.crosstab() maakt een kruistabel van HPR versus vergelijking met mannelijk/vrouwelijk typisch register — een snelle controle om te verifiëren dat de twee collecties van podcasts een vergelijkbare mix van gender-register hadden.

8. matplotlib — box plots voor de vergelijking van de hoofding

fig, ax = plt.subplots(figsize=(6, 4))
 data = [df[(df.corpus == c) & df[metric].notna()][metric].values for c in ["hpr", "comparison"]]
 ax.boxplot(data, tick_labels=["HPR", "Comparison"], showmeans=True)

Een boxplot per pitch-range metric, HPR, vergeleken met vergelijkbare podcasts, met de gemiddelde waarde en de media aangegeven.

9. SpaCy woordvectoren — het meten van onderwerpverschuiving, niet alleen woordkeuze

nlp = get_nlp() # spacy.load("en_core_web_md")
 ...
 chunk_docs = [nlp(c) for c in chunks]
 chunk_vecs = []
 for d in chunk_docs:
 v = d.vector
 norm = np.linalg.norm(v)
 if norm > 1e-6:
 chunk_vecs.append(v / norm)
 ...
 adj_sims = [float(np.dot(V[i], V[i + 1])) for i in range(n - 1)]

Elke ~130-woorden lange transcriptie wordt als een enkele vector weergegeven met behulp van het medium Engelse model van spaCy (en_core_web_md, dat ingebouwde woordvectoren heeft), en vervolgens wordt deze L2-genormaliseerd, zodat een dot product tussen twee vectorenishun cosine similariteit. Het vergelijken van de similariteit tussen opeenvolgende stukken geeft een score van "beweegt het onderwerp nauwelijks van het ene stuk naar het andere".

10. lexicale rijkdom — woordenschat diversiteit die niet de lengte van de transcript manipuleert

lr = LexicalRichness(tekst)
try:
 mtld = lr.mtld(drempel=0.72)
except Exception:
 mtld = None

Een naïeve score voor woorddiversiteit (unieke woorden ÷ totale woorden) wordt slechter naarmate een tekst langer wordt, omdat de metric op deze manier is gedefinieerd – niet omdat de spreker daadwerkelijk vaker herhaalt.

MTLD (Measure of Textual Lexical Diversity) voorkomt dit door de tekst te doorzoeken en te tellen hoeveel woorden het kost voordat de diversiteit onder een drempel komt, en vervolgens het gemiddelde te berekenen — een metriek die specifiek is ontworpen om vergelijkbaar te blijven tussen een aflevering van 10 minuten en een aflevering van 40 minuten.

11. vaderSentiment — een sentimentscore gebaseerd op een lexicon

vader = get_vader() # SentimentIntensityAnalyzer()
 sentimenten = [vader.polarity_scores(c)["compound"] for c in chunks]
 ...
 "sentiment_std": float(np.std(sentiments)) als len(sentiments) > 1 anders None,

VADER scoreert de sentiment van elke chunk van -1 tot +1 met behulp van een handmatig samengestelde lexicon en regelset (geen neuraal netwerk) — gekozen vanwege hun snelheid, determinisme en kostenefficiëntie, zodat ze per chunk kunnen worden uitgevoerd over honderden afleveringen. De waarde die wordt gebruikt is niet het gemiddelde sentiment, maar destandaarddeviatievan stemming van emoties over de verschillende segmenten van een aflevering: veroorzaakt de emotionele

tone, of course, or does it stay flat throughout?

12. zlib (standaardbibliotheek) — compressieveratio als maat voor entropie

def compressie_ratio(tekst):
 raw = tekst.encode("utf-8")
 if len(raw) < 200:
 return None
 compressed = zlib.compress(raw, level=9)
 return len(compressed) / len(raw)

Nenhuma bibliotheek van derder NLP werd gebruikt — alleen het standaardbibliotheekmodule zlib. Een compressor in de stijl van gzip zoekt en maakt gebruik van herhaalde patronen; als deze een transcript aanzienlijk kan comprimeren, betekent dit dat het transcript relatief repetitief en voorspelbaar was, en als het nauwelijks kan worden gecomprimeerd, betekent dit dat de formulering meer divers was. Dit geeft ruwweg een indicatie van "verbale monotonie".

13. re (standaardbibliotheek) — het omzetten van "voedt HPR voorkeur voor enkele hosts" in een controleerbare heuristiek

MULTI_PRESENTER_PATTERNS = [
 r"\bco-?host(?:s|ed|ing)?\b",
 r"\bguest\b",
 r"\binterview(?:s|ed|ing)?\b",
 r"\btalks? with\b",
 r"\bjoins? me\b",
 ...
 ]
 CORRESPONDENT_LINK_RE = re.compile(r'correspondents/(\d+)\.html')
 
 def multi_presenter_score(summary, notes):
 text = (summary or"") +" "+ (notes or"")
 hits = [p for p in MULTI_PRESENTER_PATTERNS if re.search(p, text, re.IGNORECASE)]
 summary_links =set(CORRESPONDENT_LINK_RE.findall(summary or""))
 return {
 "flagged_multi_presenter": bool(hits) or len(summary_links) >1,
 }

In plaats van de poging om de akoestische luidsprekerclustering (item 5, die onbetrouwbaar bleek), leverde een betrouwbaardere aanwijzing een eenvoudige tekst: de eigen afleveringsmetadata van HPR (afkomstig van een lokale kennisbank) bevatten samenvattingen geschreven door de presentator, en een korte lijst met reguliere expressievormen ("gast", "interview", "sluit aan", enz.), plus het tellen van gelinkte correspondentprofielen, was voldoende om waarschijnlijk afleveringen met meerdere presentatoren te identificeren.

14. multiprocessing.Pool — het uitvoeren van dezelfde analyse over honderden bestanden zonder honderden keren te wachten

work = [(r, local_paths[r["id"]]) for r in batch]
 met behulp van mp.Pool(args.workers) als pool en open(args.out, "a") als out_f:
 voor resultaat in pool.imap_unordered(_worker, work):
 out_f.write(json.dumps(resultat) +"\n")
 out_f.flush()

Elke batch pipeline (akoestische toonanalyse, pass voor sprekercount) gebruikte dezelfde vorm: Pool(workers).imap_unordered(...) om de functie per aflevering parallel uit te voeren over meerdere CPU-cores, waarbij de resultaten gestreamd werden naar een JSON-lines-bestand zodra ze klaar waren, in plaats van te wachten tot de hele batch. Imap_unordered werd gekozen boven map om ervoor te zorgen dat een enkele langzame aflevering de resultaten niet kon vertragen die al klaar waren.

15. subprocess + een samenvattende batchlus — werken binnen een schijfruimtebeperking

def load_done_ids(results_path):
 als results_path niet bestaat:
 return set()
 done = set()
 met open(results_path) als f:
 voor lijn in f:
 ...
 done.add(json.loads(lijn)["id"])
 return done
 ...
 rijen = [r for r in rijen als r["id"] niet in done]
 ...
 voor batch_start in range(0, len(rijen), args.batch_size):
 batch = rijen[batch_start:batch_start + args.batch_size]
 lokale_paden = fetch_batch(batch, args.scratch) # scp van de remote host
 ...
 voor p in lokale_paden.waarden():
 os.remove(p) # verwijder voor de volgende batch

Alle podcasts (ongeveer 220 GB) pasten niet op de lokale schijf (ongeveer 54 GB vrij). De oplossing was een lus: scp (via subprocess) een batch audio in, analyseer deze, verwijder deze, ga naar de volgende batch. load_done_ids() maakt de hele workflow opnieuw opstartbaar door de informatie die al in het outputbestand staat, opnieuw te lezen, zodat een crash of een opzettelijke stop halverwege geen werk verloren gaat.

Links

Ja, de boeiende wereld van Hacker Public Radio: een charmant hoekje waar tech-wonderen ✨️ elkaars magische geheimen ✨️ in de meest boeiende monotone toon ooit onthullen. Wie had gedacht dat eindeloze "community"-podcasts zo... absoluut boeiend kunnen zijn? 📻️🎙️ Het lijkt erop dat iedereen elk jaar graag wil bijdragen aan deze kakofonie—wat een privilege! 😂
https://hackerpublicradio.org/
Ja, de boeiende wereld van Hacker Public Radio: een charmant hoekje waar tech-wonderen ✨️ elkaars magische geheimen ✨️ in de meest boeiende monotone toon ooit onthullen. Wie had gedacht dat eindeloze "community"-podcasts zo... absoluut boeiend kunnen zijn? 📻️🎙️ Het lijkt erop dat iedereen elk jaar graag wil bijdragen aan deze kakofonie—wat een privilege! 😂

Geeffeedbackin dit aflevering.

Bron
Hacker Public Radio
Origineel openen ↗
Was dit nieuws nuttig?

Opmerkingen 0

Seja o primeiro a contribuir com o debate.

Difunda suas informações e promova seu argumento

Aarzel niet om nuttige informatie te delen.

Om deel te nemen aan het debat, log in of maak een gratis account aan.