# -*- coding: utf-8 -*- """Stimm-RÖNTGEN: liest Wörter + Wort-genaue Zeiten + PAUSEN + Prosodie (F0/Tempo) aus einer WAV. So sieht man beim Tunen objektiv, wo Pocket zu lange Pausen macht (Komma!), nuschelt, oder monoton wird. Nutzung: python analyze.py """ import sys, numpy as np, soundfile as sf, librosa from faster_whisper import WhisperModel p = sys.argv[1] a, sr = sf.read(p) a = np.asarray(a, dtype=np.float32).reshape(-1) dur = len(a) / sr w = WhisperModel("small", device="cpu", compute_type="int8") segs, _ = w.transcribe(p, language="de", beam_size=5, word_timestamps=True) words = [x for s in segs for x in (s.words or [])] txt = " ".join(x.word.strip() for x in words) print(f"=== {p.split(chr(92))[-1]} ===") print(f"Dauer {dur:.2f}s | {len(words)} Wörter | Tempo {len(words)/max(dur,0.01):.1f} Wörter/s") print(f"TEXT: {txt}\n") print("WORT-TIMING & PAUSE danach:") pauses = [] for i, x in enumerate(words): gap = (words[i + 1].start - x.end) if i + 1 < len(words) else 0.0 flag = " <=== LANG" if gap >= 0.30 else (" <- Pause" if gap >= 0.15 else "") if gap >= 0.15: pauses.append((x.word.strip(), gap)) print(f" {x.start:5.2f}-{x.end:5.2f} {x.word.strip():16} Pause: {gap*1000:4.0f}ms{flag}") # Rand-Stille env = np.abs(a) > 0.015 lead = (np.argmax(env) / sr * 1000) if env.any() else 0 tail = ((len(a) - 1 - np.argmax(env[::-1])) if env.any() else len(a)) tail_ms = (len(a) - tail) / sr * 1000 # Prosodie: F0 (Tonhöhe) über stimmhafte Frames f0 = librosa.yin(a, fmin=80, fmax=400, sr=sr, frame_length=1024) f0v = f0[(f0 > 90) & (f0 < 380)] if f0v.size: med = float(np.median(f0v)); rng = float(np.percentile(f0v, 90) - np.percentile(f0v, 10)) else: med = rng = 0.0 print(f"\nPAUSEN gesamt: {len(pauses)} (>=150ms) | längste: " + (", ".join(f'nach „{w_}\": {g*1000:.0f}ms' for w_, g in sorted(pauses, key=lambda t: -t[1])[:4]) or "keine")) print(f"RAND-STILLE: vorne {lead:.0f}ms, hinten {tail_ms:.0f}ms") print(f"TONHÖHE: median {med:.0f}Hz, Spanne {rng:.0f}Hz ({'monoton' if rng < 40 else 'lebendig' if rng > 90 else 'ok'})")