mshaiel2004's picture
Create main.py
b5fa15a verified
Raw History Blame Contribute Delete
6.92 kB
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)
]
}
}