mshaiel2004 commited on
Commit
728be4b
·
verified ·
1 Parent(s): 7ad2c42

Create triage.py

Browse files
Files changed (1) hide show
  1. core/triage.py +179 -0
core/triage.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from dataclasses import dataclass
4
+ from typing import List, Dict, Tuple
5
+
6
+ # ==========================================
7
+ # 1. CONSTANTS (Aligned with V7/V5 Specs)
8
+ # ==========================================
9
+ PHONEMES = [
10
+ 'p', 'pʰ', 'b', 'bʰ', 't', 'tʰ', 'd', 'dʰ', 't̪', 't̪ʰ', 'd̪', 'd̪ʱ',
11
+ 'ʈ', 'ɖ', 'k', 'kʰ', 'g', 'ɡʰ', 'q', 'ʔ', 'f', 'v', 's', 'z',
12
+ 'ʃ', 'ʒ', 'ʂ', 'ʐ', 'x', 'ɣ', 'h', 'θ', 'ð', 'tʃ', 'dʒ', 'm',
13
+ 'mʰ', 'n', 'nʰ', 'ŋ', 'ɳ', 'l', 'lʰ', 'r', 'rʰ', 'ɽ', 'ɽʰ', 'w',
14
+ 'j', 'a', 'aː', 'i', 'iː', 'u', 'uː', 'e', 'eː', 'o', 'oː',
15
+ 'ə', 'æ', 'ɔ', 'ɪ', 'ʊ'
16
+ ]
17
+ P2I = {p: i for i, p in enumerate(PHONEMES)}
18
+
19
+ ERRORS = ["Sub", "Front", "Stop", "Glide", "Clust", "Epen", "Block", "Prolong", "Repeat"]
20
+
21
+ # Mapping Error Index (0-8) to Major Type Category
22
+ # 0-4 = Articulation, 5 = Motor, 6-8 = Fluency
23
+ MAJOR_MAP = {
24
+ 0: "Artic", 1: "Artic", 2: "Artic", 3: "Artic", 4: "Artic",
25
+ 5: "Motor", 6: "Fluency", 7: "Fluency", 8: "Fluency"
26
+ }
27
+
28
+ # Clinical Priority: Treat Motor issues first, then Fluency, then Artic
29
+ PRIORITY = {"Motor": 3, "Fluency": 2, "Artic": 1}
30
+
31
+ @dataclass
32
+ class ClinicalDiagnosis:
33
+ id: int
34
+ error_idx: int
35
+ error_name: str
36
+ major_type: str
37
+ severity: float
38
+ active_indices: List[int]
39
+ active_phonemes: List[str]
40
+ position_idx: int
41
+
42
+ class TriageEngineV2:
43
+ def __init__(self, device='cpu'):
44
+ self.device = device
45
+
46
+ def scan_tensor(self, tensor_3d: np.ndarray) -> List[ClinicalDiagnosis]:
47
+ """
48
+ Scans the 64x9x3 AMLM output map for clinical syndromes.
49
+ Returns a sorted list of diagnoses.
50
+ """
51
+ diagnoses = []
52
+ uid_counter = 0
53
+
54
+ # tensor_3d shape: (3, 64, 9) -> (Channels, Phonemes, Errors)
55
+
56
+ for err_idx in range(9):
57
+ # Check maximum probability for this error type across all phonemes/positions
58
+ slice_max = np.max(tensor_3d[:, :, err_idx])
59
+
60
+ # STRICT FILTERING: Threshold > 0.6 means the AMLM is confident
61
+ if slice_max > 0.6:
62
+ # Find which phonemes are affected
63
+ # We collapse the channel dim (positions) to see if the phoneme is affected anywhere
64
+ hits = np.where(tensor_3d[:, :, err_idx].max(axis=0) > 0.6)[0]
65
+
66
+ # Filter specific weird edge cases (e.g. Gliding on vowels) if necessary
67
+ # Logic from Integration.ipynb: valid_hits = [int(h) for h in hits if not (err_idx == 3 and h >= 48)]
68
+ valid_hits = [int(h) for h in hits]
69
+
70
+ # NOISE REDUCTION: Cap at Top 10 worst errors per category
71
+ if len(valid_hits) > 10:
72
+ severity_scores = []
73
+ for h in valid_hits:
74
+ score = tensor_3d[:, h, err_idx].max()
75
+ severity_scores.append((score, h))
76
+ severity_scores.sort(key=lambda x: x[0], reverse=True)
77
+ valid_hits = [x[1] for x in severity_scores[:10]]
78
+
79
+ valid_hits.sort()
80
+
81
+ if not valid_hits: continue
82
+
83
+ # Determine dominant position (Initial, Medial, or Final)
84
+ pos_sums = tensor_3d[:, valid_hits, err_idx].sum(axis=1)
85
+ dom_pos = np.argmax(pos_sums)
86
+
87
+ diag = ClinicalDiagnosis(
88
+ id=uid_counter,
89
+ error_idx=err_idx,
90
+ error_name=ERRORS[err_idx],
91
+ major_type=MAJOR_MAP[err_idx],
92
+ severity=float(slice_max),
93
+ active_indices=valid_hits,
94
+ active_phonemes=[PHONEMES[i] for i in valid_hits],
95
+ position_idx=int(dom_pos)
96
+ )
97
+ diagnoses.append(diag)
98
+ uid_counter += 1
99
+
100
+ # Sort by Priority (Motor > Fluency > Artic) then Severity
101
+ return sorted(diagnoses, key=lambda x: (PRIORITY[x.major_type], x.severity), reverse=True)
102
+
103
+ def prepare_aeg_input(self, focus_diag: ClinicalDiagnosis, all_diagnoses: List[ClinicalDiagnosis], user_stats: List[float]) -> Dict[str, torch.Tensor]:
104
+ """
105
+ Generates the 162-Dim Input Vector for the AEG Neural Network.
106
+ Adheres strictly to 'Data Generator.py' logic.
107
+ """
108
+ # 1. Cluster Mask (Targets)
109
+ # Which sounds should we target in therapy? (The ones in the focus diagnosis)
110
+ cluster_mask = [0.0] * 64
111
+ target_set = set(focus_diag.active_indices)
112
+ for idx in focus_diag.active_indices:
113
+ cluster_mask[idx] = 1.0
114
+
115
+ # 2. Trap Mask (Forbidden Sounds)
116
+ # Sounds we must NOT use to avoid negative reinforcement.
117
+ trap_mask = [0.0] * 64
118
+
119
+ def attempt_trap(idx):
120
+ # Logic: A sound cannot be a Trap if it is currently a Target.
121
+ if idx not in target_set:
122
+ trap_mask[idx] = 1.0
123
+
124
+ # A. Standard Traps (Rule-based from Data Generator)
125
+ if focus_diag.error_name == "Front": # If Fronting K->T
126
+ # Trap T/D so we don't ask user to say them while trying to learn K
127
+ if any(p in ['k', 'g', 'kʰ', 'ɡʰ'] for p in focus_diag.active_phonemes):
128
+ for x in ['t', 'd', 'tʰ', 'dʰ']:
129
+ if x in P2I: attempt_trap(P2I[x])
130
+
131
+ # B. Comorbidity Traps (Dynamic)
132
+ # If user also has a Lisp (Sub S), Trap 'S' even if we are treating Fronting.
133
+ for other in all_diagnoses:
134
+ if other.id != focus_diag.id:
135
+ for idx in other.active_indices:
136
+ attempt_trap(idx)
137
+
138
+ # 3. Phonetic Features (8-Dim Vector)
139
+ # Describes the target sound (e.g., +Stop, +Velar)
140
+ feat = [0.0] * 8
141
+ t_str = "".join(focus_diag.active_phonemes)
142
+
143
+ if any(x in t_str for x in "pbtdkgʈɖʔ"): feat[0] = 1.0 # Stop
144
+ if any(x in t_str for x in "fvszʃʒxhθð"): feat[1] = 1.0 # Fricative
145
+ if any(x in t_str for x in "lrw"): feat[2] = 1.0 # Liquid
146
+ if any(x in t_str for x in "mnŋ"): feat[3] = 1.0 # Nasal
147
+ if any(x in t_str for x in "bdgvzʒðmnlrw"): feat[4] = 1.0 # Voiced
148
+ if any(x in t_str for x in "pbfvmθð"): feat[5] = 1.0 # Front
149
+ if any(x in t_str for x in "kgŋxɣhʔ"): feat[6] = 1.0 # Back
150
+ if len(focus_diag.active_indices) > 1 or focus_diag.error_name == "Clust": feat[7] = 1.0 # Cluster
151
+
152
+ # 4. Flags & Stats
153
+ trap_count = sum(trap_mask)
154
+ # user_stats expects: [Severity, Fatigue, Age, Delta]
155
+ sev, fat, age, delta = user_stats
156
+
157
+ # Binary Flags for the Network
158
+ is_systemic = 1.0 if len(focus_diag.active_indices) > 1 else 0.0
159
+ is_trap = 1.0 if trap_count > 0 else 0.0
160
+ is_comorb = 1.0 if len(all_diagnoses) > 1 else 0.0
161
+ is_child = 1.0 if age < 0.3 else 0.0 # Normalized age (0.3 ~ 7 years old)
162
+ is_maint = 1.0 if sev <= 0.2 else 0.0
163
+ is_fatigued = 1.0 if fat > 0.8 else 0.0
164
+
165
+ flags = [is_systemic, is_trap, is_comorb, is_child, is_maint, is_fatigued]
166
+
167
+ maj_int = {"Artic": 0, "Fluency": 1, "Motor": 2}[focus_diag.major_type]
168
+
169
+ # Convert to PyTorch Tensors on correct Device
170
+ return {
171
+ "error_idx": torch.tensor([focus_diag.error_idx]).to(self.device),
172
+ "major_idx": torch.tensor([maj_int]).to(self.device),
173
+ "pos_idx": torch.tensor([focus_diag.position_idx]).to(self.device),
174
+ "cluster": torch.tensor([cluster_mask], dtype=torch.float32).to(self.device),
175
+ "trap": torch.tensor([trap_mask], dtype=torch.float32).to(self.device),
176
+ "feat": torch.tensor([feat], dtype=torch.float32).to(self.device),
177
+ "flags": torch.tensor([flags], dtype=torch.float32).to(self.device),
178
+ "stats": torch.tensor([user_stats], dtype=torch.float32).to(self.device)
179
+ }