ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Parent Page (http://localhost:8090) β β
β β File: web/app.js β β
β β β β
β β 1. Every 1 second: β β
β β requestScreenshot() β β
β β β β β
β β βββΊ postMessage βββββββββββββββββββββββ β β
β β β {type: 'captureScreenshot'} β β β
β β β β β β
β β β ββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β β Neuroglancer Iframe β β β
β β β β (Cross-origin isolated) β β β
β β β β File: web/ng-screenshot-handler.js β β β
β β β β β β β
β β β β 2. On message: β β β
β β β β Find canvas element β β β
β β β β const canvas = document.querySelector( β β β
β β β β 'canvas.neuroglancer-gl-canvas' β β β
β β β β ); β β β
β β β β β β β
β β β β 3. Capture: β β β
β β β β canvas.toDataURL('image/jpeg', 0.8) β β β
β β β β β β β
β β βββββ€ 4. Send back: β β β
β β β β postMessage ββββββββββββββββββββββββββββ€ β β
β β β β {type: 'screenshot', β β β
β β β β jpeg_b64: '...'} β β β
β β β ββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β β β
β β βββΊ 5. handleScreenshotFromIframe(jpeg_b64) β β
β β β - Display in <img> tag β β
β β β - Update frame count β β
β β β - Calculate FPS β β
β β β β β
β β βββΊ 6. sendScreenshotToServer(jpeg_b64) β β
β β POST /api/screenshot β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
β
β HTTP POST
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Server (FastAPI) β
β File: server/stream.py β
β β
β @app.post("/api/screenshot") β
β async def receive_screenshot(request): β
β 7. Decode base64 β JPEG bytes β
β 8. Store in ng_tracker.latest_frame β
β 9. Broadcast via WebSocket to all clients β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β State Tracker β β
β β File: server/ng.py β β
β β β β
β β 10. latest_frame = { β β
β β 'jpeg_bytes': ..., β β
β β 'jpeg_b64': ..., β β
β β 'state': current_state_summary, β β
β β 'timestamp': ... β β
β β } β β
β β β β
β β 11. AI Narrator can access: β β
β β - Screenshot (visual) β β
β β - State (position, layers, etc.) β β
β β - Generate narration combining both β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Purpose: Runs inside Neuroglancer iframe to capture screenshots
Key Functions:
// Wait for Neuroglancer to load
waitForNeuroglancer(callback)
// Capture screenshot from canvas
captureScreenshot(width, height)
// Listen for requests from parent
window.addEventListener('message', ...)
// Notify parent when ready
window.parent.postMessage({type: 'ready'}, '*')Security: Can access iframe's own canvas (no cross-origin restrictions)
Purpose: Request screenshots and manage display
Key Methods:
// Setup message listener
setupMessageListener()
// Request screenshot from iframe
requestScreenshot()
// Handle received screenshot
handleScreenshotFromIframe(jpeg_b64)
// Send to server
sendScreenshotToServer(jpeg_b64)Timing: Requests screenshots at configurable FPS (default 1 fps)
Purpose: Inject screenshot handler into Neuroglancer HTML
Key Functions:
# Proxy Neuroglancer requests
@app.api_route("/ng-proxy/{path:path}", ...)
async def neuroglancer_proxy(path, request):
# Get response from Neuroglancer
response = await client.request(...)
# Inject script if HTML
if 'text/html' in content_type:
html = html.replace('</head>',
'<script src="/static/ng-screenshot-handler.js"></script></head>')
return Response(content=html, ...)Critical: Updates Content-Length header after injection
Purpose: Receive and store screenshots from browser
@app.post("/api/screenshot")
async def receive_screenshot(request):
data = await request.json()
jpeg_b64 = data.get('jpeg_b64')
# Decode and store
jpeg_bytes = base64.b64decode(jpeg_b64)
ng_tracker.latest_frame = {
'jpeg_bytes': jpeg_bytes,
'jpeg_b64': jpeg_b64,
'timestamp': timestamp,
'state': ng_tracker.current_state_summary
}Neuroglancer Canvas (WebGL)
β toDataURL()
Base64 JPEG String (~30-50 KB)
β postMessage
Parent Page JavaScript
β Display in <img>
β HTTP POST
FastAPI Server
β Base64 decode
JPEG Bytes
β Store in memory
AI Narrator (future)
Neuroglancer State Change
β State callback
State Tracker (ng.py)
β Summarize
State Summary {position, scale, layers, ...}
β Attach to screenshot
Combined Frame Data
β WebSocket
Browser Display
{
type: 'captureScreenshot',
width: 800, // Optional
height: 600 // Optional
}// Ready notification
{
type: 'ready'
}
// Screenshot response
{
type: 'screenshot',
jpeg_b64: 'base64-encoded-jpeg-data',
timestamp: 1234567890
}{
"jpeg_b64": "base64-encoded-jpeg-data",
"timestamp": 1234567890.123
}Current: Uses '*' for origin (any origin accepted)
Production: Should validate origin:
if (event.origin !== 'https://expected-domain.com') return;Why it works: Script runs in same context as canvas Blocked: External scripts trying to access cross-origin canvas
May need to allow:
script-src 'self'- Load our injected scriptconnect-src 'self'- WebSocket connectionsimg-src data:- Display base64 images
- Default: 1 fps
- Adjustable: Change
screenshotFpsin app.js - Recommendation: 1-2 fps for live monitoring
- Typical: 20-50 KB per frame (JPEG, quality 0.8)
- At 1 fps: ~3 MB/minute bandwidth
- At 2 fps: ~6 MB/minute bandwidth
- Lower quality:
toDataURL('image/jpeg', 0.6) - Smaller resolution: Resize canvas before capture
- Skip unchanged frames: Compare to previous screenshot
- Delta encoding: Only send changed regions
# In server/narrator.py
def generate_narration(self, summary, screenshot_b64):
# Send to Claude, GPT-4V, etc.
response = anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": screenshot_b64
}
},
{
"type": "text",
"text": f"What's in this brain region? Position: {summary['position']}"
}
]
}]
)// In app.js
this.screenshotHistory = [];
handleScreenshotFromIframe(jpeg_b64) {
this.screenshotHistory.push({
jpeg_b64: jpeg_b64,
timestamp: Date.now(),
state: this.currentState
});
// Keep last 10
if (this.screenshotHistory.length > 10) {
this.screenshotHistory.shift();
}
}// Record screenshots to video
const mediaRecorder = new MediaRecorder(canvas.captureStream(30));
mediaRecorder.start();- Check browser console: Should see
[MESSAGE] Neuroglancer iframe is ready - Check server logs: Should see
[PROXY] Injected screenshot handler - Verify canvas exists: In iframe console,
document.querySelector('canvas')
- Increase
screenshotFpsin app.js - Check network latency (screenshot upload may be slow)
- Reduce screenshot quality/size
- Decrease screenshot rate
- Add frame skip logic
- Only capture on state changes