File size: 6,141 Bytes
4155f6b 663ec84 4155f6b 663ec84 4155f6b 663ec84 4155f6b 663ec84 4155f6b 663ec84 4155f6b 663ec84 4155f6b 663ec84 4155f6b 663ec84 4155f6b 663ec84 | 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 | """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)
|