feat(vision): immagini di riferimento, per comporre senza camera

VisionReferenceImage (migration 013, chained after 012_vision_results):
task_id, path, station_id, device_code, calibration_snapshot,
engine_version, note, expected_json, acquired_at. station_id/device_code/
calibration_snapshot/engine_version/expected_json stay nullable - nessun
endpoint di questo task li popola ancora, arrivano coi Piani 1b/2/4.

save_reference_image salva sotto settings.upload_path (non upload_dir,
come production_export_service). preview esegue il grafo e non salva
niente - comporre non e' misurare, lo asserisce il test.

POST /api/vision/reference-images e /api/vision/preview su require_maker
(non require_measurement_tec di execute): comporre e provare sono lavoro
del Maker, non misure.

Suite intera su Python 3.11.15 locale (uv run pytest -q, SQLite
in-memory per il backend): 1 failed, 373 passed, 4 skipped in 82.35s.
Il fallimento e' quello preesistente e non correlato di
test_no_first_party_script_calls_out (fabric-debug.js locale,
git-ignored) - 371 passed prima di questo task, +2 qui.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
This commit is contained in:
2026-08-16 19:11:47 +02:00
parent 398af016f8
commit 3870cc3b0f
6 changed files with 268 additions and 3 deletions
+33 -1
View File
@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, s
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.backend.api.middleware.api_key import require_measurement_tec
from src.backend.api.middleware.api_key import require_maker, require_measurement_tec
from src.backend.database import get_db
from src.backend.models.api.vision import VisionExecuteResponse, VisionMeasurementResult
from src.backend.models.orm.task import RecipeTask
@@ -64,3 +64,35 @@ async def execute(
for m in saved
],
)
# Composing a recipe and keeping a reference image are Maker operations, not
# measuring: both endpoints below gate on `require_maker`, not the
# `require_measurement_tec` that `execute` above uses.
@router.post("/reference-images")
async def add_reference_image(
task_id: int = Form(...),
image: UploadFile = File(...),
note: str | None = Form(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_maker),
) -> dict:
task = await _task_or_404(db, task_id)
reference = await vision_service.save_reference_image(
db, task.id, await image.read(), note=note,
)
await db.commit()
return {"id": reference.id, "path": reference.path, "note": reference.note}
@router.post("/preview")
async def preview(
task_id: int = Form(...),
image: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_maker),
) -> dict:
task = await _task_or_404(db, task_id)
return await vision_service.preview(db, task, await image.read())