Automatic Speech Recognition
PEFT
Safetensors
English
whisper
lora
audio
machine-sound
anomaly-detection
predictive-maintenance
manufacturing
voice-agent
text-to-speech
non-commercial
Eval Results (legacy)
Instructions to use muruganmath/Machine-fault-detector with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use muruganmath/Machine-fault-detector with PEFT:
from peft import PeftModel from transformers import AutoModelForSpeechSeq2Seq base_model = AutoModelForSpeechSeq2Seq.from_pretrained("openai/whisper-large-v3") model = PeftModel.from_pretrained(base_model, "muruganmath/Machine-fault-detector") - Notebooks
- Google Colab
- Kaggle
File size: 6,676 Bytes
36ba721 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | #!/usr/bin/env python
"""Machine-fault-detector: audio file in -> diagnosis (or transcript) -> spoken reply WAV.
pip install -r requirements.txt
python inference.py clip.wav # -> clip_reply.wav + printed text
python inference.py clip.wav --out reply.wav --no-voice
Loads openai/whisper-large-v3 + this LoRA adapter, decodes like training (16 kHz mono,
<= 30 s chunks, peak -1 dBFS, greedy, suppress_tokens=[] because ':' and ';' are template
tokens). Per chunk:
* a complete machine template -> "Sound check. This is a fan under high load. ..."
* otherwise speech -> "I heard: <transcript>"
* noise / known hallucination -> silent
The reply is spoken with facebook/mms-tts-eng (CC-BY-NC-4.0). Research prototype: trained
on synthetic machine sounds only; do not use for maintenance or safety decisions.
"""
import argparse
import copy
import re
import sys
from math import gcd
from pathlib import Path
import numpy as np
import soundfile as sf
import torch
from scipy.signal import resample_poly
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from diagnosis import describe, parse_diagnosis # noqa: E402
from tts_text import speakable, split_sentences # noqa: E402
SR = 16000
REPO = "muruganmath/Machine-fault-detector"
# outputs that are noise, not speech (measured on the training project's clips)
NOT_SPEECH = re.compile(r"^\s*(?:(?:(?:machine|engine)(?:\s+sounds?)?)[\s.,;:]*)+$", re.I)
HALLUCINATIONS = {"you", "so", "the", "okay", "ok", "bye", "uh", "um", "hmm", "oh", "thank you",
"thanks", "thank you very much", "thanks for watching", "thank you for watching"}
def load_audio(path: str) -> np.ndarray:
x, sr = sf.read(path, dtype="float32", always_2d=True)
x = x.mean(axis=1)
if sr != SR:
g = gcd(sr, SR)
x = resample_poly(x, SR // g, sr // g).astype(np.float32)
return x
def chunks(x: np.ndarray, max_s: float = 30.0):
n = int(max_s * SR)
for i in range(0, len(x), n):
c = x[i:i + n]
if c.size >= SR and 20 * np.log10(max(float(np.sqrt(np.mean(c ** 2))), 1e-10)) >= -60.0:
yield i / SR, c # same rejection as training: < 1 s or silent
class Detector:
def __init__(self, repo: str = REPO, base: str = "openai/whisper-large-v3", device: str | None = None):
from peft import PeftModel
from transformers import WhisperForConditionalGeneration, WhisperProcessor
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
self.dtype = torch.bfloat16 if self.device == "cuda" else torch.float32
self.proc = WhisperProcessor.from_pretrained(base)
model = WhisperForConditionalGeneration.from_pretrained(base, dtype=self.dtype, attn_implementation="sdpa")
self.model = PeftModel.from_pretrained(model, repo).merge_and_unload().to(self.device).eval()
gc = copy.deepcopy(self.model.generation_config)
gc.suppress_tokens, gc.language, gc.task, gc.forced_decoder_ids = [], "en", "transcribe", None
self.gen_config = gc
@torch.inference_mode()
def decode(self, chunk: np.ndarray) -> str:
x = chunk * (10 ** (-1.0 / 20) / max(float(np.abs(chunk).max()), 1e-9)) # peak -1 dBFS
feats = self.proc.feature_extractor([x], sampling_rate=SR, return_tensors="pt").input_features
out = self.model.generate(input_features=feats.to(self.device, self.dtype), generation_config=self.gen_config,
max_new_tokens=128, num_beams=1)
return self.proc.tokenizer.batch_decode(out, skip_special_tokens=True)[0].strip()
def reply_for(text: str) -> tuple[str, str | None]:
"""(kind, reply text or None)."""
d = parse_diagnosis(text)
if d is not None:
return "machine", describe(d)
key = re.sub(r"\s+", " ", re.sub(r"[^a-z0-9' ]", " ", text.lower())).strip()
if not re.search(r"[A-Za-z0-9]", text) or NOT_SPEECH.match(text) or key in HALLUCINATIONS:
return "noise", None
return "speech", f"I heard: {text}"
class Voice:
def __init__(self, repo: str = "facebook/mms-tts-eng", device: str | None = None):
from transformers import AutoTokenizer, VitsModel
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
self.model = VitsModel.from_pretrained(repo).to(self.device).eval()
self.tok = AutoTokenizer.from_pretrained(repo)
self.sr = int(self.model.config.sampling_rate)
@torch.inference_mode()
def speak(self, text: str) -> np.ndarray:
parts, gap = [], np.zeros(int(0.12 * self.sr), np.float32)
for s in split_sentences(speakable(text)):
ids = self.tok(s, return_tensors="pt").to(self.device)
if ids["input_ids"].shape[-1] == 0:
continue
torch.manual_seed(0) # VITS is stochastic; keep replies reproducible
parts += [self.model(**ids).waveform[0].float().cpu().numpy(), gap]
if not parts:
return np.zeros(0, np.float32)
y = np.concatenate(parts[:-1])
return (y * min(1.0, 0.9 / max(float(np.abs(y).max()), 1e-9))).astype(np.float32)
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("audio")
ap.add_argument("--out", help="reply WAV (default: <audio>_reply.wav next to the input)")
ap.add_argument("--repo", default=REPO, help="adapter repo id or local folder")
ap.add_argument("--base", default="openai/whisper-large-v3", help="base model repo id or local folder")
ap.add_argument("--tts", default="facebook/mms-tts-eng", help="TTS repo id or local folder")
ap.add_argument("--no-voice", action="store_true", help="print the text only")
args = ap.parse_args()
det = Detector(args.repo, args.base)
replies = []
for t0, c in chunks(load_audio(args.audio)):
text = det.decode(c)
kind, reply = reply_for(text)
print(f"[{t0:6.1f}s] {kind:7s} model: {text}")
print(f"{'':16}reply: {reply or '(silent)'}")
if reply:
replies.append(reply)
if args.no_voice or not replies:
return
voice = Voice(args.tts)
gap = np.zeros(int(0.4 * voice.sr), np.float32)
wav = np.concatenate([np.concatenate([voice.speak(r), gap]) for r in replies])[: -gap.size]
out = args.out or str(Path(args.audio).with_name(Path(args.audio).stem + "_reply.wav"))
sf.write(out, wav, voice.sr, subtype="PCM_16")
print(f"voice reply: {out} ({wav.size / voice.sr:.1f} s)")
if __name__ == "__main__":
main()
|