Endpoint ricetta VisionSuite v2, execute con overlay e immagine salvata (#46)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,19 +5,25 @@ the numbers: that is what lets a station's configuration change without touching
|
||||
the frontend or the recipes.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
|
||||
from PIL import Image
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.backend.api.middleware.api_key import require_maker, require_measurement_tec
|
||||
from src.backend.api.routers.files import ALLOWED_IMAGE_TYPES, validate_file_size
|
||||
from src.backend.api.routers.files import (
|
||||
ALLOWED_IMAGE_TYPES, resolve_upload_path, validate_file_size,
|
||||
)
|
||||
from src.backend.config import settings
|
||||
from src.backend.database import get_db
|
||||
from src.backend.models.api.vision import (
|
||||
VisionExecuteResponse, VisionMeasurementResult, VisionReferenceImageResponse,
|
||||
VisionExecuteResponse, VisionMeasurementResult, VisionRecipeResponse,
|
||||
VisionRecipeUploadResponse, VisionReferenceImageResponse,
|
||||
)
|
||||
from src.backend.models.orm.vision import VisionResult
|
||||
from src.backend.models.orm.task import RecipeTask
|
||||
from src.backend.models.orm.user import User
|
||||
from src.backend.services import vision_service
|
||||
@@ -126,7 +132,7 @@ async def execute(
|
||||
) -> VisionExecuteResponse:
|
||||
task = await _task_or_404(db, task_id)
|
||||
|
||||
result, saved = await vision_service.execute_task(
|
||||
result, saved, report = await vision_service.execute_task(
|
||||
db,
|
||||
task,
|
||||
await _read_image(image),
|
||||
@@ -135,6 +141,7 @@ async def execute(
|
||||
lot_number=lot_number,
|
||||
serial_number=serial_number,
|
||||
production_run_id=production_run_id,
|
||||
image_ext=_IMAGE_EXTENSIONS.get(image.content_type, "png"),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@@ -147,6 +154,111 @@ async def execute(
|
||||
)
|
||||
for m in saved
|
||||
],
|
||||
outcome=report.get("outcome"),
|
||||
overlay=report.get("overlay") or [],
|
||||
image_url=f"/api/vision/results/{result.id}/image",
|
||||
failures=report.get("failures") or [],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/results/{result_id}/image")
|
||||
async def get_result_image(
|
||||
result_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_measurement_tec),
|
||||
) -> FileResponse:
|
||||
"""The image a vision run measured, as saved by `execute` (#46)."""
|
||||
result = (await db.execute(
|
||||
select(VisionResult).where(VisionResult.id == result_id)
|
||||
)).scalar_one_or_none()
|
||||
if result is None or not result.image_path:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Result image not found",
|
||||
)
|
||||
return FileResponse(resolve_upload_path(result.image_path))
|
||||
|
||||
|
||||
def _422(errors: list[str]) -> HTTPException:
|
||||
# detail e' una lista di stringhe: il frontend legge i 422 solo da `detail`.
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=errors,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/recipe", response_model=VisionRecipeUploadResponse)
|
||||
async def upload_recipe(
|
||||
task_id: int,
|
||||
recipe: UploadFile = File(...),
|
||||
files: list[UploadFile] = File(default=[]),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_maker),
|
||||
) -> VisionRecipeUploadResponse:
|
||||
"""Load a VisionSuite v2 recipe plus its meta files onto a task (#46)."""
|
||||
task = await _task_or_404(db, task_id)
|
||||
if task.task_type not in vision_service.VISION_TASK_TYPES:
|
||||
raise _422([f"task type {task.task_type!r} does not run a vision recipe"])
|
||||
|
||||
raw = await recipe.read()
|
||||
if not validate_file_size(len(raw)):
|
||||
raise _422([f"recipe exceeds {settings.max_upload_size_mb}MB"])
|
||||
try:
|
||||
graph = json.loads(raw)
|
||||
except ValueError:
|
||||
raise _422(["the recipe is not valid JSON"])
|
||||
if not isinstance(graph, dict):
|
||||
raise _422(["the recipe must be a JSON object"])
|
||||
|
||||
uploads: list[tuple[str, bytes]] = []
|
||||
for f in files:
|
||||
content = await f.read()
|
||||
if not validate_file_size(len(content)):
|
||||
raise _422([f"file {f.filename!r} exceeds {settings.max_upload_size_mb}MB"])
|
||||
uploads.append((f.filename or "", content))
|
||||
placed, errors = vision_service.match_recipe_files(graph, uploads)
|
||||
if errors:
|
||||
raise _422(errors)
|
||||
|
||||
# The folder is content-addressed and immutable; writing it before the
|
||||
# validation is harmless (an orphan folder is never referenced) and the
|
||||
# worker needs it on disk to validate meta files.
|
||||
try:
|
||||
recipe_dir = vision_service.store_recipe_folder(task.id, graph, placed)
|
||||
except ValueError as exc:
|
||||
raise _422([str(exc)])
|
||||
check = await vision_service.validate_recipe(graph, recipe_dir)
|
||||
if not check["ok"]:
|
||||
raise _422([str(e) for e in check["errors"]] or ["the recipe is not valid"])
|
||||
|
||||
task.vision_json = graph
|
||||
task.vision_recipe_dir = recipe_dir
|
||||
await db.commit()
|
||||
return VisionRecipeUploadResponse(
|
||||
task_id=task.id,
|
||||
recipe_dir=recipe_dir,
|
||||
files=vision_service.list_recipe_files(recipe_dir),
|
||||
outputs=check["outputs"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}/recipe", response_model=VisionRecipeResponse)
|
||||
async def get_recipe(
|
||||
task_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_maker),
|
||||
) -> VisionRecipeResponse:
|
||||
task = await _task_or_404(db, task_id)
|
||||
outputs: list[dict] = []
|
||||
if task.vision_json:
|
||||
check = await vision_service.validate_recipe(
|
||||
task.vision_json, task.vision_recipe_dir,
|
||||
)
|
||||
outputs = check.get("outputs", [])
|
||||
return VisionRecipeResponse(
|
||||
task_id=task.id,
|
||||
recipe_dir=task.vision_recipe_dir,
|
||||
recipe=task.vision_json,
|
||||
files=vision_service.list_recipe_files(task.vision_recipe_dir),
|
||||
outputs=outputs,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user