forensic-brain / app.py
joduor's picture
Upload folder using huggingface_hub
5de13b5 verified
Raw
History Blame Contribute Delete
12.2 kB
"""
ForensicAI 2nd Brain β€” Hugging Face Space
Interactive morphic simulation UI + GPT Action API endpoints.
"""
from __future__ import annotations
import base64, hashlib, os
from pathlib import Path
from typing import Optional
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, Response, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
import brain as brain_store
from simulation import generate_forensic_image, generate_case_heatmap, CASE_TYPES
SIMS_DIR = Path("/tmp/forensic_sims")
SIMS_DIR.mkdir(parents=True, exist_ok=True)
app = FastAPI(title="ForensicAI 2nd Brain", version="1.0.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
app.mount("/sims", StaticFiles(directory=str(SIMS_DIR)), name="sims")
# ── Interactive UI ─────────────────────────────────────────────────────────────
HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>ForensicAI Β· 2nd Brain</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#05050f;color:#c0ccdd;font-family:'Courier New',monospace;overflow:hidden;height:100vh;display:flex;flex-direction:column}
#hdr{display:flex;align-items:center;justify-content:space-between;padding:9px 16px;background:rgba(5,5,20,.95);border-bottom:1px solid #0a2a4a}
#title{color:#00b4d8;font-size:13px;letter-spacing:3px;font-weight:bold}
#gen{color:#9040c8;font-size:11px;letter-spacing:2px}
#main{display:flex;flex:1;overflow:hidden}
#cvs-wrap{flex:1;position:relative}
canvas{width:100%;height:100%;display:block;image-rendering:pixelated}
#side{width:255px;background:rgba(5,10,25,.97);border-left:1px solid #0a2a4a;overflow-y:auto;display:flex;flex-direction:column}
.pnl{padding:12px;border-bottom:1px solid #0d1e35}
.ph{color:#1e6fa0;font-size:9px;letter-spacing:2px;margin-bottom:9px;text-transform:uppercase}
.st{display:flex;justify-content:space-between;font-size:10px;margin:5px 0}
.sv{color:#00b4d8;font-weight:bold}
.bw{margin:4px 0}.bl{font-size:9px;color:#6080a0;margin-bottom:2px}
.bt{height:5px;background:#0d1e35;border-radius:2px;overflow:hidden}
.bf{height:100%;border-radius:2px;transition:width .7s}
#ins{flex:1;padding:12px;overflow-y:auto}
.ins{font-size:9px;color:#405060;margin:3px 0;padding:3px 6px;border-left:2px solid #0d1e35}
.ins.n{color:#7090a8;border-left-color:#00b4d8}
#ftr{padding:7px 16px;background:rgba(5,5,20,.95);border-top:1px solid #0a2a4a;font-size:9px;color:#253545;display:flex;justify-content:space-between}
.dot{width:5px;height:5px;border-radius:50%;background:#00b4d8;display:inline-block;animation:p 1.6s infinite}
@keyframes p{0%,100%{opacity:1}50%{opacity:.2}}
</style>
</head>
<body>
<div id="hdr">
<span id="title">⬑ FORENSICAI · 2ND BRAIN</span>
<span id="gen">GENERATION <span id="gn">001</span></span>
</div>
<div id="main">
<div id="cvs-wrap"><canvas id="c"></canvas></div>
<div id="side">
<div class="pnl">
<div class="ph">BRAIN STATE</div>
<div class="st"><span>SESSIONS</span><span class="sv" id="ss">0</span></div>
<div class="st"><span>ANALYSES</span><span class="sv" id="sa">0</span></div>
<div class="st"><span>EVIDENCE ITEMS</span><span class="sv" id="se">0</span></div>
<div class="st"><span>AVG CONFIDENCE</span><span class="sv" id="sc">0.70</span></div>
<div class="st"><span>DOMINANT CASE</span><span class="sv" id="sd">β€”</span></div>
</div>
<div class="pnl">
<div class="ph">CASE ACTIVITY</div>
<div id="cbars"></div>
</div>
<div class="pnl">
<div class="ph">SIM PARAMETERS</div>
<div class="st"><span>FEED RATE</span><span class="sv" id="sf">0.0370</span></div>
<div class="st"><span>KILL RATE</span><span class="sv" id="sk">0.0600</span></div>
<div class="st"><span>GRID</span><span class="sv">120 Γ— 120</span></div>
</div>
<div id="ins">
<div class="ph" style="margin-bottom:7px">CASE INTELLIGENCE LOG</div>
<div id="il"><div class="ins">Awaiting data…</div></div>
</div>
</div>
</div>
<div id="ftr">
<span><span class="dot"></span>&nbsp; MORPHIC ENGINE ACTIVE</span>
<span id="fp">f=0.0370 k=0.0600</span>
</div>
<script>
const W=120,H=120,Du=0.16,Dv=0.08;
let simF=0.037,simK=0.060;
const PALETTES={
homicide: [[210,40,40],[120,40,200],[230,240,255]],
sexual_assault: [[160,60,220],[0,180,220],[230,240,255]],
digital_crime: [[0,180,220],[30,80,200],[230,240,255]],
drug_offense: [[245,160,10],[20,200,100],[230,240,255]],
arson: [[245,100,20],[210,40,40],[245,200,180]],
burglary: [[80,140,255],[245,160,10],[230,240,255]],
fraud: [[20,200,100],[0,180,220],[230,240,255]],
hit_and_run: [[245,130,50],[210,40,40],[0,180,220]],
default: [[0,180,220],[30,80,200],[230,240,255]],
};
let pal=PALETTES.default;
const CASE_COLORS={
homicide:'#d22828',sexual_assault:'#9040c8',digital_crime:'#00b4d8',
drug_offense:'#f5a00a',arson:'#f56414',burglary:'#5090ff',
fraud:'#14c864',hit_and_run:'#f58232'
};
function lerp(a,b,t){return[a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t,a[2]+(b[2]-a[2])*t];}
function mapCol(v){
const blk=[5,5,15],nv=[10,15,40];
const stops=[[0,blk],[0.18,nv],[0.35,lerp(nv,pal[0],0.5)],[0.55,pal[0]],[0.75,pal[1]],[0.9,pal[2]],[1,blk.map(x=>x+225)]];
for(let i=0;i<stops.length-1;i++){
const[t0,c0]=stops[i],[t1,c1]=stops[i+1];
if(v>=t0&&v<=t1){const t=(v-t0)/(t1-t0);return lerp(c0,c1,t);}
}
return[230,240,255];
}
let u=new Float32Array(W*H).fill(1),v=new Float32Array(W*H).fill(0);
let nu=new Float32Array(W*H),nv=new Float32Array(W*H);
function seed(n=3){
for(let i=0;i<n;i++){
const cx=6+Math.floor(Math.random()*(W-12)),cy=6+Math.floor(Math.random()*(H-12)),r=7;
for(let dy=-r;dy<=r;dy++){for(let dx=-r;dx<=r;dx++){
const x=(cx+dx+W)%W,y=(cy+dy+H)%H,idx=y*W+x;
u[idx]=0.5+0.02*(Math.random()-.5);v[idx]=0.25+0.02*(Math.random()-.5);
}}
}
}
seed();
function step(n=6){
for(let s=0;s<n;s++){
for(let y=0;y<H;y++){for(let x=0;x<W;x++){
const i=y*W+x,iu=((y-1+H)%H)*W+x,id=((y+1)%H)*W+x,il=y*W+(x-1+W)%W,ir=y*W+(x+1)%W;
const lu=u[iu]+u[id]+u[il]+u[ir]-4*u[i],lv=v[iu]+v[id]+v[il]+v[ir]-4*v[i];
const uvv=u[i]*v[i]*v[i];
nu[i]=Math.max(0,Math.min(1,u[i]+Du*lu-uvv+simF*(1-u[i])));
nv[i]=Math.max(0,Math.min(1,v[i]+Dv*lv+uvv-(simF+simK)*v[i]));
}}
u.set(nu);v.set(nv);
}
}
const cvs=document.getElementById('c'),ctx=cvs.getContext('2d');
let off=null,offCtx=null;
function initCvs(){cvs.width=W;cvs.height=H;off=document.createElement('canvas');off.width=W;off.height=H;offCtx=off.getContext('2d');}
function draw(){
const img=offCtx.createImageData(W,H);const d=img.data;
let mn=1,mx=0;for(let i=0;i<W*H;i++){if(v[i]<mn)mn=v[i];if(v[i]>mx)mx=v[i];}
const rng=mx-mn+1e-9;
for(let i=0;i<W*H;i++){let t=(v[i]-mn)/rng;t=Math.max(0,Math.min(1,t*1.8-0.08));const[r,g,b]=mapCol(t);d[i*4]=r;d[i*4+1]=g;d[i*4+2]=b;d[i*4+3]=255;}
offCtx.putImageData(img,0,0);
ctx.save();ctx.imageSmoothingEnabled=false;ctx.drawImage(off,0,0,cvs.width,cvs.height);ctx.restore();
}
function applyBrain(b){
document.getElementById('gn').textContent=String(b.pattern_generation).padStart(3,'0');
document.getElementById('ss').textContent=b.session_count;
document.getElementById('sa').textContent=b.total_analyses;
document.getElementById('se').textContent=b.total_evidence_items;
document.getElementById('sc').textContent=b.avg_confidence.toFixed(3);
document.getElementById('sd').textContent=b.dominant_case==='default'?'β€”':b.dominant_case;
document.getElementById('sf').textContent=b.feed_rate.toFixed(5);
document.getElementById('sk').textContent=b.kill_rate.toFixed(5);
document.getElementById('fp').textContent='f='+b.feed_rate.toFixed(5)+' k='+b.kill_rate.toFixed(5);
simF=b.feed_rate;simK=b.kill_rate;
pal=PALETTES[b.dominant_case]||PALETTES.default;
const cf=b.case_frequency,mx=Math.max(...Object.values(cf),1);
document.getElementById('cbars').innerHTML=Object.entries(cf).map(([c,n])=>`
<div class="bw"><div class="bl">${c} <span style="float:right">${n}</span></div>
<div class="bt"><div class="bf" style="width:${Math.round(n/mx*100)}%;background:${CASE_COLORS[c]||'#5090ff'}"></div></div></div>`).join('');
const ins=b.insights.slice(-8).reverse();
if(ins.length)document.getElementById('il').innerHTML=ins.map((x,i)=>`<div class="ins${i===0?' n':''}">&gt; ${x.text}</div>`).join('');
}
async function poll(){try{const r=await fetch('/brain');if(r.ok)applyBrain(await r.json());}catch(_){}}
function resize(){const p=document.getElementById('cvs-wrap');cvs.width=p.clientWidth;cvs.height=p.clientHeight;}
initCvs();resize();window.addEventListener('resize',resize);poll();setInterval(poll,10000);
let last=0;
function loop(ts){if(ts-last>33){last=ts;step(6);draw();}requestAnimationFrame(loop);}
requestAnimationFrame(loop);
</script>
</body>
</html>"""
# ── Models ────────────────────────────────────────────────────────────────────
class SimulateRequest(BaseModel):
dominant_case: str = Field("default")
evidence_confidence: float = Field(0.70, ge=0.0, le=1.0)
case_complexity: float = Field(0.50, ge=0.0, le=1.0)
out_size: int = Field(512, ge=128, le=1024)
class BrainUpdateRequest(BaseModel):
case_type: Optional[str] = None
confidence: Optional[float] = None
complexity: Optional[float] = None
n_evidence: int = Field(0, ge=0)
insight: Optional[str] = None
# ── Endpoints ─────────────────────────────────────────────────────────────────
@app.get("/", response_class=HTMLResponse)
def index():
return HTML
@app.get("/health")
def health():
return {"status": "ok", "service": "forensicai-2nd-brain"}
@app.get("/brain")
def get_brain():
return JSONResponse(brain_store.load())
@app.post("/brain/update")
def update_brain(body: BrainUpdateRequest):
state = brain_store.update(
case_type=body.case_type,
confidence=body.confidence,
complexity=body.complexity,
n_evidence=body.n_evidence,
insight=body.insight,
)
return JSONResponse(state)
@app.post("/simulate")
def simulate(body: SimulateRequest):
state = brain_store.load()
png = generate_forensic_image(
dominant_case=body.dominant_case if body.dominant_case != "default" else state["dominant_case"],
evidence_confidence=body.evidence_confidence,
session_count=state["session_count"],
pattern_generation=state["pattern_generation"],
case_complexity=body.case_complexity,
out_size=body.out_size,
)
return {
"image_b64": base64.b64encode(png).decode(),
"mime": "image/png",
"pattern_generation": state["pattern_generation"],
"feed_rate": state["feed_rate"],
"kill_rate": state["kill_rate"],
"dominant_case": state["dominant_case"],
"display_hint": "Render image_b64 as: ![sim](data:image/png;base64,{image_b64})",
}
@app.get("/brain/visualize/b64")
def brain_visualize_b64():
state = brain_store.load()
png = generate_case_heatmap(
case_history=[],
confidence_history=state["confidence_history"],
case_frequency=state["case_frequency"],
out_size=512,
)
return {
"image_b64": base64.b64encode(png).decode(),
"mime": "image/png",
"display_hint": "Render as: ![brain](data:image/png;base64,{image_b64})",
}
@app.post("/brain/reset")
def reset_brain():
return JSONResponse(brain_store.reset())