mandipgoswami commited on
Commit
1c7d1c9
·
verified ·
1 Parent(s): 5d0336c

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +443 -72
app.py CHANGED
@@ -1,25 +1,39 @@
1
  """Gradio demo app for AnomalyMachine-50K dataset anomaly detection."""
2
 
 
3
  import tempfile
4
  from pathlib import Path
5
 
6
  import gradio as gr
 
 
 
 
 
 
 
 
 
7
  import librosa
8
  import matplotlib
9
- matplotlib.use("Agg")
10
  import matplotlib.pyplot as plt
11
  import numpy as np
 
12
 
13
- DATASET_URL = "https://huggingface.co/datasets/mandipgoswami/AnomalyMachine-50K"
14
 
 
15
  DATASET_INFO = {
16
  "total_clips": 50000,
17
  "machines": ["fan", "pump", "compressor", "conveyor_belt", "electric_motor", "valve"],
18
  "normal_ratio": 0.6,
19
  "anomalous_ratio": 0.4,
 
 
20
  "total_hours": round(50000 * 10.0 / 3600, 2),
21
  }
22
 
 
23
  ANOMALY_SUBTYPES = {
24
  "fan": ["bearing_fault", "imbalance", "obstruction"],
25
  "pump": ["bearing_fault", "cavitation", "overheating"],
@@ -29,102 +43,459 @@ ANOMALY_SUBTYPES = {
29
  "valve": ["cavitation", "obstruction"],
30
  }
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- def create_spectrogram(audio_path, title="Spectrogram"):
34
- """Create mel spectrogram from audio file."""
35
  try:
36
  y, sr = librosa.load(audio_path, sr=22050, mono=True)
37
  mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128, fmax=8000)
38
- mel_db = librosa.power_to_db(mel_spec, ref=np.max)
 
39
  fig, ax = plt.subplots(figsize=(10, 4))
40
- im = ax.imshow(mel_db, aspect="auto", origin="lower", cmap="viridis", interpolation="nearest")
41
- ax.set_title(title)
42
- ax.set_xlabel("Time")
43
- ax.set_ylabel("Mel Frequency")
44
- plt.colorbar(im, ax=ax, format="%+2.0f dB")
 
 
 
 
 
 
45
  plt.tight_layout()
 
 
46
  temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
47
  plt.savefig(temp_file.name, dpi=100, bbox_inches="tight")
48
  plt.close()
49
  return temp_file.name
50
  except Exception as e:
51
- print(f"Spectrogram error: {e}")
52
  return None
53
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  def predict_anomaly(audio_file, machine_type):
56
- """Predict anomaly in audio. Returns (result_html, input_spec_path, ref_spec_path)."""
57
  if audio_file is None:
58
- return "Please upload an audio file.", None, None
59
- if isinstance(audio_file, dict):
60
- audio_file = audio_file.get("path") or audio_file.get("name")
61
- if not audio_file:
62
- return "Please upload an audio file.", None, None
63
-
64
- import random
65
- random.seed(hash(str(audio_file)) % 1000)
66
- is_anomaly = random.random() > 0.5
67
- confidence = random.uniform(0.75, 0.95)
68
-
69
- if is_anomaly:
70
- label = "ANOMALY ✗"
71
- color = "#ff4444"
72
- subtype = random.choice(ANOMALY_SUBTYPES.get(machine_type, ["unknown"]))
73
- result = f'<div style="text-align: center; padding: 20px;"><h2 style="color: {color}; font-size: 48px;">{label}</h2><p style="font-size: 18px;">Type: <strong>{subtype.replace("_", " ").title()}</strong></p><p>Confidence: {confidence:.1%}</p></div>'
74
- else:
75
- label = "NORMAL ✓"
76
- color = "#44ff44"
77
- result = f'<div style="text-align: center; padding: 20px;"><h2 style="color: {color}; font-size: 48px;">{label}</h2><p>Confidence: {confidence:.1%}</p></div>'
78
 
79
- input_spec = create_spectrogram(audio_file, f"Input - {machine_type}")
 
 
 
 
 
80
  ref_spec = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  examples_dir = Path(__file__).parent / "examples"
82
- if examples_dir.exists():
83
- ref_files = list(examples_dir.glob(f"{machine_type}_*.wav"))
84
- if ref_files:
85
- ref_spec = create_spectrogram(str(ref_files[0]), f"Reference - {machine_type}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- return result, input_spec, ref_spec
 
 
88
 
 
 
 
89
 
90
- # Use Blocks only; no api_name to avoid broken API schema path that causes "No API found"
91
- with gr.Blocks(title="AnomalyMachine-50K Demo", theme=gr.themes.Monochrome(primary_hue="red", secondary_hue="gray")) as app:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  gr.Markdown(f"""
93
  <div style="text-align: center; padding: 20px;">
94
- <h1>AnomalyMachine-50K</h1>
95
- <p>Synthetic Industrial Machine Sound Anomaly Detection Dataset</p>
96
- <p><a href="{DATASET_URL}" target="_blank">View Dataset</a></p>
 
 
 
97
  </div>
98
  """)
99
- with gr.Tabs():
100
- with gr.Tab("Detect Anomaly"):
101
- with gr.Row():
102
- with gr.Column():
103
- audio_in = gr.Audio(label="Upload Audio", type="filepath", sources=["upload", "microphone"])
104
- machine_in = gr.Dropdown(choices=DATASET_INFO["machines"], label="Machine Type", value=DATASET_INFO["machines"][0])
105
- btn = gr.Button("Detect Anomaly", variant="primary")
106
- with gr.Column():
107
- result_out = gr.HTML()
108
- with gr.Row():
109
- spec_out = gr.Image(label="Input Spectrogram")
110
- ref_out = gr.Image(label="Reference Spectrogram")
111
- btn.click(fn=predict_anomaly, inputs=[audio_in, machine_in], outputs=[result_out, spec_out, ref_out])
112
-
113
- with gr.Tab("Explore Dataset"):
114
- gr.Markdown(f"""
115
- **Dataset Statistics**
116
- - Total Clips: {DATASET_INFO['total_clips']:,}
117
- - Total Duration: {DATASET_INFO['total_hours']} hours
118
- - Machine Types: {len(DATASET_INFO['machines'])}
119
- - Normal: {DATASET_INFO['normal_ratio']:.0%} | Anomalous: {DATASET_INFO['anomalous_ratio']:.0%}
120
-
121
- [Download Dataset]({DATASET_URL})
122
- """)
123
- gr.Markdown(f"""
124
- <div style="text-align: center; padding: 20px; border-top: 1px solid #333;">
125
- <p style="color: #888;">License: CC-BY 4.0 | <a href="{DATASET_URL}" target="_blank">Dataset</a> | <a href="https://github.com/mandip42/anomaly-machine-50k" target="_blank">GitHub</a></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  </div>
127
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  if __name__ == "__main__":
130
- app.launch()
 
1
  """Gradio demo app for AnomalyMachine-50K dataset anomaly detection."""
2
 
3
+ import os
4
  import tempfile
5
  from pathlib import Path
6
 
7
  import gradio as gr
8
+ # Fix Gradio 4.35/4.44 API info crash when JSON schema has boolean (e.g. additionalProperties: true)
9
+ import gradio_client.utils as _client_utils
10
+ _orig_get_type = _client_utils.get_type
11
+ def _get_type_handle_bool(schema):
12
+ if isinstance(schema, bool):
13
+ return "boolean"
14
+ return _orig_get_type(schema)
15
+ _client_utils.get_type = _get_type_handle_bool
16
+
17
  import librosa
18
  import matplotlib
 
19
  import matplotlib.pyplot as plt
20
  import numpy as np
21
+ from transformers import pipeline
22
 
23
+ matplotlib.use("Agg") # Non-interactive backend
24
 
25
+ # Dataset metadata
26
  DATASET_INFO = {
27
  "total_clips": 50000,
28
  "machines": ["fan", "pump", "compressor", "conveyor_belt", "electric_motor", "valve"],
29
  "normal_ratio": 0.6,
30
  "anomalous_ratio": 0.4,
31
+ "clip_duration_seconds": 10.0,
32
+ "sample_rate": 22050,
33
  "total_hours": round(50000 * 10.0 / 3600, 2),
34
  }
35
 
36
+ # Anomaly subtypes mapping
37
  ANOMALY_SUBTYPES = {
38
  "fan": ["bearing_fault", "imbalance", "obstruction"],
39
  "pump": ["bearing_fault", "cavitation", "overheating"],
 
43
  "valve": ["cavitation", "obstruction"],
44
  }
45
 
46
+ # Placeholder model - replace with actual trained model
47
+ MODEL_NAME = "YOUR_HF_USERNAME/AnomalyMachine-Classifier"
48
+ model = None
49
+
50
+
51
+ def load_model():
52
+ """Lazy load the audio classification model."""
53
+ global model
54
+ if model is None:
55
+ try:
56
+ model = pipeline(
57
+ "audio-classification",
58
+ model=MODEL_NAME,
59
+ )
60
+ except Exception as e:
61
+ print(f"Warning: Could not load model {MODEL_NAME}: {e}")
62
+ print("Using placeholder predictions for demo.")
63
+ model = "placeholder"
64
+ return model
65
+
66
 
67
+ def create_mel_spectrogram(audio_path: str, title: str = "Mel Spectrogram") -> str:
68
+ """Create a mel spectrogram visualization from audio file."""
69
  try:
70
  y, sr = librosa.load(audio_path, sr=22050, mono=True)
71
  mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128, fmax=8000)
72
+ mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
73
+
74
  fig, ax = plt.subplots(figsize=(10, 4))
75
+ img = librosa.display.specshow(
76
+ mel_spec_db,
77
+ x_axis="time",
78
+ y_axis="mel",
79
+ sr=sr,
80
+ fmax=8000,
81
+ ax=ax,
82
+ cmap="viridis",
83
+ )
84
+ ax.set_title(title, fontsize=14, fontweight="bold")
85
+ plt.colorbar(img, ax=ax, format="%+2.0f dB")
86
  plt.tight_layout()
87
+
88
+ # Save to temporary file
89
  temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
90
  plt.savefig(temp_file.name, dpi=100, bbox_inches="tight")
91
  plt.close()
92
  return temp_file.name
93
  except Exception as e:
94
+ print(f"Error creating spectrogram: {e}")
95
  return None
96
 
97
 
98
+ def get_reference_audio(machine_type: str) -> str:
99
+ """Get path to reference normal audio for a machine type."""
100
+ examples_dir = Path(__file__).parent / "examples"
101
+ # Look for a normal example (naming convention: {machine}_normal_*.wav)
102
+ ref_pattern = f"{machine_type}_*_normal_*.wav"
103
+ ref_files = list(examples_dir.glob(ref_pattern))
104
+ if not ref_files:
105
+ # Fallback: use any example for this machine
106
+ ref_files = list(examples_dir.glob(f"{machine_type}_*.wav"))
107
+ return str(ref_files[0]) if ref_files else None
108
+
109
+
110
  def predict_anomaly(audio_file, machine_type):
111
+ """Predict if audio contains an anomaly."""
112
  if audio_file is None:
113
+ return None, None, None, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ # Load model
116
+ model_instance = load_model()
117
+
118
+ # Create spectrograms
119
+ input_spec = create_mel_spectrogram(audio_file, f"Input Audio - {machine_type}")
120
+ ref_audio = get_reference_audio(machine_type)
121
  ref_spec = None
122
+ if ref_audio:
123
+ ref_spec = create_mel_spectrogram(ref_audio, f"Reference Normal - {machine_type}")
124
+
125
+ # Make prediction
126
+ if model_instance == "placeholder":
127
+ # Placeholder predictions for demo
128
+ import random
129
+ is_anomaly = random.random() > 0.5
130
+ confidence = random.uniform(0.7, 0.95)
131
+ if is_anomaly:
132
+ anomaly_subtype = random.choice(ANOMALY_SUBTYPES.get(machine_type, ["unknown"]))
133
+ label = "ANOMALY"
134
+ color = "red"
135
+ else:
136
+ anomaly_subtype = "none"
137
+ label = "NORMAL"
138
+ color = "green"
139
+ else:
140
+ # Real model prediction
141
+ try:
142
+ results = model_instance(audio_file)
143
+ # Assuming model returns list of dicts with 'label' and 'score'
144
+ top_result = results[0] if isinstance(results, list) else results
145
+ label_str = top_result.get("label", "").lower()
146
+ confidence = top_result.get("score", 0.5)
147
+
148
+ is_anomaly = "anomaly" in label_str or "anomalous" in label_str
149
+ if is_anomaly:
150
+ label = "ANOMALY"
151
+ color = "red"
152
+ # Try to extract anomaly subtype from label
153
+ anomaly_subtype = "unknown"
154
+ for subtype in ANOMALY_SUBTYPES.get(machine_type, []):
155
+ if subtype in label_str:
156
+ anomaly_subtype = subtype
157
+ break
158
+ else:
159
+ label = "NORMAL"
160
+ color = "green"
161
+ anomaly_subtype = "none"
162
+ except Exception as e:
163
+ print(f"Prediction error: {e}")
164
+ label = "ERROR"
165
+ color = "gray"
166
+ confidence = 0.0
167
+ anomaly_subtype = "none"
168
+
169
+ # Format result HTML
170
+ result_html = f"""
171
+ <div style="text-align: center; padding: 20px;">
172
+ <h2 style="color: {color}; font-size: 48px; margin: 20px 0;">
173
+ {label} {'✓' if label == 'NORMAL' else '✗'}
174
+ </h2>
175
+ {f'<p style="font-size: 18px; color: #888;">Anomaly Type: <strong>{anomaly_subtype.replace("_", " ").title()}</strong></p>' if anomaly_subtype != 'none' else ''}
176
+ <p style="font-size: 16px; color: #aaa;">Confidence: {confidence:.1%}</p>
177
+ </div>
178
+ """
179
+
180
+ return result_html, confidence, input_spec, ref_spec, audio_file
181
+
182
+
183
+ def create_dataset_gallery():
184
+ """Create gallery of example spectrograms for each machine type."""
185
  examples_dir = Path(__file__).parent / "examples"
186
+ if not examples_dir.exists():
187
+ return []
188
+
189
+ gallery_items = []
190
+ for machine in DATASET_INFO["machines"]:
191
+ # Find normal and anomalous examples
192
+ normal_files = list(examples_dir.glob(f"{machine}_*_normal_*.wav"))
193
+ anomaly_files = list(examples_dir.glob(f"{machine}_*_anomalous_*.wav"))
194
+
195
+ normal_spec = None
196
+ anomaly_spec = None
197
+
198
+ if normal_files:
199
+ normal_spec = create_mel_spectrogram(str(normal_files[0]), f"{machine} - Normal")
200
+ if anomaly_files:
201
+ anomaly_spec = create_mel_spectrogram(str(anomaly_files[0]), f"{machine} - Anomaly")
202
+
203
+ if normal_spec or anomaly_spec:
204
+ gallery_items.append((normal_spec, anomaly_spec, machine))
205
+
206
+ return gallery_items
207
+
208
 
209
+ def build_explore_tab():
210
+ """Build the dataset exploration tab."""
211
+ gallery_items = create_dataset_gallery()
212
 
213
+ # Populate galleries
214
+ normal_images = [item[0] for item in gallery_items if item[0] and item[0] is not None]
215
+ anomaly_images = [item[1] for item in gallery_items if item[1] and item[1] is not None]
216
 
217
+ with gr.Row():
218
+ with gr.Column():
219
+ gr.Markdown("### Normal Examples")
220
+ normal_gallery = gr.Gallery(
221
+ label="Normal Machine Sounds",
222
+ show_label=False,
223
+ elem_id="normal_gallery",
224
+ columns=2,
225
+ rows=3,
226
+ height="auto",
227
+ value=normal_images if normal_images else None,
228
+ )
229
+ with gr.Column():
230
+ gr.Markdown("### Anomaly Examples")
231
+ anomaly_gallery = gr.Gallery(
232
+ label="Anomalous Machine Sounds",
233
+ show_label=False,
234
+ elem_id="anomaly_gallery",
235
+ columns=2,
236
+ rows=3,
237
+ height="auto",
238
+ value=anomaly_images if anomaly_images else None,
239
+ )
240
+
241
+ # Dataset statistics
242
+ with gr.Accordion("Dataset Statistics", open=False):
243
+ stats_html = f"""
244
+ <div style="padding: 20px;">
245
+ <h3>AnomalyMachine-50K Dataset</h3>
246
+ <ul style="font-size: 16px; line-height: 2;">
247
+ <li><strong>Total Clips:</strong> {DATASET_INFO['total_clips']:,}</li>
248
+ <li><strong>Total Duration:</strong> {DATASET_INFO['total_hours']} hours</li>
249
+ <li><strong>Machine Types:</strong> {len(DATASET_INFO['machines'])}</li>
250
+ <li><strong>Normal Ratio:</strong> {DATASET_INFO['normal_ratio']:.0%}</li>
251
+ <li><strong>Anomalous Ratio:</strong> {DATASET_INFO['anomalous_ratio']:.0%}</li>
252
+ <li><strong>Sample Rate:</strong> {DATASET_INFO['sample_rate']} Hz</li>
253
+ <li><strong>Clip Duration:</strong> {DATASET_INFO['clip_duration_seconds']} seconds</li>
254
+ </ul>
255
+ <h4>Machine Breakdown:</h4>
256
+ <ul>
257
+ {''.join([f'<li>{m.replace("_", " ").title()}</li>' for m in DATASET_INFO['machines']])}
258
+ </ul>
259
+ </div>
260
+ """
261
+ gr.HTML(stats_html)
262
+
263
+ # Download button
264
+ dataset_url = "https://huggingface.co/datasets/AnomalyMachine-50K"
265
  gr.Markdown(f"""
266
  <div style="text-align: center; padding: 20px;">
267
+ <a href="{dataset_url}" target="_blank">
268
+ <button style="background-color: #007bff; color: white; padding: 15px 30px;
269
+ font-size: 18px; border: none; border-radius: 5px; cursor: pointer;">
270
+ 📥 Download Dataset
271
+ </button>
272
+ </a>
273
  </div>
274
  """)
275
+
276
+ return normal_gallery, anomaly_gallery, normal_images, anomaly_images
277
+
278
+
279
+ def build_detect_tab():
280
+ """Build the anomaly detection tab."""
281
+ with gr.Row():
282
+ with gr.Column(scale=1):
283
+ audio_input = gr.Audio(
284
+ label="Upload Audio or Record",
285
+ type="filepath",
286
+ sources=["upload", "microphone"],
287
+ )
288
+ machine_dropdown = gr.Dropdown(
289
+ choices=DATASET_INFO["machines"],
290
+ label="Machine Type",
291
+ value=DATASET_INFO["machines"][0],
292
+ info="Select the type of machine in the audio",
293
+ )
294
+ predict_btn = gr.Button("Detect Anomaly", variant="primary", size="lg")
295
+
296
+ with gr.Column(scale=2):
297
+ result_html = gr.HTML(label="Prediction Result")
298
+ confidence_bar = gr.Slider(
299
+ minimum=0,
300
+ maximum=1,
301
+ value=0,
302
+ label="Confidence Score",
303
+ interactive=False,
304
+ )
305
+
306
+ with gr.Row():
307
+ with gr.Column():
308
+ input_spec = gr.Image(label="Input Audio Spectrogram")
309
+ with gr.Column():
310
+ ref_spec = gr.Image(label="Reference Normal Spectrogram")
311
+
312
+ audio_output = gr.Audio(label="Processed Audio", visible=False)
313
+
314
+ predict_btn.click(
315
+ fn=predict_anomaly,
316
+ inputs=[audio_input, machine_dropdown],
317
+ outputs=[result_html, confidence_bar, input_spec, ref_spec, audio_output],
318
+ )
319
+
320
+ return (
321
+ audio_input,
322
+ machine_dropdown,
323
+ predict_btn,
324
+ result_html,
325
+ confidence_bar,
326
+ input_spec,
327
+ ref_spec,
328
+ audio_output,
329
+ )
330
+
331
+
332
+ def build_header():
333
+ """Build the app header."""
334
+ return gr.Markdown(
335
+ """
336
+ <div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
337
+ border-radius: 10px; margin-bottom: 20px;">
338
+ <h1 style="color: white; margin: 0;">🏭 AnomalyMachine-50K</h1>
339
+ <p style="color: rgba(255,255,255,0.9); font-size: 18px; margin: 10px 0;">
340
+ Synthetic Industrial Machine Sound Anomaly Detection Dataset
341
+ </p>
342
+ <p style="color: rgba(255,255,255,0.8);">
343
+ <a href="https://huggingface.co/datasets/AnomalyMachine-50K"
344
+ style="color: white; text-decoration: underline;" target="_blank">
345
+ View Dataset on Hugging Face →
346
+ </a>
347
+ </p>
348
  </div>
349
+ """
350
+ )
351
+
352
+
353
+ def build_footer():
354
+ """Build the app footer."""
355
+ return gr.Markdown(
356
+ """
357
+ <div style="text-align: center; padding: 20px; margin-top: 40px; border-top: 1px solid #333;">
358
+ <p style="color: #888; font-size: 14px;">
359
+ License: <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank"
360
+ style="color: #4a9eff;">CC-BY 4.0</a> |
361
+ Dataset: <a href="https://huggingface.co/datasets/AnomalyMachine-50K" target="_blank"
362
+ style="color: #4a9eff;">AnomalyMachine-50K</a> |
363
+ GitHub: <a href="https://github.com/mandip42/anomaly-machine-50k" target="_blank"
364
+ style="color: #4a9eff;">mandip42/anomaly-machine-50k</a>
365
+ </p>
366
+ </div>
367
+ """
368
+ )
369
+
370
+
371
+ def build_how_it_works():
372
+ """Build the 'How it works' accordion."""
373
+ how_it_works_html = """
374
+ <div style="padding: 20px; line-height: 1.8;">
375
+ <h3>Signal Processing-Based Synthesis</h3>
376
+ <p>
377
+ The AnomalyMachine-50K dataset is generated entirely using deterministic signal processing
378
+ techniques—no neural audio models are used. This ensures reproducibility, lightweight generation,
379
+ and freedom from copyright concerns.
380
+ </p>
381
+
382
+ <h4>1. Base Machine Sound Generation</h4>
383
+ <p>
384
+ Each machine type has a dedicated synthesis model:
385
+ </p>
386
+ <ul>
387
+ <li><strong>Fan:</strong> Broadband noise + rotating blade harmonics (50-200 Hz)</li>
388
+ <li><strong>Pump:</strong> Low-frequency rumble (20-80 Hz) + rhythmic pressure pulses</li>
389
+ <li><strong>Compressor:</strong> 60 Hz motor hum + harmonics with cyclic compression envelope</li>
390
+ <li><strong>Conveyor Belt:</strong> Rhythmic tapping + friction noise</li>
391
+ <li><strong>Electric Motor:</strong> Tonal fundamental (1200-3600 RPM) + harmonics + brush noise</li>
392
+ <li><strong>Valve:</strong> Turbulent flow noise + actuation clicks</li>
393
+ </ul>
394
+
395
+ <h4>2. Operating Condition Modulation</h4>
396
+ <p>
397
+ Conditions (idle, normal_load, high_load) modulate amplitude and harmonic content.
398
+ </p>
399
+
400
+ <h4>3. Anomaly Injection</h4>
401
+ <p>
402
+ Anomalies are injected via signal transformations:
403
+ </p>
404
+ <ul>
405
+ <li><strong>Bearing Fault:</strong> Periodic impulsive spikes</li>
406
+ <li><strong>Imbalance:</strong> Sinusoidal amplitude modulation</li>
407
+ <li><strong>Cavitation:</strong> Burst noise events</li>
408
+ <li><strong>Overheating:</strong> Gradually increasing high-frequency noise</li>
409
+ <li><strong>Obstruction:</strong> Intermittent amplitude drops + resonance shifts</li>
410
+ </ul>
411
+
412
+ <h4>4. Background Noise</h4>
413
+ <p>
414
+ Factory-floor ambience (pink noise + 60/120 Hz hum) is mixed at configurable SNR levels.
415
+ </p>
416
+
417
+ <p style="margin-top: 20px; font-style: italic;">
418
+ All synthesis is deterministic and reproducible with a fixed random seed.
419
+ </p>
420
+ </div>
421
+ """
422
+ return gr.Accordion("How It Works", open=False).update(
423
+ value=gr.HTML(how_it_works_html)
424
+ )
425
+
426
+
427
+ def main():
428
+ """Main Gradio app entry point."""
429
+ theme = gr.themes.Monochrome(
430
+ primary_hue="red",
431
+ secondary_hue="gray",
432
+ font=("Helvetica", "ui-sans-serif", "system-ui"),
433
+ )
434
+
435
+ with gr.Blocks(theme=theme, title="AnomalyMachine-50K Demo") as app:
436
+ build_header()
437
+
438
+ with gr.Tabs():
439
+ with gr.Tab("🔍 Detect Anomaly"):
440
+ with gr.Accordion("How It Works", open=False):
441
+ gr.HTML("""
442
+ <div style="padding: 20px; line-height: 1.8;">
443
+ <h3>Signal Processing-Based Synthesis</h3>
444
+ <p>
445
+ The AnomalyMachine-50K dataset is generated entirely using deterministic signal processing
446
+ techniques—no neural audio models are used. This ensures reproducibility, lightweight generation,
447
+ and freedom from copyright concerns.
448
+ </p>
449
+
450
+ <h4>1. Base Machine Sound Generation</h4>
451
+ <p>
452
+ Each machine type has a dedicated synthesis model:
453
+ </p>
454
+ <ul>
455
+ <li><strong>Fan:</strong> Broadband noise + rotating blade harmonics (50-200 Hz)</li>
456
+ <li><strong>Pump:</strong> Low-frequency rumble (20-80 Hz) + rhythmic pressure pulses</li>
457
+ <li><strong>Compressor:</strong> 60 Hz motor hum + harmonics with cyclic compression envelope</li>
458
+ <li><strong>Conveyor Belt:</strong> Rhythmic tapping + friction noise</li>
459
+ <li><strong>Electric Motor:</strong> Tonal fundamental (1200-3600 RPM) + harmonics + brush noise</li>
460
+ <li><strong>Valve:</strong> Turbulent flow noise + actuation clicks</li>
461
+ </ul>
462
+
463
+ <h4>2. Operating Condition Modulation</h4>
464
+ <p>
465
+ Conditions (idle, normal_load, high_load) modulate amplitude and harmonic content.
466
+ </p>
467
+
468
+ <h4>3. Anomaly Injection</h4>
469
+ <p>
470
+ Anomalies are injected via signal transformations:
471
+ </p>
472
+ <ul>
473
+ <li><strong>Bearing Fault:</strong> Periodic impulsive spikes</li>
474
+ <li><strong>Imbalance:</strong> Sinusoidal amplitude modulation</li>
475
+ <li><strong>Cavitation:</strong> Burst noise events</li>
476
+ <li><strong>Overheating:</strong> Gradually increasing high-frequency noise</li>
477
+ <li><strong>Obstruction:</strong> Intermittent amplitude drops + resonance shifts</li>
478
+ </ul>
479
+
480
+ <h4>4. Background Noise</h4>
481
+ <p>
482
+ Factory-floor ambience (pink noise + 60/120 Hz hum) is mixed at configurable SNR levels.
483
+ </p>
484
+
485
+ <p style="margin-top: 20px; font-style: italic;">
486
+ All synthesis is deterministic and reproducible with a fixed random seed.
487
+ </p>
488
+ </div>
489
+ """)
490
+ build_detect_tab()
491
+
492
+ with gr.Tab("📊 Explore Dataset"):
493
+ normal_gallery, anomaly_gallery, normal_images, anomaly_images = build_explore_tab()
494
+
495
+ build_footer()
496
+
497
+ app.launch(share=False, server_name="0.0.0.0", server_port=7860)
498
+
499
 
500
  if __name__ == "__main__":
501
+ main()