import base64,io,json,re,threading from pathlib import Path import numpy as np from PIL import Image import torch from .networks import SketchEncoder,SpikingReadout,PongSNN,CATEGORIES,NavigationPolicy ROOT=Path(__file__).resolve().parents[1] DEVICE='mps' if torch.backends.mps.is_available() else 'cpu' torch.set_num_threads(12) LOCK=threading.RLock() CACHE={} def decode_image(data): if not isinstance(data,str) or len(data)>12_000_000: raise ValueError('Image is too large.') raw=base64.b64decode(data.split(',',1)[-1],validate=True) im=Image.open(io.BytesIO(raw)) if im.width*im.height>16_000_000: raise ValueError('Image exceeds 16 megapixels.') return im.convert('RGB') def encode_image(im): b=io.BytesIO(); im.save(b,format='PNG') return 'data:image/png;base64,'+base64.b64encode(b.getvalue()).decode() def load(name): if name in CACHE: return CACHE[name] path=ROOT/'models'/'hf'/name if not (path/'config.json').exists() and name!='image': raise RuntimeError(f'{name} model has not been downloaded. Run scripts/download_models.py.') from transformers import AutoTokenizer,AutoModelForCausalLM,CLIPProcessor,CLIPModel,BlipProcessor,BlipForConditionalGeneration if name=='language': tok=AutoTokenizer.from_pretrained(path,local_files_only=True) model=AutoModelForCausalLM.from_pretrained(path,local_files_only=True,torch_dtype=torch.float32).to(DEVICE).eval() obj=(tok,model) elif name=='vision': obj=(CLIPProcessor.from_pretrained(path,local_files_only=True),CLIPModel.from_pretrained(path,local_files_only=True).to(DEVICE).eval()) elif name=='caption': obj=(BlipProcessor.from_pretrained(path,local_files_only=True),BlipForConditionalGeneration.from_pretrained(path,local_files_only=True).to(DEVICE).eval()) elif name=='speech': from transformers import WhisperProcessor,WhisperForConditionalGeneration obj=(WhisperProcessor.from_pretrained(path,local_files_only=True),WhisperForConditionalGeneration.from_pretrained(path,local_files_only=True).to(DEVICE).eval()) elif name=='image': from diffusers import AutoPipelineForText2Image # Half-precision VAE decoding overflows on some Apple GPUs. Load fp16 files # into float32 for stable local generation, including the decoder. obj=AutoPipelineForText2Image.from_pretrained(path,local_files_only=True,torch_dtype=torch.float32,variant='fp16',use_safetensors=True).to(DEVICE) obj.enable_attention_slicing() else: raise ValueError('Unknown model') CACHE[name]=obj return obj def language(prompt,max_tokens=160): with LOCK,torch.inference_mode(): tok,model=load('language') text=tok.apply_chat_template([{'role':'system','content':'You are a helpful assistant for a classroom AI brain exhibit. Follow the requested format exactly. Be brief.'},{'role':'user','content':prompt}],tokenize=False,add_generation_prompt=True) inputs=tok(text,return_tensors='pt').to(DEVICE) out=model.generate(**inputs,max_new_tokens=max_tokens,do_sample=False,pad_token_id=tok.eos_token_id) return tok.decode(out[0,inputs.input_ids.shape[1]:],skip_special_tokens=True).strip() def image_features(im): with LOCK,torch.inference_mode(): proc,model=load('vision'); data=proc(images=im,return_tensors='pt').to(DEVICE) feat=model.get_image_features(**data); feat=feat/feat.norm(dim=-1,keepdim=True) return feat[0].cpu().numpy() def text_features(texts): with LOCK,torch.inference_mode(): proc,model=load('vision'); data=proc(text=texts,return_tensors='pt',padding=True,truncation=True).to(DEVICE) feat=model.get_text_features(**data); feat=feat/feat.norm(dim=-1,keepdim=True) return feat.cpu().numpy() def classify_image(im,labels): feat=image_features(im); refs=text_features(['a photo of '+label for label in labels]) scores=torch.tensor(refs@feat*30).softmax(0).numpy() return sorted([{'label':l,'probability':float(p)} for l,p in zip(labels,scores)],key=lambda a:-a['probability']),feat def caption(im): with LOCK,torch.inference_mode(): proc,model=load('caption'); data=proc(images=im,return_tensors='pt').to(DEVICE) out=model.generate(**data,max_new_tokens=45) return proc.decode(out[0],skip_special_tokens=True) def generate_image(prompt,seed=21): with LOCK,torch.inference_mode(): pipe=load('image'); gen=torch.Generator(device='cpu').manual_seed(seed) im=pipe(prompt=prompt,num_inference_steps=2,guidance_scale=0.,height=512,width=512,generator=gen).images[0] return encode_image(im) def sketch_input(im): gray=255-np.asarray(im.convert('L')) ys,xs=np.where(gray>30) if len(xs)<8: return None bbox=(int(xs.min()),int(ys.min()),int(xs.max())+1,int(ys.max())+1) crop=Image.fromarray(gray).crop(bbox) crop.thumbnail((25,25),Image.Resampling.LANCZOS) out=Image.new('L',(28,28)); out.paste(crop,((28-crop.width)//2,(28-crop.height)//2)) return torch.tensor(np.asarray(out,dtype=np.float32)[None,None]/255) def sketch(im,threshold=1.): x=sketch_input(im) if x is None: return {'candidates':[],'spikes':[],'membrane':[],'message':'Add a few strokes to begin.'} with LOCK,torch.inference_mode(): if 'sketch' not in CACHE: ck=torch.load(ROOT/'models'/'sketch.pt',map_location='cpu',weights_only=True) enc=SketchEncoder(); enc.load_state_dict(ck['encoder']); enc.eval() net=SpikingReadout(); net.load_state_dict(ck['snn']); net.eval() CACHE['sketch']=(enc,net,ck['mean'],ck['std']) enc,net,mean,std=CACHE['sketch'] features=(enc.features(x)-mean)/std logits,spikes,mem=net(features,threshold,True) scores=logits[0].softmax(0).numpy() ids=np.argsort(-scores)[:5] return {'candidates':[{'label':CATEGORIES[i],'probability':float(scores[i])} for i in ids], 'spikes':spikes[:,0,:32].int().tolist(),'membrane':mem[:,0,0].tolist(), 'spike_count':int(spikes.sum()),'threshold':threshold,'model':'CNN encoder → trained LIF layer → linear class readout'} def pong_config(): receipt=json.loads((ROOT/'models'/'pong_v2_receipt.json').read_text()) return {'model':'Target-position LIF SNN','optimal_threshold':receipt['optimal_threshold'], 'steps':10,'neurons':64,'validation_target_mae':receipt['threshold_validation_mae'][str(receipt['optimal_threshold'])], 'imitation_accuracy':receipt['imitation_accuracy'],'test_states':receipt['test_states'], 'benchmark_scope':receipt['benchmark_scope']} def pong(state,threshold): if len(state)!=5 or not np.isfinite(state).all():raise ValueError('Use five finite game observations.') with LOCK,torch.inference_mode(): if 'pong' not in CACHE: net=PongSNN();net.load_state_dict(torch.load(ROOT/'models'/'pong_v2.pt',weights_only=True));net.eval();CACHE['pong']=net goal,spikes,mem=CACHE['pong'](torch.tensor([state],dtype=torch.float32),threshold,True) active=bool(spikes.sum()>0) target=float(goal[0].clamp(.095,.905)) if active else float(state[4]) delta=target-state[4] action=-1 if delta<-.025 else 1 if delta>.025 else 0 return {'action':action,'target_y':target,'decision_active':active,'model':'Target-position LIF SNN', 'spikes':spikes[:,0,:32].int().tolist(),'membrane':mem[:,0,0].tolist(), 'spike_count':int(spikes.sum()),'threshold':threshold,'neurons':64,'steps':10} def navigate(dx,dy,blocked): with LOCK,torch.inference_mode(): if 'navigation' not in CACHE: net=NavigationPolicy(); net.load_state_dict(torch.load(ROOT/'models'/'navigation.pt',weights_only=True)); net.eval(); CACHE['navigation']=net logits=CACHE['navigation'](torch.tensor([[dx,dy,*blocked]],dtype=torch.float32))[0] # Physical collision gate rejects impossible movement, independently of policy. for i,b in enumerate(blocked): if b: logits[i]=-1e6 return int(logits.argmax()),logits.softmax(0).tolist() def transcribe(path): import subprocess,tempfile,soundfile as sf with tempfile.NamedTemporaryFile(suffix='.wav') as out: subprocess.run(['ffmpeg','-v','error','-y','-i',str(path),'-ar','16000','-ac','1',out.name],check=True,capture_output=True) audio,sr=sf.read(out.name) if len(audio)>16000*30: raise ValueError('Please record 30 seconds or less.') with LOCK,torch.inference_mode(): proc,model=load('speech'); inp=proc(audio,sampling_rate=16000,return_tensors='pt').input_features.to(DEVICE) out=model.generate(inp,max_new_tokens=128) return proc.batch_decode(out,skip_special_tokens=True)[0].strip()