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
67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
"""HTTP client for the FabledCurator GPU-job API.
|
|
|
|
The agent's ONLY contact with FC — lease/submit/heartbeat/fail + fetch image
|
|
bytes, all over HTTP with the bearer token. No DB/Redis.
|
|
"""
|
|
import requests
|
|
|
|
|
|
class FcClient:
|
|
def __init__(self, base_url: str, token: str, agent_id: str):
|
|
self.base = base_url.rstrip("/")
|
|
self.agent_id = agent_id
|
|
self.s = requests.Session()
|
|
self.s.headers["Authorization"] = f"Bearer {token}"
|
|
|
|
def lease(self, batch_size: int) -> list[dict]:
|
|
r = self.s.post(
|
|
f"{self.base}/api/gpu/jobs/lease",
|
|
json={"agent_id": self.agent_id, "batch_size": batch_size},
|
|
timeout=30,
|
|
)
|
|
r.raise_for_status()
|
|
return r.json().get("jobs", [])
|
|
|
|
def submit(self, job_id: int, regions: list[dict], replace_kinds: list[str]) -> dict:
|
|
r = self.s.post(
|
|
f"{self.base}/api/gpu/jobs/submit",
|
|
json={
|
|
"agent_id": self.agent_id, "job_id": job_id,
|
|
"regions": regions, "replace_kinds": replace_kinds,
|
|
},
|
|
timeout=120,
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def heartbeat(self, job_ids: list[int]) -> None:
|
|
try:
|
|
self.s.post(
|
|
f"{self.base}/api/gpu/jobs/heartbeat",
|
|
json={"agent_id": self.agent_id, "job_ids": job_ids},
|
|
timeout=30,
|
|
)
|
|
except requests.RequestException:
|
|
pass
|
|
|
|
def fail(self, job_id: int, error: str) -> None:
|
|
try:
|
|
self.s.post(
|
|
f"{self.base}/api/gpu/jobs/fail",
|
|
json={"agent_id": self.agent_id, "job_id": job_id, "error": error},
|
|
timeout=30,
|
|
)
|
|
except requests.RequestException:
|
|
pass
|
|
|
|
def fetch_image(self, image_url: str) -> bytes:
|
|
# image_url is a server-relative path ("/images/...").
|
|
r = self.s.get(f"{self.base}{image_url}", timeout=180)
|
|
r.raise_for_status()
|
|
return r.content
|
|
|
|
def queue_status(self) -> dict:
|
|
r = self.s.get(f"{self.base}/api/gpu/status", timeout=15)
|
|
r.raise_for_status()
|
|
return r.json()
|