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