Machine-fault-detector / tts_text.py
muruganmath's picture
Add trained LoRA adapter (main), tokenizer/processor, lineage, standalone inference and model card
36ba721 verified
Raw History Blame Contribute Delete
4.21 kB
"""Make Whisper output speakable for a character-level TTS (MMS-VITS has no digits).
"Alarm F007 on line 3, VFD tripped" -> "alarm eff zero zero seven on line three, vee eff dee tripped"
Codes (letters+digits) and digit strings with leading zeros are read digit by digit,
which is how operators read alarm and part numbers; plain numbers < 10000 are read
as words; ALL-CAPS acronyms are spelled out.
"""
from __future__ import annotations
import re
_ONES = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
"eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen",
"nineteen"]
_TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]
_SYMBOLS = {"%": " percent", "&": " and ", "+": " plus ", "°": " degrees ", "#": " number ",
"/": " slash ", "@": " at "}
def _under_100(n: int) -> str:
if n < 20:
return _ONES[n]
t, o = divmod(n, 10)
return _TENS[t] + ("" if o == 0 else " " + _ONES[o])
def number_to_words(n: int) -> str:
if n < 100:
return _under_100(n)
if n < 1000:
h, r = divmod(n, 100)
return _ONES[h] + " hundred" + ("" if r == 0 else " " + _under_100(r))
th, r = divmod(n, 1000)
return _under_100(th) + " thousand" + ("" if r == 0 else " " + number_to_words(r))
# Letter names: a character-level TTS reads a lone "f" as a sound, not as the letter
# (measured: "f zero zero seven" came back from Whisper as "007").
_LETTERS = {"a": "ay", "b": "bee", "c": "see", "d": "dee", "e": "ee", "f": "eff", "g": "jee", "h": "aitch",
"i": "eye", "j": "jay", "k": "kay", "l": "el", "m": "em", "n": "en", "o": "oh", "p": "pee",
"q": "cue", "r": "ar", "s": "ess", "t": "tee", "u": "you", "v": "vee", "w": "double you",
"x": "ex", "y": "why", "z": "zed"}
def _spell(letters: str) -> str:
return " ".join(_LETTERS[ch] for ch in letters.lower())
def _digits(s: str) -> str:
return " ".join(_ONES[int(d)] for d in s)
def _read_number(s: str) -> str:
if len(s) > 1 and s.startswith("0") or len(s) > 4:
return _digits(s)
return number_to_words(int(s))
def _token(tok: str) -> str:
if re.fullmatch(r"\d+(\.\d+)?", tok):
whole, _, frac = tok.partition(".")
out = _read_number(whole)
return out + (" point " + _digits(frac) if frac else "")
if re.search(r"\d", tok) and re.search(r"[A-Za-z]", tok): # code like F007, M6x20, PN-4471A
parts = re.findall(r"[A-Za-z]+|\d+", tok)
return " ".join(_spell(p) if p.isalpha() and len(p) <= 3 else
(p.lower() if p.isalpha() else _digits(p)) for p in parts)
if re.fullmatch(r"[A-Z]{2,5}s?", tok): # VFD, PLC, HMIs
letters = tok[:-1] if tok.endswith("s") else tok
return _spell(letters) + ("s" if tok.endswith("s") else "")
if re.fullmatch(r"\d+", re.sub(r"[^\d]", "", tok) or "x") and "-" in tok: # 12-34
return " ".join(_digits(p) for p in tok.split("-") if p)
return tok
def speakable(text: str) -> str:
for sym, rep in _SYMBOLS.items():
text = text.replace(sym, rep)
out = []
for tok in re.findall(r"[A-Za-z0-9][A-Za-z0-9.\-']*[A-Za-z0-9]|[A-Za-z0-9]|[^\sA-Za-z0-9]", text):
if tok[-1] == "." and len(tok) > 1:
tok = tok[:-1]
out.append(_token(tok))
s = " ".join(out)
s = re.sub(r"\s+([,.!?;:])", r"\1", s)
s = re.sub(r"[^a-zA-Z' ,.!?;:\-]", " ", s)
return re.sub(r"\s+", " ", s).strip().lower()
def split_sentences(text: str, max_chars: int = 200) -> list[str]:
"""Split into sentence-sized pieces so the TTS can start speaking early."""
parts = [p.strip() for p in re.split(r"(?<=[.!?;])\s+", text) if p.strip()]
out: list[str] = []
for p in parts:
while len(p) > max_chars:
cut = p.rfind(",", 0, max_chars)
cut = cut if cut > 0 else p.rfind(" ", 0, max_chars)
cut = cut if cut > 0 else max_chars
out.append(p[:cut + 1].strip())
p = p[cut + 1:].strip()
if p:
out.append(p)
return out