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>
This commit is contained in:
Hitonabi
2026-07-02 10:29:33 +02:00
parent aff0105700
commit 09a1c98514
49 changed files with 8231 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
"""Deutsche Zahlen-/Uhrzeit-Normalisierung für Lucys Stimme (num2words).
Eigenständig (nur re + num2words, KEIN torch/pocket_tts-Import), damit schnell testbar und von
pocket_server importierbar.
Kernidee: Zahlen im Fließtext verbalisieren ("18:01" -> "achtzehn Uhr eins", "16 GB" -> "sechzehn
GB", "4.5" -> "vier Komma fünf"), aber Modell-/Versionsnummern SCHÜTZEN ("RX 9070 XT", "Qwen3.6",
"H100" bleiben roh — die soll man nicht als Kardinalzahl lesen). Abschaltbar via LUCY_NUM_NORM=0.
"""
import os
import re
try:
from num2words import num2words as _n2w
except Exception: # Lib fehlt -> No-op (Stimme läuft trotzdem)
_n2w = None
# AUS per Default (2026-07-01, empirisch via Whisper-Rücktranskription verifiziert): Pocket german_24l
# spricht ROHE Ziffern ("21 Uhr 40", "16 GB", "4.5 GHz", "9:30") sauber. Vorverbalisierte deutsche
# Kompositzahlen ("einundzwanzig") ZERBRICHT das Modell zu Kauderwelsch ("ein Mund", "ein Aus-20").
# Also NICHT verbalisieren. Mit LUCY_NUM_NORM=1 wieder anschaltbar (falls je ein Modell es braucht).
NUM_NORM = os.environ.get("LUCY_NUM_NORM", "0") not in ("0", "false", "False")
_MONTHS = "Januar Februar März April Mai Juni Juli August September Oktober November Dezember".split()
_MONTH_RX = "|".join(_MONTHS)
# FULLY-uppercase 2+-Einheiten, neben denen Zahlen VERBALISIERT werden (sechzehn GB) — im Gegensatz
# zu Modell-Akronymen (RX/XT/RTX), wo die Ziffern roh bleiben (RX 9070 XT). Gemischt-Case-Einheiten
# (GHz/MHz/kg/km/Grad/Prozent) werden von der Akronym-Regel eh nicht erfasst -> dort wird ohnehin verbalisiert.
_CAPS_UNITS = {"GB", "TB", "MB", "KB", "PB", "EB", "KW", "MW"}
# Garble-Kandidaten (Whisper-verifiziert): Pocket german_24l kann diese Akronyme/Codes NICHT sauber
# sprechen ("ROCm"->"AOC HM", "UTC"->Kauderwelsch) -> Ersatz durch gesprochene/buchstabierte Form.
# Erweiterbar via LUCY_ACRONYM_EXTRA="ABC=A B C,XYZ=..." (neue Fälle findet transcribe.py).
_ACRONYM_FIX = {
"ROCm": "Rockem", "UTC": "U T C", "GMT": "G M T",
"CEST": "C E S T", "CET": "C E T", "MESZ": "M E S Z", "MEZ": "M E Z",
"NVMe": "N V M E", "PCIe": "P C I E",
}
def _build_acr():
m = dict(_ACRONYM_FIX)
for pair in os.environ.get("LUCY_ACRONYM_EXTRA", "").split(","):
if "=" in pair:
k, v = pair.split("=", 1)
if k.strip():
m[k.strip()] = v.strip()
return m
_ACR_MAP = _build_acr()
_ACR_RX = re.compile(r"\b(" + "|".join(re.escape(k) for k in sorted(_ACR_MAP, key=len, reverse=True)) + r")\b") if _ACR_MAP else None
ACR_ON = os.environ.get("LUCY_ACRONYM", "1") not in ("0", "false", "False") # A/B-Abschaltung
def fix_acronyms(text: str) -> str:
"""Garble-anfällige Akronyme durch gesprochene Form ersetzen (ROCm -> Rockem, UTC -> U T C)."""
return _ACR_RX.sub(lambda m: _ACR_MAP[m.group(0)], text) if (_ACR_RX and ACR_ON) else text
def _card(v) -> str:
try:
return _n2w(int(v), lang="de")
except Exception:
return str(v)
def normalize_numbers(text: str) -> str:
if not NUM_NORM or _n2w is None:
return text
masks: list[str] = []
def _mask(s: str) -> str:
masks.append(s)
return f"\x00{len(masks) - 1}\x00"
# 1) Uhrzeiten: "18:01 Uhr"/"18:01" -> "18 Uhr 01", "18:00" -> "18 Uhr" (Ziffern verbalisiert Schritt 6)
text = re.sub(r"\b(\d{1,2}):00(\s*Uhr)?\b", r"\1 Uhr", text)
text = re.sub(r"\b(\d{1,2}):(\d{2})\s*Uhr\b", r"\1 Uhr \2", text)
text = re.sub(r"\b(\d{1,2}):(\d{2})\b", r"\1 Uhr \2", text)
# 2) Datums-Ordinalzahlen: "3. Januar" -> "dritter Januar"
def _ord(m):
try:
return f"{_n2w(int(m.group(1)), to='ordinal', lang='de')} {m.group(2)}"
except Exception:
return m.group(0)
text = re.sub(rf"\b(\d{{1,2}})\.\s+({_MONTH_RX})\b", _ord, text)
# 3) Modell-/Versions-Token (Buchstabe UND Ziffer, evtl. mit Punkten: Qwen3.6, H100, v2.1, RTX4090)
# -> ganzes Token maskieren (nie verbalisieren). Satz-Endpunkt/-Komma bleibt draußen.
def _mask_model(m):
tok = m.group(0); trail = ""
while tok and tok[-1] in ".,":
trail = tok[-1] + trail; tok = tok[:-1]
if re.search(r"[A-Za-zÄÖÜäöü]", tok) and re.search(r"\d", tok):
return _mask(tok) + trail
return m.group(0)
text = re.sub(r"[A-Za-zÄÖÜäöü0-9]+(?:[.,][A-Za-zÄÖÜäöü0-9]+)*", _mask_model, text)
# 4) Standalone-Dezimalzahl (nur Ziffern, kein Buchstabe): "4.5" -> "vier Komma fünf"
# (Nachkommastellen ziffernweise, wie im Deutschen üblich).
def _dec(m):
return f"{_card(m.group(1))} Komma {' '.join(_card(d) for d in m.group(2))}"
text = re.sub(r"(?<![\w\x00])(\d+)[.,](\d+)(?![\w\x00])", _dec, text)
# 5) Zahl neben ALL-CAPS-Token: Einheit (GB/TB..) -> verbalisieren; Modell-Akronym (RX/XT) -> roh maskieren.
def _by_ctx(num, acr):
return num if acr.upper() in _CAPS_UNITS else _mask(num)
text = re.sub(r"\b([A-ZÄÖÜ]{2,})\s+(\d{1,6})\b", lambda m: f"{m.group(1)} {_by_ctx(m.group(2), m.group(1))}", text)
text = re.sub(r"\b(\d{1,6})\s+([A-ZÄÖÜ]{2,})\b", lambda m: f"{_by_ctx(m.group(1), m.group(2))} {m.group(2)}", text)
# 6) verbleibende reine Ganzzahlen (0..9999) verbalisieren; größere roh lassen (IDs/Codes)
def _c(m):
v = int(m.group(0))
return _n2w(v, lang="de") if v <= 9999 else m.group(0)
text = re.sub(r"(?<![\w\x00])\d{1,4}(?![\w\x00])", _c, text)
# 7) Masken zurückholen
text = re.sub(r"\x00(\d+)\x00", lambda m: masks[int(m.group(1))], text)
return text