joakes90 commited on
Commit
990a00c
·
verified ·
1 Parent(s): 693dda4

Upload 8 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ gradio_app/examples/i2_360.wav filter=lfs diff=lfs merge=lfs -text
37
+ gradio_app/examples/single_two_stroke.wav filter=lfs diff=lfs merge=lfs -text
38
+ gradio_app/examples/v2_45.wav filter=lfs diff=lfs merge=lfs -text
gradio_app/README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Engine Sound Classifier
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 6.20.0
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # Engine Sound Classifier (early checkpoint)
13
+
14
+ Fine-tuned [PANNs CNN14](https://github.com/qiuqiangkong/audioset_tagging_cnn)
15
+ (pretrained on AudioSet) that classifies short engine-audio clips into one of
16
+ 34 configurations — cylinder count, layout, and stroke type (e.g. `i4`,
17
+ `v8_cross`, `single_two_stroke`, `2_rotor`).
18
+
19
+ This is a **first published checkpoint**, not a finished product. It's shared
20
+ to show the approach and current state, warts included.
21
+
22
+ ## Current performance
23
+
24
+ - Recording-level balanced accuracy: **~0.33** on held-out test data (random
25
+ guessing over 34 classes is ~0.03).
26
+ - Cylinder-family accuracy (does it at least get the cylinder count right) is
27
+ meaningfully higher than exact-class accuracy — most confusion is between
28
+ siblings within the same engine family, not wild misfires.
29
+ - Known weak spots: engines with many cylinders (8/12) are confused with
30
+ close siblings far more than 1–2 cylinder engines, which have more
31
+ distinctive, widely-spaced firing pulses. A couple of classes (`v12`,
32
+ `2_rotor`) remain poorly calibrated even after two rounds of fixes.
33
+
34
+ The full diagnostic history — what was tried, what broke, and why — lives in
35
+ `FINDINGS.md` in the training repo this Space was built from.
36
+
37
+ ## How predictions are made
38
+
39
+ Audio is resampled to 32 kHz mono, split into overlapping 2-second windows
40
+ (matching the training data's window length), and the model's per-window
41
+ class probabilities are averaged before picking the top prediction. This
42
+ "recording-level" pooling is consistently more accurate than scoring a single
43
+ window.
44
+
45
+ ## Deploying this Space
46
+
47
+ The fine-tuned checkpoint (`best_model_run_d_soft_weights.pth`, ~325 MB) is
48
+ not committed here. Either:
49
+
50
+ 1. Add it to this Space's repo via `git lfs`, or
51
+ 2. Upload it to a separate Hugging Face **model** repo and set the `HF_MODEL_REPO`
52
+ (and optionally `HF_MODEL_FILENAME`) Space secret/variable — `app.py` will
53
+ fetch it with `huggingface_hub.hf_hub_download` at startup.
gradio_app/app.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio demo for the PANNs CNN14 engine-sound classifier (34 engine
3
+ configurations, e.g. i4, v8_cross, single_two_stroke).
4
+
5
+ This is an early checkpoint (Run D: SpecAugment-fixed + softened class
6
+ weights). Recording-level balanced accuracy is ~0.33 on the held-out test
7
+ set -- well above the 1/34 ~= 0.03 random baseline, but far from solved.
8
+ See the README on this Space for known failure modes before trusting a
9
+ prediction.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+
16
+ import gradio as gr
17
+ import torch
18
+ import torchaudio
19
+ from panns_inference import AudioTagging
20
+
21
+ APP_DIR = Path(__file__).parent
22
+ SAMPLE_RATE = 32000
23
+ WINDOW_SEC = 2.0 # matches the training manifest's window length
24
+ HOP_SEC = 1.0 # 50% overlap keeps short clips from being a single window
25
+
26
+ # Local file takes priority (e.g. copied alongside app.py, or git-lfs'd into
27
+ # the Space). Falls back to a Hugging Face *model* repo so the Space repo
28
+ # itself doesn't have to carry a 300+MB checkpoint in git.
29
+ CHECKPOINT_FILENAME = "best_model_run_d_soft_weights.pth"
30
+ HF_MODEL_REPO = os.environ.get("HF_MODEL_REPO") # e.g. "username/engine-cnn14"
31
+ HF_MODEL_FILENAME = os.environ.get("HF_MODEL_FILENAME", CHECKPOINT_FILENAME)
32
+
33
+ device = "cuda" if torch.cuda.is_available() else "cpu"
34
+
35
+ with open(APP_DIR / "class_names.json") as f:
36
+ CLASS_NAMES = json.load(f)["class_names"]
37
+
38
+
39
+ def _resolve_checkpoint_path() -> str:
40
+ local_path = APP_DIR / CHECKPOINT_FILENAME
41
+ if local_path.exists():
42
+ return str(local_path)
43
+ if HF_MODEL_REPO:
44
+ from huggingface_hub import hf_hub_download
45
+
46
+ return hf_hub_download(repo_id=HF_MODEL_REPO, filename=HF_MODEL_FILENAME)
47
+ raise FileNotFoundError(
48
+ f"No checkpoint at {local_path} and HF_MODEL_REPO is not set. Either "
49
+ f"place {CHECKPOINT_FILENAME} next to app.py, or set HF_MODEL_REPO to "
50
+ "a Hugging Face model repo that holds it."
51
+ )
52
+
53
+
54
+ def _load_model() -> torch.nn.Module:
55
+ # checkpoint_path=None lets panns_inference fetch/cache its own pretrained
56
+ # AudioSet weights -- this mirrors exactly how training started, then we
57
+ # overwrite the head and load our fine-tuned state on top.
58
+ at = AudioTagging(checkpoint_path=None, device=device)
59
+ # AudioTagging only wraps the model in DataParallel on CUDA, so on a
60
+ # CPU-only Space (the common free tier) `.module` would not exist.
61
+ backbone = at.model.module if isinstance(at.model, torch.nn.DataParallel) else at.model
62
+ backbone.fc_audioset = torch.nn.Linear(
63
+ in_features=backbone.fc_audioset.in_features, out_features=len(CLASS_NAMES)
64
+ )
65
+ state_dict = torch.load(_resolve_checkpoint_path(), map_location=device)
66
+ backbone.load_state_dict(state_dict)
67
+ backbone.to(device)
68
+ backbone.eval()
69
+ return backbone
70
+
71
+
72
+ model = _load_model()
73
+
74
+
75
+ def _load_waveform(path: str) -> torch.Tensor:
76
+ waveform, sr = torchaudio.load(path)
77
+ if waveform.shape[0] > 1:
78
+ waveform = waveform.mean(dim=0, keepdim=True)
79
+ if sr != SAMPLE_RATE:
80
+ waveform = torchaudio.functional.resample(waveform, sr, SAMPLE_RATE)
81
+ return waveform.squeeze(0)
82
+
83
+
84
+ def _windows(waveform: torch.Tensor) -> torch.Tensor:
85
+ win = int(WINDOW_SEC * SAMPLE_RATE)
86
+ hop = int(HOP_SEC * SAMPLE_RATE)
87
+ if waveform.shape[-1] < win:
88
+ waveform = torch.nn.functional.pad(waveform, (0, win - waveform.shape[-1]))
89
+ return waveform.unsqueeze(0)
90
+ starts = list(range(0, waveform.shape[-1] - win + 1, hop))
91
+ if starts[-1] + win < waveform.shape[-1]:
92
+ starts.append(waveform.shape[-1] - win)
93
+ return torch.stack([waveform[s : s + win] for s in starts])
94
+
95
+
96
+ @torch.inference_mode()
97
+ def classify(audio_path: str):
98
+ if audio_path is None:
99
+ return None, "Upload or record a clip first."
100
+
101
+ waveform = _load_waveform(audio_path)
102
+ duration_sec = waveform.shape[-1] / SAMPLE_RATE
103
+ batch = _windows(waveform).to(device)
104
+
105
+ logits = model(batch)["embedding"]
106
+ logits = model.fc_audioset(logits)
107
+ # mean_prob pooling: bounded per window, so one loud/confident window
108
+ # can't dominate the recording-level call (see FINDINGS.md).
109
+ probs = torch.softmax(logits, dim=1).mean(dim=0)
110
+
111
+ top = torch.topk(probs, k=min(5, len(CLASS_NAMES)))
112
+ result = {CLASS_NAMES[i]: float(p) for p, i in zip(top.values, top.indices)}
113
+ details = (
114
+ f"{duration_sec:.1f}s clip -> {batch.shape[0]} overlapping "
115
+ f"{WINDOW_SEC:.0f}s window(s), averaged."
116
+ )
117
+ return result, details
118
+
119
+
120
+ EXAMPLES_DIR = APP_DIR / "examples"
121
+ examples = [str(p) for p in sorted(EXAMPLES_DIR.glob("*.wav"))]
122
+
123
+ DESCRIPTION = """
124
+ Fine-tuned PANNs CNN14 (pretrained on AudioSet), classifying short engine
125
+ audio clips into one of 34 configurations (cylinder count/layout/stroke,
126
+ e.g. `i4`, `v8_cross`, `single_two_stroke`, `2_rotor`).
127
+
128
+ **Status: early, ambitious, not accurate.** Recording-level balanced
129
+ accuracy is ~0.33 on held-out data (random guessing over 34 classes is
130
+ ~0.03). Cylinder-family-level accuracy (does it at least get the cylinder
131
+ count right) is meaningfully higher. Known weak spots: engines with many
132
+ cylinders (8/12) are confused with siblings far more than 1-2 cylinder
133
+ engines; a few classes (`v12`, `2_rotor`) are still poorly calibrated.
134
+ Full write-up of the diagnosis and next steps is in the project's
135
+ `FINDINGS.md`.
136
+ """
137
+
138
+ demo = gr.Interface(
139
+ fn=classify,
140
+ inputs=gr.Audio(type="filepath", label="Engine audio (upload or record)"),
141
+ outputs=[
142
+ gr.Label(num_top_classes=5, label="Predicted configuration"),
143
+ gr.Textbox(label="Details", interactive=False),
144
+ ],
145
+ examples=examples if examples else None,
146
+ cache_examples=False,
147
+ title="Engine Sound Classifier (early checkpoint)",
148
+ description=DESCRIPTION,
149
+ )
150
+
151
+ if __name__ == "__main__":
152
+ demo.launch()
gradio_app/best_model_run_d_soft_weights.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a8ce6d399f0d6f49cd3ecfa41503fca52ccb65a1a8b28492babcb4e47ef23ca8
3
+ size 323403171
gradio_app/class_names.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "class_names": [
3
+ "2_rotor",
4
+ "h12",
5
+ "h2",
6
+ "h4",
7
+ "h6",
8
+ "i2_180",
9
+ "i2_180_two_stroke",
10
+ "i2_270",
11
+ "i2_360",
12
+ "i2_360_two_stroke",
13
+ "i3",
14
+ "i4",
15
+ "i4_crossplane",
16
+ "i4_diesel",
17
+ "i5",
18
+ "i5_diesel",
19
+ "i6",
20
+ "i6_diesel",
21
+ "single_four_stroke",
22
+ "single_two_stroke",
23
+ "v10_72",
24
+ "v12",
25
+ "v2_45",
26
+ "v2_90",
27
+ "v4",
28
+ "v4_two_stroke",
29
+ "v6_120",
30
+ "v6_60",
31
+ "v6_90_even",
32
+ "v8_cross",
33
+ "v8_diesel",
34
+ "v8_flat",
35
+ "v8_voodoo",
36
+ "vr6"
37
+ ]
38
+ }
gradio_app/examples/i2_360.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:09928bcb73b6b8a35531d651e71b1a14bd0d80a4d7009fbe367075fb2f56909f
3
+ size 512078
gradio_app/examples/single_two_stroke.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:29ade754afe5e7358efba7deb28323018c19fd1f750a82d34fc35d7861677399
3
+ size 512078
gradio_app/examples/v2_45.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:909d6080bf8f94b78611e5fd46e9d9740a1c7b493ffe9e09593c068fb2af8015
3
+ size 512078
gradio_app/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ torch
3
+ torchaudio
4
+ panns_inference
5
+ torchlibrosa
6
+ matplotlib
7
+ numpy
8
+ huggingface_hub