Shubham2004 commited on
Commit
705fa30
Β·
verified Β·
1 Parent(s): 9eeeabb

Upload 3 files

Browse files
Files changed (3) hide show
  1. childfluency.py +140 -0
  2. lpe_module.py +58 -0
  3. wavlm_encoder.py +71 -0
childfluency.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/models/childfluency.py
2
+ """
3
+ ChildFluency-Net β€” Full Model
4
+
5
+ Combines WavLM acoustic encoder + LPE linguistic encoder
6
+ into a unified architecture with dual SLD/OD classification heads.
7
+
8
+ Architecture:
9
+ [WavLM β†’ 256-dim] + [LPE β†’ 32-dim]
10
+ ↓ concat [288-dim]
11
+ Shared MLP [64-dim]
12
+ ↓
13
+ [SLD Head] [OD Head]
14
+ ↓
15
+ Mean pool across windows β†’ SLD rate % (clinical output)
16
+
17
+ Clinical output:
18
+ SLD rate > 3% β†’ refer for specialist assessment
19
+ """
20
+
21
+ import torch
22
+ import torch.nn as nn
23
+ from typing import Dict
24
+ from wavlm_encoder import WavLMEncoder
25
+ from lpe_module import LPEModule
26
+
27
+
28
+ class ChildFluencyNet(nn.Module):
29
+
30
+ def __init__(
31
+ self,
32
+ wavlm_name : str = "microsoft/wavlm-large",
33
+ acoustic_dim : int = 256,
34
+ lpe_dim : int = 32,
35
+ hidden_dim : int = 128,
36
+ dropout : float = 0.3
37
+ ):
38
+ super().__init__()
39
+
40
+ # Branch A β€” acoustic
41
+ self.acoustic_encoder = WavLMEncoder(
42
+ model_name = wavlm_name,
43
+ output_dim = acoustic_dim
44
+ )
45
+
46
+ # Branch B β€” linguistic position
47
+ self.lpe_module = LPEModule(
48
+ input_dim = 6,
49
+ output_dim = lpe_dim
50
+ )
51
+
52
+ fusion_dim = acoustic_dim + lpe_dim # 256 + 32 = 288
53
+
54
+ # Shared MLP after fusion
55
+ self.shared_mlp = nn.Sequential(
56
+ nn.Linear(fusion_dim, hidden_dim),
57
+ nn.GELU(),
58
+ nn.Dropout(dropout),
59
+ nn.Linear(hidden_dim, hidden_dim // 2) # β†’ 64-dim
60
+ )
61
+
62
+ # Head 1 β€” SLD (Stuttering-Like Disfluencies)
63
+ # Detects: blocks, prolongations, sound/syllable repetitions
64
+ # These are PATHOLOGICAL β€” what clinicians diagnose
65
+ self.sld_head = nn.Linear(hidden_dim // 2, 1)
66
+
67
+ # Head 2 β€” OD (Other Disfluencies)
68
+ # Detects: word repetitions, interjections (um, uh)
69
+ # These are NORMAL β€” present in all speakers
70
+ self.od_head = nn.Linear(hidden_dim // 2, 1)
71
+
72
+ def forward(
73
+ self,
74
+ audio : torch.Tensor, # [batch, 64000]
75
+ lpe_features : torch.Tensor # [batch, 6]
76
+ ) -> Dict[str, torch.Tensor]:
77
+ """
78
+ Args:
79
+ audio : raw waveform, 4 seconds at 16kHz
80
+ lpe_features : 6 linguistic position features per window
81
+
82
+ Returns dict with:
83
+ sld_logit : [batch] raw score for SLD detection
84
+ od_logit : [batch] raw score for OD detection
85
+ embedding : [batch, 64] shared representation
86
+ """
87
+ # Branch A
88
+ acoustic_emb = self.acoustic_encoder(audio) # [batch, 256]
89
+
90
+ # Branch B
91
+ lpe_emb = self.lpe_module(lpe_features) # [batch, 32]
92
+
93
+ # Fuse
94
+ fused = torch.cat([acoustic_emb, lpe_emb], dim=-1) # [batch, 288]
95
+ shared = self.shared_mlp(fused) # [batch, 64]
96
+
97
+ return {
98
+ "sld_logit" : self.sld_head(shared).squeeze(-1), # [batch]
99
+ "od_logit" : self.od_head(shared).squeeze(-1), # [batch]
100
+ "embedding" : shared # [batch, 64]
101
+ }
102
+
103
+ @torch.no_grad()
104
+ def predict_recording(self, window_outputs: list) -> Dict:
105
+ """
106
+ Aggregates window-level predictions into recording-level diagnosis.
107
+ Called during inference on a full recording.
108
+
109
+ Args:
110
+ window_outputs : list of dicts from forward(), one per window
111
+
112
+ Returns:
113
+ sld_rate : float SLD rate % (clinical diagnostic metric)
114
+ has_stutter : bool True if SLD rate > 3%
115
+ sld_timeline: list per-window SLD probability (for visualization)
116
+ """
117
+ sld_probs = torch.sigmoid(
118
+ torch.stack([o["sld_logit"] for o in window_outputs])
119
+ )
120
+ od_probs = torch.sigmoid(
121
+ torch.stack([o["od_logit"] for o in window_outputs])
122
+ )
123
+
124
+ sld_rate = (sld_probs > 0.5).float().mean().item() * 100.0
125
+
126
+ return {
127
+ "sld_rate" : round(sld_rate, 2),
128
+ "has_stutter" : sld_rate > 3.0,
129
+ "mean_sld_prob": round(sld_probs.mean().item(), 4),
130
+ "mean_od_prob" : round(od_probs.mean().item(), 4),
131
+ "sld_timeline" : sld_probs.cpu().tolist()
132
+ }
133
+
134
+ def count_params(self):
135
+ trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
136
+ total = sum(p.numel() for p in self.parameters())
137
+ print(f"ChildFluencyNet")
138
+ print(f" Trainable : {trainable:,}")
139
+ print(f" Total : {total:,}")
140
+ print(f" Frozen : {total - trainable:,}")
lpe_module.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/models/lpe_module.py
2
+ """
3
+ Linguistic Position Encoding (LPE) Module
4
+
5
+ Core novel contribution of ChildFluency-Net.
6
+
7
+ Encodes 6 loci-of-stuttering features into a 32-dim vector.
8
+ These features are pre-computed by build_features.py using
9
+ WhisperX transcription + spaCy linguistic parsing.
10
+
11
+ Clinical basis (Bloodstein 1960, Bernstein-Ratner 1997):
12
+ Stuttering is 3-5x more likely on:
13
+ - Sentence-initial positions
14
+ - Content words (nouns, verbs) vs function words
15
+ - Longer, rarer words
16
+ - Near clause boundaries (cognitive planning spikes)
17
+ - In longer, more complex utterances (higher MLU)
18
+
19
+ Input features [6 scalars, all in 0-1 range]:
20
+ 0: sentence position (0=initial=high risk, 1=final)
21
+ 1: word class (1=content, 0=function)
22
+ 2: syllable count (normalized, max=6 syllables)
23
+ 3: clause boundary prox (1=near boundary)
24
+ 4: word rarity (1=rare=high risk, 0=common)
25
+ 5: MLU (sentence length / 20)
26
+ """
27
+
28
+ import torch
29
+ import torch.nn as nn
30
+
31
+
32
+ class LPEModule(nn.Module):
33
+
34
+ def __init__(
35
+ self,
36
+ input_dim : int = 6,
37
+ output_dim : int = 32
38
+ ):
39
+ super().__init__()
40
+
41
+ # Small MLP: 6 β†’ 16 β†’ 32
42
+ # Kept intentionally small β€” 6 features don't need
43
+ # a large network, just a learned non-linear transformation
44
+ self.encoder = nn.Sequential(
45
+ nn.Linear(input_dim, 16),
46
+ nn.ReLU(),
47
+ nn.Linear(16, output_dim)
48
+ )
49
+
50
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
51
+ """
52
+ Args:
53
+ x : [batch, 6] pre-computed LPE feature vectors
54
+
55
+ Returns:
56
+ : [batch, 32] LPE embeddings
57
+ """
58
+ return self.encoder(x)
wavlm_encoder.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/models/wavlm_encoder.py
2
+ """
3
+ WavLM Acoustic Encoder
4
+
5
+ Takes raw 4-second audio β†’ outputs 256-dim acoustic embedding.
6
+
7
+ WavLM-Large is chosen over Wav2Vec2 because:
8
+ - Trained with masked speech prediction + denoising objective
9
+ - More robust to background noise in clinical recordings
10
+ - State-of-the-art on SUPERB benchmark across all speech tasks
11
+ """
12
+
13
+ import torch
14
+ import torch.nn as nn
15
+ from transformers import WavLMModel
16
+
17
+
18
+ class WavLMEncoder(nn.Module):
19
+
20
+ def __init__(
21
+ self,
22
+ model_name : str = "microsoft/wavlm-large",
23
+ output_dim : int = 256,
24
+ freeze_cnn : bool = True
25
+ ):
26
+ super().__init__()
27
+
28
+ # Load pretrained WavLM-Large
29
+ self.wavlm = WavLMModel.from_pretrained(model_name)
30
+
31
+ # Freeze the CNN feature extractor (low-level, no benefit fine-tuning)
32
+ # Only the transformer layers get fine-tuned
33
+ if freeze_cnn:
34
+ for param in self.wavlm.feature_extractor.parameters():
35
+ param.requires_grad = False
36
+
37
+ # WavLM-Large hidden size = 1024
38
+ wavlm_dim = self.wavlm.config.hidden_size
39
+
40
+ # Project 1024 β†’ 256
41
+ self.projection = nn.Sequential(
42
+ nn.Linear(wavlm_dim, 512),
43
+ nn.GELU(),
44
+ nn.Dropout(0.1),
45
+ nn.Linear(512, output_dim)
46
+ )
47
+
48
+ def forward(self, audio: torch.Tensor) -> torch.Tensor:
49
+ """
50
+ Args:
51
+ audio : [batch, time_samples] raw waveform at 16kHz
52
+ 4 seconds = 64,000 samples
53
+
54
+ Returns:
55
+ embedding : [batch, 256]
56
+ """
57
+ # WavLM transformer layers β†’ frame-level features
58
+ out = self.wavlm(input_values=audio)
59
+ hidden = out.last_hidden_state # [batch, frames, 1024]
60
+
61
+ # Mean pool all frames into one vector per clip
62
+ pooled = hidden.mean(dim=1) # [batch, 1024]
63
+
64
+ # Project to output dim
65
+ return self.projection(pooled) # [batch, 256]
66
+
67
+ def trainable_params(self):
68
+ trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
69
+ total = sum(p.numel() for p in self.parameters())
70
+ print(f"WavLMEncoder β€” trainable: {trainable:,} / total: {total:,} "
71
+ f"({100*trainable/total:.1f}%)")