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:
@@ -6,6 +6,8 @@ to the worker over HTTP and stays ignorant of how a number was produced.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException, status
|
||||
@@ -15,7 +17,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.backend.config import settings
|
||||
from src.backend.models.orm.measurement import Measurement
|
||||
from src.backend.models.orm.task import RecipeSubtask, RecipeTask
|
||||
from src.backend.models.orm.vision import VisionResult
|
||||
from src.backend.models.orm.vision import VisionReferenceImage, VisionResult
|
||||
from src.backend.services import measurement_service
|
||||
|
||||
|
||||
@@ -133,3 +135,45 @@ async def execute_task(
|
||||
))
|
||||
|
||||
return result, saved
|
||||
|
||||
|
||||
async def save_reference_image(
|
||||
db: AsyncSession,
|
||||
task_id: int,
|
||||
image_bytes: bytes,
|
||||
*,
|
||||
note: str | None = None,
|
||||
station_id: int | None = None,
|
||||
device_code: str | None = None,
|
||||
calibration_snapshot: dict | None = None,
|
||||
) -> VisionReferenceImage:
|
||||
"""Keep an image so the graph can be composed and re-tried without a camera."""
|
||||
# `upload_path` is the resolved Path, `upload_dir` the bare setting: the
|
||||
# rest of the codebase uses the former (see production_export_service).
|
||||
folder = settings.upload_path / "vision" / "reference" / str(task_id)
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
name = f"{uuid4().hex}.png"
|
||||
(folder / name).write_bytes(image_bytes)
|
||||
|
||||
reference = VisionReferenceImage(
|
||||
task_id=task_id,
|
||||
path=str(Path("vision") / "reference" / str(task_id) / name),
|
||||
note=note,
|
||||
station_id=station_id,
|
||||
device_code=device_code,
|
||||
calibration_snapshot=calibration_snapshot,
|
||||
)
|
||||
db.add(reference)
|
||||
await db.flush()
|
||||
await db.refresh(reference)
|
||||
return reference
|
||||
|
||||
|
||||
async def preview(db: AsyncSession, task: RecipeTask, image_bytes: bytes) -> dict:
|
||||
"""Run the graph and save nothing. Composing is not measuring."""
|
||||
if not task.vision_json:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail="this task carries no vision graph",
|
||||
)
|
||||
return await _call_worker(image_bytes, task.vision_json)
|
||||
|
||||
Reference in New Issue
Block a user