8419ebd761
The last piece: a Dockerised desktop-GPU worker that talks to FC ONLY over HTTP (lease → fetch pixels → detect figures + CCIP-embed → submit), so Redis/Postgres stay private. New top-level agent/ (outside CI scope — verified by running it): - fc_agent/worker.py: the lease/compute/submit loop, concurrency 1, start/pause/ stop (stop frees the card; unprocessed leases expire + re-queue). - fc_agent/models.py: imgutils wrappers — detect_person (figures) + CCIP embed. The two API seams to verify against the installed dghs-imgutils (flagged). - fc_agent/media.py: stills + video frame sampling (ffmpeg) at FC's cadence → per-frame instances (the bag). - fc_agent/crops.py: vendored crop primitive. client.py: the FC HTTP client. - fc_agent/app.py: FastAPI localhost control UI (start/pause/stop + progress + queue depth). Dockerfile (CUDA + onnxruntime-gpu + ffmpeg) + requirements + README (token → build → run --gpus all → Start; CPU-fallback path). This completes the CCIP pipeline end to end: agent produces region CCIP vectors → RegionService stores → matcher suggests characters → rail. Verified by running on the desktop (not CI). README calls out the imgutils API + model-string checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
"""FastAPI control surface for the agent (served on localhost).
|
|
|
|
Start / pause / resume / stop the worker, set nothing else here (config is env),
|
|
and watch progress + the server-side queue. The container exposes this on a
|
|
localhost port; stopping the worker frees the GPU.
|
|
"""
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
|
|
from .config import Config
|
|
from .worker import Worker
|
|
|
|
cfg = Config.from_env()
|
|
worker = Worker(cfg)
|
|
app = FastAPI(title="FabledCurator GPU agent")
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
def index() -> str:
|
|
return _PAGE
|
|
|
|
|
|
@app.post("/start")
|
|
def start():
|
|
worker.start()
|
|
return JSONResponse(worker.status())
|
|
|
|
|
|
@app.post("/pause")
|
|
def pause():
|
|
worker.pause()
|
|
return JSONResponse(worker.status())
|
|
|
|
|
|
@app.post("/resume")
|
|
def resume():
|
|
worker.resume()
|
|
return JSONResponse(worker.status())
|
|
|
|
|
|
@app.post("/stop")
|
|
def stop():
|
|
worker.stop()
|
|
return JSONResponse(worker.status())
|
|
|
|
|
|
@app.get("/status")
|
|
def status():
|
|
s = worker.status()
|
|
s["fc_url"] = cfg.fc_url
|
|
s["configured"] = bool(cfg.token)
|
|
try:
|
|
s["queue"] = worker.client.queue_status()
|
|
except Exception:
|
|
s["queue"] = None
|
|
return JSONResponse(s)
|
|
|
|
|
|
_PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|
<title>FabledCurator GPU agent</title>
|
|
<style>
|
|
body{font:14px system-ui;margin:2rem;max-width:640px;background:#14171a;color:#e8e8e8}
|
|
h1{font-size:18px} button{font:14px system-ui;padding:.5rem 1rem;border:0;border-radius:6px;
|
|
margin-right:.5rem;cursor:pointer;color:#fff} .start{background:#2e7d32}.pause{background:#b26a00}
|
|
.stop{background:#b3261e} .stat{display:inline-block;margin-right:1.5rem}
|
|
.n{font-size:22px;font-weight:700} code{background:#222;padding:2px 6px;border-radius:4px}
|
|
.q{margin-top:1rem;color:#9aa}
|
|
</style></head><body>
|
|
<h1>FabledCurator GPU agent</h1>
|
|
<p>FC: <code id=fc>—</code> · token <code id=cfg>—</code></p>
|
|
<p>
|
|
<button class=start onclick=act('start')>Start</button>
|
|
<button class=pause onclick=act('pause')>Pause</button>
|
|
<button class=pause onclick=act('resume')>Resume</button>
|
|
<button class=stop onclick=act('stop')>Stop</button>
|
|
</p>
|
|
<p>
|
|
<span class=stat><span class=n id=state>idle</span><br>state</span>
|
|
<span class=stat><span class=n id=done>0</span><br>processed</span>
|
|
<span class=stat><span class=n id=err>0</span><br>errors</span>
|
|
<span class=stat><span class=n id=cur>—</span><br>current image</span>
|
|
</p>
|
|
<div class=q id=queue></div>
|
|
<script>
|
|
async function act(p){await fetch('/'+p,{method:'POST'});refresh()}
|
|
async function refresh(){
|
|
const s=await (await fetch('/status')).json()
|
|
state.textContent=s.state; done.textContent=s.processed; err.textContent=s.errors
|
|
cur.textContent=s.current??'—'; fc.textContent=s.fc_url
|
|
cfg.textContent=s.configured?'set':'MISSING'
|
|
queue.textContent=s.queue?`queue — pending ${s.queue.pending} · in flight ${s.queue.leased} · done ${s.queue.done} · errored ${s.queue.error}`:'queue — unreachable'
|
|
}
|
|
refresh(); setInterval(refresh,3000)
|
|
</script></body></html>"""
|