"""The heavy container: the runner behind an internal API. Separate from the FastAPI server on purpose. The API image stays light, a VisionSuite upgrade does not restart production traffic, and an execution that crashes does not take the other tablets' requests down with it. """ from __future__ import annotations import json import numpy as np from fastapi import FastAPI, File, Form, HTTPException, UploadFile, status from PIL import Image from src.vision.runner import engine_version, run_graph app = FastAPI(title="TieMeasureFlow Vision Worker", version="0.1.0") @app.get("/health") async def health() -> dict: return {"status": "ok", "engine_version": engine_version()} @app.post("/run") async def run( image: UploadFile = File(...), graph: str = Form(...), ) -> dict: try: parsed_graph = json.loads(graph) except json.JSONDecodeError as exc: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=f"graph is not valid JSON: {exc}", ) from exc frame = np.array(Image.open(image.file).convert("L")) try: outcome = run_graph(frame, parsed_graph) except ValueError as exc: # from_dict refuses a schema version it does not handle; that is a bad # request, not a server fault. raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc), ) from exc return { "outputs": outcome.outputs, "failures": [ {"tool_id": f.tool_id, "tool_name": f.tool_name, "error": f.error} for f in outcome.failures ], "engine_version": outcome.engine_version, "duration_ms": outcome.duration_ms, }