brain-lab / space_app.py
Chungulus's picture
Deploy Brain Lab CPU classroom app with verified saved research playback
663ec84 verified
Raw History Blame Contribute Delete
6.14 kB
"""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)