sound-broken / fault_rules.py
mitvho09's picture
Upload Space app
edb671a verified
Raw
History Blame Contribute Delete
16.2 kB
"""Deterministic candidate fault ranking (the grounding layer).
This is what makes a 4B model reliable: instead of asking the model to BOTH
know acoustic fault signatures AND reason, we encode known signatures here as
transparent rules, produce a ranked shortlist with human-readable evidence,
and let the model only pick + explain the best-supported candidate.
Every diagnosis is therefore traceable to a measured feature value.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, List
from audio_analyzer import AudioFeatures
@dataclass
class Candidate:
name: str
urgency: str # CRITICAL | HIGH | MEDIUM | LOW
weight: float # 0-1 strength of acoustic support
evidence: str # plain-language reason, formatted with feature values
@dataclass
class Rule:
name: str
urgency: str
weight: float
when: Callable[[AudioFeatures], bool]
evidence: str # an f-string-style template using {field} placeholders
def _fmt(template: str, f: AudioFeatures) -> str:
try:
return template.format(**f.to_dict())
except Exception:
return template
# --- Rule tables -----------------------------------------------------------
# Start broad; add appliances over time. Order within a list does not matter;
# candidates are sorted by weight after evaluation.
RULES: dict[str, List[Rule]] = {
"Washing machine": [
Rule("Worn drum bearing", "HIGH", 0.90,
lambda f: f.has_regular_pattern and 50 < f.pattern_interval_ms < 400
and f.spectral_centroid_hz > 1500,
"Regular click every {pattern_interval_ms:.0f} ms with a bright "
"{spectral_centroid_hz:.0f} Hz spectrum is the classic spalled bearing-race signature."),
Rule("Drive belt slip / wear", "MEDIUM", 0.70,
lambda f: f.dominant_frequency_hz > 1800 and f.harmonic_ratio > 0.55,
"A sustained {dominant_frequency_hz:.0f} Hz tone that is strongly harmonic "
"(ratio {harmonic_ratio:.2f}) is typical of a slipping/worn drive belt."),
Rule("Drum load imbalance", "LOW", 0.60,
lambda f: f.rms_variance > 0.02 and not f.has_regular_pattern,
"Large loudness swings (variance {rms_variance:.4f}) with no rhythm point to "
"an unbalanced drum load rather than a mechanical fault."),
Rule("Foreign object in drum/pump", "MEDIUM", 0.65,
lambda f: f.onset_rate_per_sec > 4 and not f.has_regular_pattern
and f.zero_crossing_rate > 0.12,
"Frequent irregular harsh knocks ({onset_rate_per_sec:.1f}/s) suggest a coin or "
"clip loose in the drum or pump."),
],
"Electric fan": [
Rule("Blade imbalance", "MEDIUM", 0.80,
lambda f: f.rms_variance > 0.008 and f.dominant_frequency_hz < 120,
"Low-frequency hum at {dominant_frequency_hz:.0f} Hz modulating in amplitude "
"(variance {rms_variance:.4f}) indicates an unbalanced or dusty blade."),
Rule("Dry / failing motor bearing", "HIGH", 0.85,
lambda f: f.spectral_centroid_hz > 2500 and f.zero_crossing_rate > 0.15,
"A bright {spectral_centroid_hz:.0f} Hz, harsh (ZCR {zero_crossing_rate:.2f}) "
"sound is a dry motor bearing approaching failure."),
Rule("Blade striking housing", "HIGH", 0.75,
lambda f: f.has_regular_pattern and f.pattern_interval_ms < 100,
"Fast regular ticking every {pattern_interval_ms:.0f} ms means a blade is "
"clipping the cage or housing on each rotation."),
],
"Electric motor (generic)": [
Rule("Bearing failure", "HIGH", 0.88,
lambda f: f.has_regular_pattern and f.spectral_centroid_hz > 1800,
"Periodic impacts every {pattern_interval_ms:.0f} ms with high spectral centroid "
"{spectral_centroid_hz:.0f} Hz indicate bearing-race damage."),
Rule("Electrical hum / loose lamination", "LOW", 0.60,
lambda f: 90 < f.dominant_frequency_hz < 130 and f.harmonic_ratio > 0.6,
"A steady {dominant_frequency_hz:.0f} Hz harmonic hum is mains-related "
"(loose laminations); usually cosmetic."),
Rule("Brush / commutator arcing", "MEDIUM", 0.70,
lambda f: f.zero_crossing_rate > 0.2 and f.spectral_bandwidth_hz > 3000,
"Very harsh broadband noise (ZCR {zero_crossing_rate:.2f}, bandwidth "
"{spectral_bandwidth_hz:.0f} Hz) suggests worn brushes arcing on the commutator."),
Rule("High-frequency squeal / bearing whine", "MEDIUM", 0.75,
lambda f: f.dominant_frequency_hz > 1800 and f.harmonic_ratio > 0.5,
"A sustained {dominant_frequency_hz:.0f} Hz tonal whine (harmonic ratio "
"{harmonic_ratio:.2f}) points to a dry bearing or a glazed drive belt squealing."),
],
"Tumble dryer": [
Rule("Drum roller wear", "HIGH", 0.85,
lambda f: f.has_regular_pattern and 80 < f.pattern_interval_ms < 300
and f.spectral_centroid_hz > 800,
"Rhythmic thump every {pattern_interval_ms:.0f} ms with moderate spectral "
"brightness ({spectral_centroid_hz:.0f} Hz) is classic drum-roller wear."),
Rule("Belt slipping / glazing", "MEDIUM", 0.70,
lambda f: f.dominant_frequency_hz > 1800 and f.harmonic_ratio > 0.50,
"A sustained high {dominant_frequency_hz:.0f} Hz squeal with strong harmonic "
"content (ratio {harmonic_ratio:.2f}) indicates a glazed or slipping belt."),
Rule("Foreign object (coins / buttons)", "LOW", 0.60,
lambda f: f.onset_rate_per_sec > 5 and not f.has_regular_pattern,
"Frequent irregular rattling ({onset_rate_per_sec:.1f}/s) is typical of coins "
"or buttons trapped between the drum and the tub."),
],
"Refrigerator/Freezer": [
Rule("Compressor bearing failure", "HIGH", 0.88,
lambda f: f.has_regular_pattern and f.spectral_centroid_hz > 1500
and f.pattern_interval_ms < 150,
"Fast regular clicking every {pattern_interval_ms:.0f} ms with a bright "
"{spectral_centroid_hz:.0f} Hz signature points to compressor bearing wear."),
Rule("Evaporator fan motor bearing", "MEDIUM", 0.72,
lambda f: f.has_regular_pattern and f.dominant_frequency_hz > 400
and f.spectral_centroid_hz > 2000,
"A steady {dominant_frequency_hz:.0f} Hz drone with high brightness "
"({spectral_centroid_hz:.0f} Hz) suggests a failing evaporator fan bearing."),
Rule("Condenser fan grinding", "MEDIUM", 0.65,
lambda f: f.zero_crossing_rate > 0.15 and f.spectral_bandwidth_hz > 2500,
"Broadband harsh noise (ZCR {zero_crossing_rate:.2f}, bandwidth "
"{spectral_bandwidth_hz:.0f} Hz) is consistent with condenser fan rub or grind."),
],
"Air conditioner": [
Rule("Compressor failure", "CRITICAL", 0.92,
lambda f: f.has_regular_pattern and f.spectral_centroid_hz > 1800
and f.rms_db > -30,
"Loud ({rms_db:.0f} dB) rhythmic knocking every {pattern_interval_ms:.0f} ms "
"with high spectral content ({spectral_centroid_hz:.0f} Hz) is compressor "
"failure — shut down immediately."),
Rule("Fan blade damage / debris", "MEDIUM", 0.70,
lambda f: f.has_regular_pattern and f.pattern_interval_ms < 200
and f.spectral_centroid_hz < 1500,
"Low-frequency thwack every {pattern_interval_ms:.0f} ms with a dull spectrum "
"({spectral_centroid_hz:.0f} Hz) indicates a damaged or obstructed fan blade."),
Rule("Refrigerant leak (hissing)", "MEDIUM", 0.65,
lambda f: f.spectral_centroid_hz > 3000 and f.zero_crossing_rate > 0.12
and not f.has_regular_pattern,
"A bright hiss ({spectral_centroid_hz:.0f} Hz) with no periodic structure is "
"consistent with refrigerant escaping from a leak."),
],
"Vacuum cleaner": [
Rule("Brush roll bearing failure", "HIGH", 0.85,
lambda f: f.has_regular_pattern and f.spectral_centroid_hz > 2000
and f.pattern_interval_ms < 80,
"Very fast regular clicking every {pattern_interval_ms:.0f} ms with a bright "
"{spectral_centroid_hz:.0f} Hz tone indicates brush-roll bearing failure."),
Rule("Motor bearing whine", "MEDIUM", 0.75,
lambda f: f.dominant_frequency_hz > 2000 and f.harmonic_ratio > 0.50,
"A sustained high-pitched whine at {dominant_frequency_hz:.0f} Hz with strong "
"harmonics (ratio {harmonic_ratio:.2f}) is a dry motor bearing."),
Rule("Airway blockage", "MEDIUM", 0.68,
lambda f: f.rms_db > -25 and f.spectral_centroid_hz > 2500
and not f.has_regular_pattern,
"Unusually loud ({rms_db:.0f} dB) broadband rush ({spectral_centroid_hz:.0f} Hz) "
"without periodic structure suggests a clogged airway or full dustbin."),
],
"Dishwasher": [
Rule("Wash pump bearing failure", "HIGH", 0.82,
lambda f: f.has_regular_pattern and f.spectral_centroid_hz > 1500
and f.pattern_interval_ms < 200,
"Rhythmic rattling every {pattern_interval_ms:.0f} ms with bright content "
"({spectral_centroid_hz:.0f} Hz) indicates a worn wash-pump bearing."),
Rule("Drain pump cavitating", "MEDIUM", 0.68,
lambda f: f.onset_rate_per_sec > 4 and f.spectral_bandwidth_hz > 3000
and not f.has_regular_pattern,
"Irregular gurgling ({onset_rate_per_sec:.1f}/s) with broad harsh spectrum "
"({spectral_bandwidth_hz:.0f} Hz) suggests the drain pump is cavitating."),
Rule("Spray arm imbalance", "LOW", 0.55,
lambda f: f.has_regular_pattern and f.dominant_frequency_hz < 200
and f.rms_variance > 0.01,
"Slow regular swish every {pattern_interval_ms:.0f} ms with loudness variation "
"(variance {rms_variance:.4f}) points to a bent or blocked spray arm."),
],
"Microwave": [
Rule("Turntable motor failure", "MEDIUM", 0.70,
lambda f: f.dominant_frequency_hz > 0 and f.dominant_frequency_hz < 100
and f.harmonic_ratio > 0.6 and not f.has_regular_pattern,
"A steady low hum ({dominant_frequency_hz:.0f} Hz) with high harmonic purity "
"(ratio {harmonic_ratio:.2f}) but no mechanical clicking means the turntable "
"motor has seized while the magnetron runs."),
Rule("Magnetron arcing / failure", "HIGH", 0.85,
lambda f: f.zero_crossing_rate > 0.2 and f.spectral_centroid_hz > 2000
and f.rms_db > -25,
"Loud ({rms_db:.0f} dB) harsh buzzing (ZCR {zero_crossing_rate:.2f}) with "
"bright spectrum ({spectral_centroid_hz:.0f} Hz) indicates magnetron arcing."),
Rule("Cooling fan bearing", "LOW", 0.60,
lambda f: f.has_regular_pattern and f.pattern_interval_ms < 150
and f.spectral_centroid_hz > 1200,
"Regular fast ticking every {pattern_interval_ms:.0f} ms at moderate brightness "
"({spectral_centroid_hz:.0f} Hz) is the cooling fan bearing wearing out."),
],
"Bicycle (chain/gears)": [
Rule("Chain wear / dry links", "MEDIUM", 0.72,
lambda f: f.has_regular_pattern and f.pattern_interval_ms < 200
and f.spectral_centroid_hz > 1500,
"Fast rhythmic clicking every {pattern_interval_ms:.0f} ms with a bright "
"{spectral_centroid_hz:.0f} Hz tone is classic chain-link wear."),
Rule("Wheel bearing pitting", "HIGH", 0.85,
lambda f: f.has_regular_pattern and 50 < f.pattern_interval_ms < 400
and f.spectral_centroid_hz > 2000,
"Regular thump every {pattern_interval_ms:.0f} ms with high-frequency "
"content ({spectral_centroid_hz:.0f} Hz) indicates a pitted wheel bearing."),
Rule("Derailleur misalignment", "MEDIUM", 0.65,
lambda f: f.onset_rate_per_sec > 3 and not f.has_regular_pattern
and f.spectral_centroid_hz > 1800,
"Irregular metallic rattling ({onset_rate_per_sec:.1f}/s) with bright tones "
"({spectral_centroid_hz:.0f} Hz) suggests the derailleur is out of alignment."),
],
"Power drill": [
Rule("Brush / commutator wear", "MEDIUM", 0.75,
lambda f: f.zero_crossing_rate > 0.2 and f.spectral_bandwidth_hz > 3000,
"Very harsh broadband noise (ZCR {zero_crossing_rate:.2f}, bandwidth "
"{spectral_bandwidth_hz:.0f} Hz) is consistent with worn motor brushes."),
Rule("Gear train grinding", "MEDIUM", 0.70,
lambda f: f.spectral_centroid_hz > 1800 and f.harmonic_ratio < 0.4
and f.onset_rate_per_sec > 3,
"Bright ({spectral_centroid_hz:.0f} Hz) but non-tonal (harmonic ratio "
"{harmonic_ratio:.2f}) grinding with rapid onsets ({onset_rate_per_sec:.1f}/s) "
"points to worn or chipped gears."),
Rule("Bearing failure", "HIGH", 0.82,
lambda f: f.has_regular_pattern and f.spectral_centroid_hz > 2000
and f.pattern_interval_ms < 100,
"Fast regular ticking every {pattern_interval_ms:.0f} ms with high spectral "
"brightness ({spectral_centroid_hz:.0f} Hz) is a spindle or armature bearing "
"approaching failure."),
],
"Car engine": [
Rule("Rod knock / bearing", "CRITICAL", 0.90,
lambda f: f.has_regular_pattern and 20 < f.pattern_interval_ms < 200
and f.spectral_centroid_hz > 1200,
"A rhythmic deep knock every {pattern_interval_ms:.0f} ms that tracks RPM is a "
"rod/main bearing knock — stop driving."),
Rule("Belt squeal (serpentine/timing)", "MEDIUM", 0.72,
lambda f: f.dominant_frequency_hz > 2000 and f.harmonic_ratio > 0.5,
"A high {dominant_frequency_hz:.0f} Hz squeal is usually a glazed/loose "
"serpentine belt or tensioner."),
Rule("Exhaust leak / tappet", "MEDIUM", 0.65,
lambda f: f.onset_rate_per_sec > 6 and f.zero_crossing_rate > 0.1,
"Rapid ticking ({onset_rate_per_sec:.1f}/s) can be a noisy tappet or a small "
"exhaust-manifold leak."),
],
}
# Appliances without a dedicated table fall back to the generic motor rules.
GENERIC_FALLBACK = "Electric motor (generic)"
def rank_candidates(features: AudioFeatures, appliance: str) -> List[Candidate]:
"""Return fault candidates whose rules fire, strongest first.
If nothing fires, return a single low-confidence 'inconclusive' candidate so
downstream code (and the model) always has something honest to say.
"""
table = RULES.get(appliance) or RULES.get(GENERIC_FALLBACK, [])
fired: List[Candidate] = []
for rule in table:
try:
if rule.when(features):
fired.append(Candidate(
name=rule.name, urgency=rule.urgency, weight=rule.weight,
evidence=_fmt(rule.evidence, features),
))
except Exception:
continue
fired.sort(key=lambda c: c.weight, reverse=True)
if not fired:
fired.append(Candidate(
name="Inconclusive",
urgency="LOW",
weight=0.0,
evidence=(
f"No known fault signature matched. Anomaly score is "
f"{features.anomaly_score:.2f}; sound may be within normal range "
f"or the recording is too quiet/short."
),
))
return fired