Files
mission-control-v2/client/lucy-tts/analyze.py
T
Hitonabi 09a1c98514 Lucy-TTS/F5: Skripte + Batches versionieren, schwere Assets ignoriert
- pocket_server.py (Produktions-TTS mit Stimmen-Waechter), text_norm, Bench-/Diag-Skripte
- lucy-f5: f5_server/f5_test/bench_dml (DirectML-Experiment, Phase C/D offen)
- .gitignore: venvs/Modelle/Audio/Logs der beiden Ordner + box_recon/gemma_swap-Scratch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 10:29:33 +02:00

51 lines
2.1 KiB
Python

# -*- 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 <datei.wav>
"""
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'})")