"""CPU Space adapter for the existing classroom app and saved research outputs.""" import json import os from pathlib import Path import shutil import sys import time from shared_usage import SharedUsage,SharedLimit ROOT=Path(__file__).resolve().parent/'brain_lab' sys.path.insert(0,str(ROOT)) import server as lab from fastapi import Request,HTTPException from fastapi.responses import FileResponse,JSONResponse from backend import credentials,mario,ml,mindeye_live app=lab.app ml.torch.set_num_threads(int(os.environ.get('BRAIN_LAB_THREADS','2'))) ml.DEVICE='cpu' if sys.platform!='darwin' or not mario.JAVA.is_file(): mario.JAVA=Path(shutil.which('java') or '/unavailable-java') mario.JAVAC=Path(shutil.which('javac') or '/unavailable-javac') # Owner-authorized shared keys are provided only through encrypted Space secrets. # Standard workstation environment variables and Keychain are never consulted. def space_key(provider): name={'openrouter':'BRAIN_LAB_OPENROUTER_API_KEY','perplexity':'BRAIN_LAB_PERPLEXITY_API_KEY'}.get(provider) return os.environ.get(name,'').strip() if name else '' credentials.get_key=space_key lab.sketch_provider.get_key=credentials.get_key shared_usage=SharedUsage(ROOT/'data/shared_recognition_usage.sqlite3', daily=max(1,min(2000,int(os.environ.get('BRAIN_LAB_SHARED_DAILY','200')))), per_minute=max(1,min(60,int(os.environ.get('BRAIN_LAB_SHARED_PER_MINUTE','20'))))) _seen={} def owner_key_needed(config): keys=config.get('api_keys',{});provider=config.get('provider','openrouter') return bool((not keys.get(provider) and space_key(provider)) or (not keys.get('openrouter') and space_key('openrouter'))) @app.middleware('http') async def cpu_space_policy(request,call_next): path=request.url.path if request.method=='POST' and (path=='/api/tribe/jobs' or path=='/api/tribe/demo' or path.startswith('/api/mindeye/live/') or path=='/api/cinema'): return JSONResponse({'detail':'New large-model predictions are not enabled on this CPU Space. The saved Cinema clips and published MindEye2 replay are ready to use.'},status_code=503) # Expire unused game sessions and their Java processes; live requests keep # the visitor's session. No private histories are copied into the Space. if path.startswith('/api/'): sid=request.headers.get('X-Lab-Session','classroom');now=time.monotonic() with lab.STATE_LOCK: for old,stamp in list(_seen.items()): if old!=sid and now-stamp>1800: state=lab.STATES.pop(old,None);_seen.pop(old,None) if state and state.get('mario'): try:state['mario'].close() except OSError:pass _seen[sid]=now response=await call_next(request) response.headers['X-Brain-Lab-Edition']='spaces-cpu' return response @app.get('/healthz') def health(): return {'app':'Brain Lab','edition':'spaces-cpu','saved_clips':len(lab.clip_library._clips()), 'large_model_jobs_enabled':False, 'api_keys':'owner-space-secrets' if space_key('openrouter') else 'visitor-session-only'} # Serve already-computed model outputs as immutable compressed JSON, without # repeatedly converting the large prediction arrays on a shared CPU. app.router.routes=[r for r in app.router.routes if getattr(r,'path','') not in ['/api/tribe/results/{ident}','/api/mindeye/live/status','/api/sketch/config','/api/sketch']] @app.get('/api/tribe/results/{ident}') def cached_response(ident:str): if not ident.isalnum() or len(ident)!=32: return JSONResponse({'detail':'Saved prediction not found.'},status_code=404) path=ROOT/'data/space_results'/(ident+'.json.gz') if not path.is_file():return JSONResponse({'detail':'Saved prediction not found.'},status_code=404) return FileResponse(path,media_type='application/json',headers={'Content-Encoding':'gzip'}) @app.get('/api/mindeye/live/status') def live_status(): result=mindeye_live.status() result.update(hosted_cpu=True,installed=False,recorded_scan_tested=False,recorded_examples=[], note='Fresh fMRI decoding is not configured on this CPU Space. Published research comparisons remain available.') return result @app.get('/api/sketch/config') def sketch_config(request:Request): config=lab.session(request)[1]['sketch_config'] result=lab.sketch_provider.config_status(config) shared=bool(result['key_configured'] and owner_key_needed(config)) result.update(hosted=True,global_storage_available=False,shared_key_enabled=shared, key_source='classroom' if shared else 'visitor' if result['key_configured'] else 'missing', shared_allowance=shared_usage.summary() if shared else None) return result @app.post('/api/sketch/config') def set_sketch_config(body:lab.SketchConfig,request:Request): if body.save_globally: return JSONResponse({'detail':'Keys can be used only for this visitor session; global key storage is disabled on the public site.'},status_code=400) _,state=lab.session(request);state['sketch_config']['provider']=body.provider if body.api_key:state['sketch_config'].setdefault('api_keys',{})[body.provider]=body.api_key return sketch_config(request) @app.post('/api/sketch') def recognize(body:lab.SketchRequest,request:Request): _,state=lab.session(request);lab.require(state,'occipital','thalamus') if body.hint:lab.require(state,'wernicke','association','frontal') image=ml.decode_image(body.image);config=state['sketch_config'] # Blank previews never make a provider request or consume the shared quota. blank=lab.np.count_nonzero(lab.np.asarray(image.convert('L'))<220)<8 if owner_key_needed(config) and not blank: try: with shared_usage.lease():return lab.sketch_provider.recognize(image,config,body.hint) except SharedLimit as error:raise HTTPException(429,str(error)) from None return lab.sketch_provider.recognize(image,config,body.hint)