09a1c98514
- 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>
51 lines
2.1 KiB
Python
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'})")
|