Rc / app.py
SiPongo
push
a2cfb1f
Raw History Blame Contribute Delete
7.99 kB
"""
ONNX Grid Vision API
=====================
FastAPI-based image grid classification API with web automation.
Uses ONNX Runtime for inference, patchright for browser automation.
Deploy to cloud platforms with Docker support.
"""
import asyncio
import logging
import uuid
import time
from contextlib import asynccontextmanager
from typing import Optional
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from browser_engine import VisionEngine
# ── Logging ──────────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("gridvision")
# ── Globals ──────────────────────────────────────────────────────────────────
engine: Optional[VisionEngine] = None
active_sessions: dict = {} # task_id -> status info
# ── Request / Response Models ────────────────────────────────────────────────
class ProcessRequest(BaseModel):
sitekey: str = Field(..., description="Widget site key")
url: str = Field(..., description="Page URL for domain context")
is_enterprise: bool = Field(False, description="Enterprise widget mode")
is_invisible: bool = Field(False, description="Invisible widget mode")
proxy: Optional[str] = Field(None, description="Proxy URL (socks5://user:pass@host:port)")
headless: bool = Field(True, description="Run browser headless")
timeout: int = Field(120, ge=10, le=300, description="Processing timeout in seconds")
class ProcessResponse(BaseModel):
success: bool
token: Optional[str] = None
error: Optional[str] = None
elapsed: Optional[float] = None
class StatusResponse(BaseModel):
task_id: str
status: str # pending, processing, completed, failed
token: Optional[str] = None
error: Optional[str] = None
elapsed: float
# ── Lifespan ─────────────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
global engine
logger.info("Loading ONNX models...")
engine = VisionEngine(models_dir="models")
engine.load_models()
logger.info("ONNX Grid Vision API ready")
yield
logger.info("Shutting down")
# ── FastAPI App ──────────────────────────────────────────────────────────────
app = FastAPI(
title="ONNX Grid Vision API",
description=(
"Image grid classification service using YOLOv5 segmentation "
"and per-class binary classifiers with web automation."
),
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── Background Processor ────────────────────────────────────────────────────
async def _process_background(task_id: str, req: ProcessRequest):
active_sessions[task_id]["status"] = "processing"
start = time.time()
try:
token = await engine.process(
sitekey=req.sitekey,
url=req.url,
proxy=req.proxy,
headless=req.headless,
timeout=req.timeout,
is_enterprise=req.is_enterprise,
is_invisible=req.is_invisible,
)
active_sessions[task_id]["status"] = "completed"
active_sessions[task_id]["token"] = token
active_sessions[task_id]["elapsed"] = round(time.time() - start, 2)
logger.info(f"Task {task_id} completed in {active_sessions[task_id]['elapsed']}s")
except Exception as e:
active_sessions[task_id]["status"] = "failed"
active_sessions[task_id]["error"] = str(e)
active_sessions[task_id]["elapsed"] = round(time.time() - start, 2)
logger.error(f"Task {task_id} failed: {e}")
# ── Endpoints ────────────────────────────────────────────────────────────────
@app.get("/", tags=["Info"])
async def root():
return {
"name": "ONNX Grid Vision API",
"version": "1.0.0",
"endpoints": {
"POST /process": "Process verification synchronously (blocking)",
"POST /process/async": "Process verification asynchronously",
"GET /status/{task_id}": "Check async task status",
"GET /health": "Health check",
},
}
@app.get("/health", tags=["Info"])
async def health():
return {"status": "ok", "models_loaded": engine is not None and engine.loaded}
@app.post("/process", response_model=ProcessResponse, tags=["Processing"])
async def process(req: ProcessRequest):
"""
Process verification synchronously.
Returns the token directly (blocks until done or timeout).
"""
if not engine or not engine.loaded:
raise HTTPException(503, "Engine not ready (models still loading)")
start = time.time()
try:
token = await engine.process(
sitekey=req.sitekey,
url=req.url,
proxy=req.proxy,
headless=req.headless,
timeout=req.timeout,
is_enterprise=req.is_enterprise,
is_invisible=req.is_invisible,
)
elapsed = round(time.time() - start, 2)
if token:
return ProcessResponse(success=True, token=token, elapsed=elapsed)
else:
return ProcessResponse(success=False, error="Timeout: could not complete", elapsed=elapsed)
except Exception as e:
elapsed = round(time.time() - start, 2)
logger.error(f"Sync process failed: {e}")
return ProcessResponse(success=False, error=str(e), elapsed=elapsed)
@app.post("/process/async", tags=["Processing"])
async def process_async(req: ProcessRequest, background_tasks: BackgroundTasks):
"""
Process verification asynchronously.
Returns a task_id immediately. Poll /status/{task_id} for results.
"""
if not engine or not engine.loaded:
raise HTTPException(503, "Engine not ready (models still loading)")
task_id = str(uuid.uuid4())
active_sessions[task_id] = {
"status": "pending",
"token": None,
"error": None,
"elapsed": 0.0,
}
background_tasks.add_task(_process_background, task_id, req)
return {"task_id": task_id, "status": "pending"}
@app.get("/status/{task_id}", response_model=StatusResponse, tags=["Processing"])
async def status(task_id: str):
"""Check the status of an async processing task."""
if task_id not in active_sessions:
raise HTTPException(404, "Task not found")
session = active_sessions[task_id]
return StatusResponse(
task_id=task_id,
status=session["status"],
token=session.get("token"),
error=session.get("error"),
elapsed=session["elapsed"],
)
# ── Entry Point ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)