Files
FabledCurator/agent/fc_agent/app.py
T
bvandeusen 8419ebd761
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m32s
feat(agent): desktop GPU agent container — CCIP + figure crops over HTTP (#114)
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
2026-06-29 14:03:01 -04:00

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>"""