Spaces:
Sleeping
Sleeping
File size: 6,919 Bytes
b5fa15a | 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 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | import torch
import numpy as np
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from core.tensor_bridge import TensorBridge
from core.amlm import load_amlm_model
from core.aeg import load_aeg_model
from core.triage import TriageEngineV2
# ==========================================
# 1. SETUP & CONFIG
# ==========================================
app = FastAPI(title="Clario Training Module", version="1.0.0")
# Global Model Containers
MODELS = {}
DEVICE = "cpu" # Force CPU for Hugging Face Spaces (unless GPU upgraded)
# Paths to weights (Uploaded in Phase 5)
AMLM_PATH = "models/amlm_v5.pth"
AEG_PATH = "models/aeg_v5.pth"
# Activity Format Map (Must match AEG Training Data)
FORMAT_MAP = [
"Auditory_Bombardment", "Phoneme_Isolation", "Minimal_Pairs",
"Syllable_Chaining", "Carrier_Phrases", "Pacing",
"Shadowing", "Speed_Drills"
]
STRUCTURE_MAP = ["CV", "VC", "CVC", "VCV", "CVCV", "CCV", "VCC", "Complex"]
# ==========================================
# 2. DATA MODELS (Pydantic)
# ==========================================
class TensorUpdatePayload(BaseModel):
current_tensor: Optional[List[List[List[float]]]] = None # 3x64x9 List
new_results: List[Dict[str, Any]] # From Backend or Practice
class UserStats(BaseModel):
severity: float # 0.0 - 1.0
fatigue: float # 0.0 - 1.0
age: float # Normalized 0.0 - 1.0
delta: float # Velocity (-0.5 to +0.5)
class StrategyGeneratePayload(BaseModel):
current_tensor: List[List[List[float]]] # The raw observed tensor
user_stats: UserStats
force_easier: bool = False # The "Frustration Valve"
# ==========================================
# 3. LIFECYCLE EVENTS
# ==========================================
@app.on_event("startup")
async def startup_event():
print("๐ Initializing Clario Training Module...")
try:
# Load Cognitive Core
MODELS['amlm'] = load_amlm_model(AMLM_PATH, DEVICE)
MODELS['aeg'] = load_aeg_model(AEG_PATH, DEVICE)
MODELS['bridge'] = TensorBridge()
MODELS['triage'] = TriageEngineV2(DEVICE)
print("โ
All Systems Operational.")
except Exception as e:
print(f"โ Startup Failed: {e}")
# ==========================================
# 4. HELPER FUNCTIONS
# ==========================================
def _prepare_amlm_input(raw_tensor_list):
"""
Converts raw 3x64x9 list into Batchx6x64x9 Tensor for AMLM.
Generates visibility mask based on non-zero entries.
"""
# 1. Convert to Numpy (3, 64, 9)
val_map = np.array(raw_tensor_list, dtype=np.float32)
# 2. Generate Mask (Assumption: Val > 0 means "Observed Error")
# Zeros are treated as "Ambiguous/Untested" for the Inference Engine
vis_map = (val_map > 0.0).astype(np.float32)
# 3. Concatenate (6, 64, 9)
input_numpy = np.concatenate([val_map, vis_map], axis=0)
# 4. Add Batch Dim & Convert to Torch
return torch.FloatTensor(input_numpy).unsqueeze(0).to(DEVICE)
# ==========================================
# 5. ENDPOINTS
# ==========================================
@app.post("/tensor/update")
async def update_tensor(payload: TensorUpdatePayload):
"""
THE FAST LOOP: Updates the patient's digital twin with new error data.
"""
bridge = MODELS['bridge']
# Run Bridge Logic
updated_tensor = bridge.update_tensor(
payload.current_tensor,
payload.new_results
)
return {
"status": "success",
"message": "Tensor updated successfully.",
"updated_tensor": updated_tensor
}
@app.post("/strategy/generate")
async def generate_strategy(payload: StrategyGeneratePayload):
"""
THE SLOW LOOP: Generates a full therapy session blueprint.
"""
amlm = MODELS['amlm']
aeg = MODELS['aeg']
triage = MODELS['triage']
# 1. AMLM Inference (Fill Gaps)
# We use the raw tensor to infer the full clinical image
amlm_input = _prepare_amlm_input(payload.current_tensor)
with torch.no_grad():
diagnosis_map, _ = amlm(amlm_input)
# Squeeze batch dim -> (3, 64, 9) Numpy
diag_np = diagnosis_map[0].cpu().numpy()
# 2. Triage (Find Focus)
all_diagnoses = triage.scan_tensor(diag_np)
if not all_diagnoses:
return {"status": "healthy", "message": "No significant errors detected."}
focus_diag = all_diagnoses[0] # Highest priority issue
# 3. Prepare Stats (Frustration Valve Logic)
stats = payload.user_stats
effective_fatigue = 1.0 if payload.force_easier else stats.fatigue
# 4. AEG Input Preparation
aeg_inputs = triage.prepare_aeg_input(
focus_diag,
all_diagnoses,
[stats.severity, effective_fatigue, stats.age, stats.delta]
)
# 5. AEG Inference (Generate Blueprint)
with torch.no_grad():
p_vol, p_diff, p_fmt, p_targ, p_struct, p_forbid, p_dyn = aeg(
aeg_inputs['error_idx'], aeg_inputs['major_idx'], aeg_inputs['pos_idx'],
aeg_inputs['cluster'], aeg_inputs['trap'], aeg_inputs['feat'],
aeg_inputs['flags'], aeg_inputs['stats']
)
# 6. Decode Outputs
vol = int(torch.clamp(p_vol, 3, 10).item())
# Extract Sequences (Slice by volume)
diff_seq = torch.argmax(p_diff, dim=2)[0].tolist()[:vol]
# Difficulty is 0-4 internally, map to 1-5 for UI
diff_seq_ui = [d + 1 for d in diff_seq]
fmt_indices = torch.argmax(p_fmt, dim=2)[0].tolist()[:vol]
fmt_names = [FORMAT_MAP[i] for i in fmt_indices]
struct_indices = torch.argmax(p_struct, dim=2)[0].tolist()[:vol]
struct_names = [STRUCTURE_MAP[i] for i in struct_indices]
# Target Phonemes (Multi-hot)
target_logits = p_targ[0][:vol] # (Vol, 64)
target_phonemes = []
from core.triage import PHONEMES # Helper to map back
for step_logits in target_logits:
# Sigmoid > 0.5 implies active target
active_indices = (torch.sigmoid(step_logits) > 0.5).nonzero(as_tuple=True)[0].tolist()
targets = [PHONEMES[i] for i in active_indices]
if not targets: targets = focus_diag.active_phonemes # Fallback to main targets
target_phonemes.append(targets)
return {
"status": "generated",
"focus_diagnosis": {
"name": focus_diag.error_name,
"type": focus_diag.major_type,
"severity": focus_diag.severity
},
"blueprint": {
"volume": vol,
"plan": [
{
"step": i+1,
"difficulty": diff_seq_ui[i],
"format": fmt_names[i],
"structure": struct_names[i],
"targets": target_phonemes[i]
}
for i in range(vol)
]
}
} |