vision: ricetta DXF riapribile, immagine salvata, rotte source e files, soglia segue la quota (Refs #105)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jbf7qKuxsnAVjxthRy7dnB
This commit is contained in:
@@ -485,6 +485,84 @@ def list_recipe_files(recipe_dir: str | None) -> list[str]:
|
||||
)
|
||||
|
||||
|
||||
# #105: nomi e id della ricetta costruita da «Crea ricetta da DXF» (src/vision/costruttori.py).
|
||||
_WIZARD_DXF_FILE = "disegno.dxf"
|
||||
_WIZARD_CALIBRATION_FILE = "calibrazione.json"
|
||||
_WIZARD_REFERENCE_STEM = "riferimento"
|
||||
|
||||
|
||||
def _is_real_number(value: object) -> bool:
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
|
||||
|
||||
|
||||
def dxf_compare_source(graph: dict | None, recipe_dir: str | None) -> dict | None:
|
||||
"""#105: i parametri con cui il wizard ha costruito la ricetta, per riaprirlo.
|
||||
|
||||
Legge i valori dal grafo (tool `pezzo` LocateBlob, tool `dxf` CompareDxf) e i
|
||||
file dalla cartella ricetta. None se il grafo non e' uno del wizard (il task
|
||||
non ha ricetta, o l'ha caricata a mano): la rotta risponde 404. Pura: niente DB.
|
||||
"""
|
||||
if not isinstance(graph, dict) or not isinstance(graph.get("tools"), list):
|
||||
return None
|
||||
tools = {t.get("id"): t for t in graph["tools"] if isinstance(t, dict)}
|
||||
pezzo, confronto = tools.get("pezzo"), tools.get("dxf")
|
||||
if not pezzo or pezzo.get("kind") != "LocateBlob":
|
||||
return None
|
||||
if not confronto or confronto.get("kind") != "CompareDxf":
|
||||
return None
|
||||
p_values = pezzo.get("values") if isinstance(pezzo.get("values"), dict) else {}
|
||||
c_values = confronto.get("values") if isinstance(confronto.get("values"), dict) else {}
|
||||
band, min_pct = c_values.get("band_plus"), c_values.get("points_in_pct_nominal")
|
||||
if not (_is_real_number(band) and _is_real_number(min_pct)):
|
||||
return None
|
||||
polarity = p_values.get("polarity")
|
||||
if polarity not in ("dark", "light"):
|
||||
polarity = "dark" # default di LocateBlob e della rotta di creazione
|
||||
|
||||
folder = None
|
||||
if recipe_dir:
|
||||
try:
|
||||
folder = _safe_join(settings.upload_path, recipe_dir)
|
||||
except (ValueError, OSError):
|
||||
folder = None
|
||||
if folder is not None and not folder.is_dir():
|
||||
folder = None
|
||||
|
||||
px_per_mm = None
|
||||
dxf_name = None
|
||||
image_name = None
|
||||
if folder is not None:
|
||||
try:
|
||||
cal = json.loads((folder / _WIZARD_CALIBRATION_FILE).read_text(encoding="utf-8"))
|
||||
value = cal.get("px_per_mm") if isinstance(cal, dict) else None
|
||||
if _is_real_number(value) and value > 0:
|
||||
px_per_mm = float(value)
|
||||
except (OSError, ValueError):
|
||||
px_per_mm = None # manca o illeggibile: il wizard chiedera' la scala
|
||||
if (folder / _WIZARD_DXF_FILE).is_file():
|
||||
dxf_name = _WIZARD_DXF_FILE
|
||||
for candidate in sorted(folder.glob(f"{_WIZARD_REFERENCE_STEM}.*")):
|
||||
if candidate.is_file():
|
||||
image_name = candidate.name
|
||||
break
|
||||
return {
|
||||
"band_mm": float(band), "min_points_pct": float(min_pct), "polarity": polarity,
|
||||
"px_per_mm": px_per_mm, "dxf": dxf_name, "image": image_name,
|
||||
}
|
||||
|
||||
|
||||
def recipe_file_path(recipe_dir: str | None, relative: str) -> Path | None:
|
||||
"""#105: percorso di un file della cartella ricetta, o None se non sicuro o assente."""
|
||||
if not recipe_dir:
|
||||
return None
|
||||
try:
|
||||
folder = _safe_join(settings.upload_path, recipe_dir)
|
||||
target = _safe_join(folder, relative)
|
||||
except (ValueError, OSError):
|
||||
return None
|
||||
return target if target.is_file() else None
|
||||
|
||||
|
||||
def save_result_files(
|
||||
task_id: int, image_bytes: bytes, overlay: list, image_ext: str = "png",
|
||||
) -> tuple[str, str]:
|
||||
@@ -880,9 +958,27 @@ async def ensure_points_quote(
|
||||
Una sola volta: se esiste gia' una quota con `vision_output`, non si tocca.
|
||||
Se la versione ha gia' misure non si scrive (il copy-on-write sta nel router
|
||||
dei task, qui non si aggira): ritorna False. Ritorna True se l'ha creata.
|
||||
#105: unica eccezione, la quota nata da qui (`dxf.points_in_pct`, «Punti in
|
||||
tolleranza») senza misure segue la soglia nuova: ltl = min_points_pct, e
|
||||
ritorna True anche in quel caso.
|
||||
"""
|
||||
from src.backend.services import recipe_service # import locale: evita cicli
|
||||
|
||||
# #105: ricetta ripresa e salvata con un'altra soglia -> la quota la segue
|
||||
own = (await db.execute(
|
||||
select(RecipeSubtask)
|
||||
.where(RecipeSubtask.task_id == task.id)
|
||||
.where(RecipeSubtask.vision_output == "dxf.points_in_pct")
|
||||
.where(RecipeSubtask.description == "Punti in tolleranza")
|
||||
.limit(1)
|
||||
)).scalar_one_or_none()
|
||||
if own is not None:
|
||||
if (own.ltl is not None and float(own.ltl) == float(min_points_pct)) or await recipe_service.version_has_measurements(db, task.version_id):
|
||||
return False
|
||||
own.ltl = min_points_pct
|
||||
await db.flush()
|
||||
return True
|
||||
|
||||
existing = (await db.execute(
|
||||
select(RecipeSubtask.id)
|
||||
.where(RecipeSubtask.task_id == task.id)
|
||||
|
||||
Reference in New Issue
Block a user