Visione: quota non prodotta salvata come NaN (value NULL, fail) invece del 422; migrazione 022

Refs #118

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 21:21:54 +01:00
parent 4485803122
commit 06f308be6f
15 changed files with 317 additions and 71 deletions
+19 -21
View File
@@ -693,23 +693,13 @@ async def execute_task(
)
outputs = report["outputs"]
# Every quote or none. A half-measured piece read as a whole one is worse
# than a piece not measured at all. A quote counts as produced only if its
# output is present *and* a finite, non-bool number: the in-process runner
# already drops NaN/inf itself (see src/vision/runner.py), but the worker
# is reached over HTTP - a different implementation of it, or a malformed
# response, could still hand back None or a non-finite float for a mapped
# key instead of omitting it outright.
missing = [q.vision_output for q in quotes if not _is_measurable(outputs.get(q.vision_output))]
if missing:
failed = ", ".join(f["tool_name"] for f in report["failures"])
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=(
f"the graph did not produce {', '.join(missing)}"
+ (f" (tools that failed: {failed})" if failed else "")
),
)
# #118: una quota che il grafo non produce non blocca piu' l'esecuzione
# (prima 422 "the graph did not produce"): viene salvata con value NULL,
# mostrata NaN, sempre `fail`; i motivi restano nei `failures` del risultato.
# Una quota conta come prodotta solo se l'uscita c'e' *ed* e' un numero
# finito non-bool: il worker risponde via HTTP, un'altra implementazione o
# una risposta malformata potrebbe dare None o un non finito per una chiave
# mappata invece di ometterla.
overlay = report.get("overlay") or []
image_path, overlay_path = save_result_files(task.id, image_bytes, overlay, image_ext)
@@ -737,7 +727,11 @@ async def execute_task(
subtask_id=quote.id,
version_id=version_id,
measured_by=measured_by,
value=float(outputs[quote.vision_output]),
# #118: None = quota non prodotta (NaN), salvata come fail
value=(
float(outputs[quote.vision_output])
if _is_measurable(outputs.get(quote.vision_output)) else None
),
lot_number=lot_number,
serial_number=serial_number,
input_method="camera",
@@ -762,8 +756,9 @@ async def check_task(
VisionResult, niente file, niente misure. L'esito per quota usa
calculate_pass_fail sulle quote con vision_output, nell'ordine di
marker_number come execute. Le quote che il grafo non produce (pezzo non
ancora in posizione) restano fuori da `measurements` e il perche' sta in
`failures`: un'anteprima non deve dare 422 a ogni frame.
ancora in posizione) entrano in `measurements` con `value` None (NaN) e
`pass_fail` "fail" (#118, come execute); il perche' sta in `failures`:
un'anteprima non deve dare 422 a ogni frame.
"""
if task.task_type not in VISION_TASK_TYPES:
raise HTTPException(
@@ -809,6 +804,7 @@ async def check_task(
**worker_kwargs,
)
outputs = report["outputs"]
# #118: una voce per ogni quota, in ordine di marker_number; non prodotta -> NaN/fail
measurements = [
{
"subtask_id": q.id,
@@ -816,8 +812,10 @@ async def check_task(
"pass_fail": measurement_service.calculate_pass_fail(
float(outputs[q.vision_output]), q,
)[0],
} if _is_measurable(outputs.get(q.vision_output)) else {
"subtask_id": q.id, "value": None, "pass_fail": "fail",
}
for q in quotes if _is_measurable(outputs.get(q.vision_output))
for q in quotes
]
return {
"outcome": report.get("outcome"),