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)