feat(vision): il runner, unica implementazione dell'esecuzione

Aggiunge il sottomodulo vendor/visionsuite (pin f095fcc), src/vision/runner.py
(run_graph/engine_version, pura libreria: niente HTTP, niente DB) e i test.

Deviazione dal brief: vs-task/vs-measure/vs-pm2d dichiarano
requires-python>=3.13 a monte, mentre il monorepo resta a 3.11. L'extra
`vision` porta marker `python_version>='3.13'` cosi' la sync di base
(server+client+dev, 3.11) resta intatta; per lavorare sulla vision serve
`uv sync --extra vision --extra dev --python 3.13`. Ricade sul Task 3: il
worker (Dockerfile.vision) non puo' partire da python:3.11-slim come previsto
dal suo brief, deve usare 3.13.
This commit is contained in:
2026-08-16 17:28:09 +02:00
parent 9546ded1e8
commit c5f3366dd1
8 changed files with 474 additions and 1 deletions
+60
View File
@@ -0,0 +1,60 @@
"""Il runner: grafo più immagine, numeri fuori. Niente HTTP, niente database."""
import numpy as np
import pytest
from src.vision.runner import RunOutcome, engine_version, run_graph
def a_disc(radius: int = 60, size: int = 400) -> np.ndarray:
"""Un disco chiaro su fondo scuro: un pezzo finto ma misurabile."""
image = np.zeros((size, size), dtype=np.uint8)
yy, xx = np.ogrid[:size, :size]
centre = size // 2
image[(yy - centre) ** 2 + (xx - centre) ** 2 <= radius ** 2] = 255
return image
def a_circle_graph() -> dict:
"""Un grafo di due strumenti, costruito con l'API di vs-task."""
from visionsuite.task import Task
from visionsuite.task.tools import DetectCircleTool, DiameterTool
task = Task(name="prova")
found = task.add(DetectCircleTool(name="disco"))
found.center_row, found.center_col, found.radius = 200.0, 200.0, 60.0
found.scan_size = 30.0
quota = task.add(DiameterTool(name="diametro"))
quota.link("circle", found, "circle")
return task.to_dict()
def test_the_graph_produces_named_outputs():
outcome = run_graph(a_disc(), a_circle_graph())
assert isinstance(outcome, RunOutcome)
assert outcome.failures == []
# Le chiavi sono «id dello strumento punto nome dell'uscita»: è la stessa
# forma che la subtask salva in vision_output.
assert any(key.endswith(".diameter") for key in outcome.outputs)
diameter = next(v for k, v in outcome.outputs.items() if k.endswith(".diameter"))
assert diameter == pytest.approx(120.0, abs=4.0)
def test_a_broken_tool_does_not_sink_the_others():
"""vs-task non ferma l'esecuzione su un errore: il runner lo riporta."""
graph = a_circle_graph()
graph["tools"].append(
{"id": "ghost", "kind": "DetectCircle", "name": "assente",
"properties": {"center_row": 5.0, "center_col": 5.0, "radius": 900.0}}
)
outcome = run_graph(a_disc(), graph)
assert [f.tool_id for f in outcome.failures] == ["ghost"]
assert any(key.endswith(".diameter") for key in outcome.outputs)
def test_the_engine_says_which_version_it_is():
version = engine_version()
assert len(version) == 40 # un SHA di git, per intero