Files
TieMeasureFlow/docs/superpowers/specs/2026-08-16-visionsuite-integration-design.md
T
Adriano 245975791f docs(spec): design dell'integrazione VisionSuite
Otto decisioni dal brainstorming, con il motivo di ognuna: motore su
entrambi i lati con versione in lockstep, agente sottile, server che non
importa mai VisionSuite, device a livello PC, esecuzione decisa dalla
stazione, verdetto tenuto fuori dal grafo, sorgente immagine astratta.

Modello dati, API, errori e le quattro domande che restano aperte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 16:29:05 +02:00

400 lines
19 KiB
Markdown

# Integrazione di VisionSuite in TieMeasureFlow — design
> Progetto: TieMeasureFlow V3.0.0 → V3.1.0
> Data: 2026-08-16
> Stato: design approvato, piano di implementazione da scrivere
## Perché
TieMeasureFlow oggi registra misure prese a mano, con il calibro USB o con il
tastierino. Il passo successivo che il cliente chiede è misurare con una camera:
acquisire l'immagine di un pezzo e ricavarne le quote, oppure confrontarne il
profilo con il disegno DXF di riferimento.
La visione artificiale non va scritta qui. Tielogic ha già
[VisionSuite](ssh://git@git.tielogic.xyz:222/Adriano/visionsuite.git), un
monorepo di moduli riusabili il cui README indica esplicitamente TieMeasureFlow
fra i consumatori previsti. Questo documento descrive come agganciarlo.
Il terreno è in parte già preparato. La migrazione `007_task_type` ha introdotto
il tipo di task dichiarato e l'enum contiene fin da allora due valori che nessuno
produce né consuma ancora:
```python
TASK_TYPES = ("note", "measure", "drawing", "xf_compare", "camera_measure")
MEASURING_TASK_TYPES = ("measure", "camera_measure")
```
Questo lavoro li riempie.
## Che cosa entra e che cosa resta fuori
**Entra:** l'acquisizione da camera, l'esecuzione di un programma di visione su
quell'immagine — pattern matching più misure, oppure pattern matching più
confronto DXF — e l'ingresso dei risultati nel modello di misura che già esiste.
**Resta fuori, per ora:** i semafori USB. Sono previsti dall'architettura — è
l'agente di stazione a possederli, insieme alle camere — ma non fanno parte di
questa consegna. Il confine è disegnato perché ci entrino senza ristrutturazioni.
## Le otto decisioni
Sono le scelte prese in fase di brainstorming, con il motivo per cui sono state
prese. Chi legge fra sei mesi ha bisogno del motivo più della scelta.
### 1. La visione gira sia sul server sia sulla stazione
Il motore è un componente unico distribuito in due posti: accanto al server e
dentro l'agente di stazione. Questo dà la resilienza — se cade il collegamento,
la stazione continua a misurare — e permette al server di elaborare in fase di
produzione, di comporre ricette per conto del Maker e di rifare analisi su
immagini archiviate.
È coerente con la decisione **D-3**, chiusa il 28/07: dati sul server, app di
stazione su ogni PC.
### 2. Server e stazione montano la stessa identica versione di VisionSuite
Non è una raccomandazione di build: è un vincolo metrologico. Se il server monta
un commit e una stazione ne monta un altro, lo stesso pezzo con lo stesso grafo
può dare due numeri diversi, e nessuno se ne accorge finché non arriva una
contestazione.
Da qui tre obblighi che il design deve garantire:
- VisionSuite entra come **sottomodulo git su un commit preciso**, lo stesso per
entrambi — la forma che il README di VisionSuite prescrive
- ogni misura porta scritto **con quale versione del motore** è stata prodotta
- una stazione che monta una versione diversa da quella del server **si rifiuta
di misurare** e lo dichiara, invece di produrre numeri plausibili e sbagliati
### 3. L'agente di stazione è sottile
L'agente parla ai device, consegna immagini e — più avanti — piloterà i semafori.
Non contiene decisioni di visione.
La conseguenza pratica è l'impacchettamento: un agente che non contiene
VisionSuite non contiene nemmeno PyTorch. Distribuirlo come eseguibile su Windows
o come container su Linux diventa realistico, e la scelta su driver e formato può
restare tardiva — che è bene, perché dipende dalla **D-4**, ancora aperta.
L'agente incorpora il motore solo quando la stazione è configurata per elaborare
in locale, e in quel caso resta comunque un guscio: riceve un grafo, lo esegue,
restituisce le uscite.
### 4. Il server non importa mai VisionSuite
La visione sul lato server vive in un **servizio separato** nello stesso compose,
il *vision worker*. Il server FastAPI continua a fare quello che fa —
autenticazione, ricette, misure, statistica — e resta leggero.
Quattro ragioni concrete: l'immagine dell'API non passa da poche centinaia di
megabyte a cinque gigabyte; un aggiornamento di VisionSuite non richiede di
riavviare l'API in produzione; un'elaborazione che va in crash non porta giù le
richieste degli altri tablet; il giorno in cui la visione chiede una GPU o una
macchina propria si sposta senza toccare il resto.
Il confine va difeso: se un domani il server importasse VisionSuite per una
scorciatoia, il vantaggio si perde in silenzio.
### 5. La configurazione dei device sta sul PC
Le camere — e domani i semafori — appartengono alla macchina, non alla ricetta.
Il server ne tiene uno **specchio** in sola lettura, popolato dall'agente quando
si connette, perché il Maker deve poter scegliere un device da un elenco stando
alla scrivania.
Un device che l'agente non dichiara più è **offline**, non cancellato: una
ricetta che lo referenzia deve continuare a esistere e a spiegare perché non può
girare.
### 6. Dove si esegue lo decide la configurazione della stazione
Ogni PC dichiara se elabora in locale o delega al server, nella stessa
configurazione dove già stanno i device. Il default è il server.
Le macchine capaci si prendono il carico e non mandano immagini in rete; quelle
deboli delegano. Se il server non risponde e la stazione ha il motore, la
stazione ripiega in locale e lo segna nel risultato. Un impianto misto si governa
senza toccare le ricette.
La ricetta non dice mai dove gira: legare una scelta di impianto a un documento
di produzione significa dover rimettere mano alle ricette per spostare il carico.
### 7. Il verdetto lo dà TieMeasureFlow, non il grafo
Gli strumenti di quota di `vs-task` hanno anche loro `nominal`, `plus`, `minus` e
un `outcome`. Quell'esito viene ignorato e quei limiti restano vuoti: il grafo
produce numeri, `measurement_service` decide se sono buoni.
Due giudici sullo stesso valore è il modo più rapido per ottenere un CSV che si
contraddice e una contestazione che non si sa come chiudere.
La conseguenza è che tutto il resto continua a funzionare senza modifiche:
pass/warning/fail, gate del fuori tolleranza con autorizzazione del capoturno,
Cp/Cpk, carta di controllo, colonne del CSV di produzione. Una quota da camera è
una misura come le altre.
### 8. La sorgente immagine è un'astrazione a tre facce
Il motore riceve un'immagine e non sa da dove venga. Le tre sorgenti sono: dal
vivo da un device tramite l'agente, un'immagine di riferimento salvata sul
server, un file caricato dal Maker.
Non è un'astrazione nostra: è quella che VisionSuite si è già data — *«espongono
una sorgente, non una telecamera: è questo che permette di collaudare un impianto
a cinque telecamere senza avere le cinque telecamere sul tavolo, e di scrivere
prove che danno sempre lo stesso risultato»*.
Sblocca tre cose che altrimenti non ci sarebbero:
- il Maker compone alla scrivania, senza tenere occupata una stazione né avere il
pezzo in mano
- una ricetta si prova prima di andare in produzione, invece di scoprirne i
difetti misurando
- diventa possibile una **prova di non regressione**: salvando accanto
all'immagine il risultato atteso, si riesegue l'intero parco ricette dopo un
aggiornamento di VisionSuite e si vede quali quote si sono mosse
Quest'ultima è la rete di sicurezza che rende sostenibile aggiornare il
sottomodulo, con un motore che sta su due host.
## Architettura
### I componenti
| Componente | Nuovo | Dove vive | Responsabilità |
|---|---|---|---|
| `vendor/visionsuite` | sì | sottomodulo git | i pacchetti `vs-core`, `vs-camera`, `vs-pm2d`, `vs-measure`, `vs-dxf`, su un commit fisso |
| Vision runner | sì | libreria condivisa | immagine + grafo + calibrazione → quote e artefatti. Unica implementazione, due host |
| Vision worker | sì | container nel compose | espone il runner dietro una API interna. Il container pesante |
| Agente di stazione | sì | PC di reparto | possiede i device, acquisisce, apre la connessione verso il server, incorpora il runner se configurato |
| Backend FastAPI | modificato | container esistente | tipi di task, grafo sul task, registro device, inoltro, ingresso risultati. **Nessun import di VisionSuite** |
| Frontend Flask | modificato | container esistente | Maker: device, fotogramma, aggancio quote. Measure: schermata del task con camera |
L'aggancio del sottomodulo segue la forma prescritta dal README di VisionSuite:
dipendenza dai soli pacchetti che servono, dichiarati in path editable.
```toml
[tool.uv.sources]
visionsuite = { path = "vendor/visionsuite/packages/vs-core", editable = true }
vs-camera = { path = "vendor/visionsuite/packages/vs-camera", editable = true }
```
I due host non montano lo stesso insieme, ed è il punto dell'intera struttura di
VisionSuite:
| Pacchetto | Worker | Agente | Perché |
|---|---|---|---|
| `vs-core` | sì | sì | strutture di base e calibrazione, servono a entrambi |
| `vs-pm2d` | sì | solo se elabora in locale | pattern matching |
| `vs-measure` | sì | solo se elabora in locale | primitive e quote |
| `vs-dxf` | sì | solo se elabora in locale | confronto col disegno. È quello che porta PyTorch |
| `vs-camera` | **no** | sì | il worker non tocca hardware: riceve immagini già acquisite |
Un agente su una stazione che delega al server monta quindi due soli pacchetti,
`vs-core` e `vs-camera`, e resta leggero. È la differenza fra un eseguibile
distribuibile e un'installazione da cinque gigabyte su ogni PC di reparto.
I nomi di importazione non sono uniformi — `vs-core` si importa come
`visionsuite`, `vs-pm2d` come `pm2d`, `vs-dxf` come `dxf_compare` — ed è una
stortura nota e dichiarata a monte, non un errore da correggere qui.
### Il flusso di una misura, esecuzione sul server
```
operatore apre il task
→ client chiede al server di eseguire
→ server chiede all'agente un fotogramma sul device dichiarato
→ agente acquisisce e carica l'immagine
→ server passa immagine e grafo al worker
→ worker restituisce le uscite
→ server applica pass/fail, gate del fuori tolleranza, salva in measurements
→ client mostra esito e immagine con le zone dove ha guardato
```
### Il flusso con esecuzione in locale
Il giro si accorcia: il server manda il grafo all'agente, l'agente acquisisce ed
esegue, e rimanda uscite più immagine. **Da lì in poi il server fa le stesse
identiche cose.** Il verdetto, il gate e il salvataggio non cambiano mai posto:
cambia chi calcola i numeri, non chi decide se sono buoni.
È questo che rende le due strade equivalenti a valle, e che permette di cambiare
la configurazione di una stazione senza toccare né il frontend né le ricette.
## Modello dati
### Il grafo di visione
`recipe_tasks` prende **`vision_json`** (JSON, nullable): il grafo di `vs-task`
serializzato, opaco al server.
Il versionamento arriva gratis. Il copy-on-write delle ricette copia già i task
in profondità, quindi una modifica al grafo su una ricetta con misure produce una
versione nuova come qualunque altra modifica; in `recipe_service` è un campo in
più nella copia, non una logica nuova.
Non si riusa `annotations_json`, che sta lì accanto: le annotazioni sono un
disegno per l'operatore, il grafo è un programma da eseguire. Hanno cicli di vita
e regole di validazione diversi, e mescolarli significa non poter validare né
l'uno né l'altro.
### Il legame fra quota del grafo e quota della ricetta
`recipe_subtasks` prende **`vision_output`** (String, nullable): la coppia
«identificativo dello strumento, nome della sua uscita».
È la stessa forma di legame che `vs-task` usa internamente, e per la stessa
ragione dichiarata nel suo README: *«Il legame è un ID, non un puntatore»* — un
riferimento in memoria non si scrive su disco.
Tutto il resto della subtask — nominale, UTL, UWL, LWL, LTL, unità — resta com'è
e continua a significare quello che significa oggi.
### Le misure
`input_method` passa da `("usb_caliper", "manual")` a
`("usb_caliper", "manual", "camera")`.
Il contorno della visione **non** va su `measurements`. Quella tabella la leggono
la statistica e l'export a ogni giro, e appesantirla la rovina. Ma soprattutto
sarebbe sbagliato di modello: una sola acquisizione produce N quote, e immagine,
overlay, device e versione del motore sono gli stessi per tutte.
Nasce quindi **`vision_results`**, una riga per **esecuzione**:
| Campo | Perché c'è |
|---|---|
| `image_path`, `overlay_path` | l'immagine, e quella con sopra le zone dove ha guardato |
| `engine_version` | il commit di VisionSuite che ha prodotto i numeri |
| `executed_on` | `server` o `station` |
| `station_id`, `device_id` | da quale macchina e quale camera |
| `calibration_snapshot` | la taratura in vigore in quel momento, **copiata** |
| `graph_snapshot` | il grafo effettivamente eseguito, **copiato** |
| `duration_ms`, `executed_at` | quanto è costata, quando |
Su `measurements` una sola colonna nuova: `vision_result_id`, FK nullable.
Calibrazione e grafo si copiano invece di essere referenziati per la stessa
ragione: una taratura rifatta sei mesi dopo non deve riscrivere il significato di
una misura già presa.
### Il registro dei device
**`station_devices`**: `station_id`, `code` (l'identificativo locale), `kind`
(`camera` oggi, `light` domani), `label`, `capabilities` (JSON), `calibration`
(JSON) con `calibration_taken_at`, `last_seen_at`.
Si popola dall'agente. Il server lo riceve e non lo modifica.
### Le immagini di riferimento
**`vision_reference_images`**: `task_id`, `path`, e la provenienza per intero —
`station_id`, `device_id`, snapshot della calibrazione, `engine_version`,
`acquired_at`, una nota — più `expected_json` per l'atteso della prova di non
regressione.
La provenienza non è rimandabile. Senza, si riesegue il grafo mesi dopo, escono
numeri diversi, e non c'è modo di distinguere un motore aggiornato da un'ottica
spostata.
### Migrazione 011
Cinque cose: `vision_json` su `recipe_tasks`, `vision_output` su
`recipe_subtasks`, `camera` in `input_method_enum`, `vision_result_id` su
`measurements`, e le tre tabelle nuove.
Più una sesta, **da fare adesso o mai**: `task_type_enum` contiene `xf_compare`,
refuso per `dxf_compare`. Oggi nessuna riga usa quel valore e correggerlo costa
una riga. Dal primo task salvato in poi diventa una migrazione di dati, e il
refuso finisce nelle API pubbliche.
Come le migrazioni 008 e 010, va scritta in *batch mode* per la compatibilità
SQLite dei test e verificata in resa MySQL con `alembic upgrade X:Y --sql` prima
del deploy: è la procedura che il progetto si è già dato.
## API
L'agente apre lui la connessione. **Il server non bussa mai a un PC**, e questo
evita di dover aprire porte sulle macchine di reparto — che è materia della D-4.
| Verso | Endpoint | Cosa fa |
|---|---|---|
| agente → server | `WS /api/stations/agent` | si autentica con la chiave di stazione, dichiara i device, resta in ascolto |
| server → agente | comando `acquire` | «scatta sul device X»; l'agente carica con `POST /api/vision/frames` |
| server → agente | comando `execute` | esecuzione in locale: manda il grafo, riceve le uscite |
| client → server | `POST /api/vision/execute` | esegui il task per la produzione in corso; **il server sceglie dove** |
| client → server | `POST /api/vision/preview` | authoring: esegui su una sorgente qualsiasi, non salvare nulla |
| client → server | `GET /api/stations/{code}/devices` | l'elenco per il Maker |
| client → server | `POST /api/vision/reference-images` | salva un fotogramma come riferimento |
| server → worker | `POST /run` | interno, mai esposto: immagine e grafo → uscite e overlay |
Un solo endpoint decide dove si esegue, ed è `execute`. Il client non sa e non
deve sapere se ha misurato il server o la stazione: chiede una misura e riceve un
esito.
## Errori e casi limite
| Caso | Comportamento |
|---|---|
| Versione del motore diversa fra stazione e server | la stazione si rifiuta di misurare e lo dichiara. Mai produrre numeri con un motore non concorde |
| Agente non raggiungibile | il task di visione non è eseguibile e lo dice. Da decidere con il cliente se la stazione debba consentire il ripiego sul calibro |
| Server non raggiungibile, stazione con motore | la stazione esegue in locale e lo segna in `executed_on` |
| Device dichiarato dalla ricetta ma offline | la ricetta resta valida, il task spiega perché non può girare |
| Il grafo non produce un'uscita attesa da una subtask | la misura non si salva, l'errore nomina l'uscita mancante |
| Elaborazione in crash o oltre il tempo | il worker isola il guasto, l'API resta in piedi, il task riporta l'errore |
| Device senza calibrazione | le quote in millimetri non si producono. Mai convertire con un fattore implicito |
## Prove
Ai tre file di test che il progetto ha già e che leggono i sorgenti invece di
renderizzare — `test_offline.py`, `test_layout_shell.py`,
`test_template_js_syntax.py` — se ne aggiunge la stessa specie:
- **una prova che il server non importi VisionSuite.** È il confine su cui poggia
la decisione 4, e senza qualcuno che lo controlli si perde in silenzio
- **una prova che il commit del sottomodulo dichiarato coincida** con quello che
server e agente montano
Per il resto, il runner si prova su immagini di riferimento salvate — che è
precisamente ciò per cui l'astrazione della sorgente esiste. Nessuna prova
richiede una camera collegata.
## Quello che questo design non risolve
- **Il dimensionamento del server.** Il pattern matching è pesante e, con
l'esecuzione lato server come default, la CPU è contesa fra tutte le stazioni.
Va aggiunto alla **D-4**, che finora chiedeva solo quale macchina e quanto
disco: ora chiede anche quanti core e quanta RAM.
- **La conservazione delle immagini.** Due politiche distinte, entrambe da
portare in D-4: le immagini di riferimento sono poche e vivono quanto la
ricetta; quelle di produzione sono una per pezzo e crescono senza limite, e
vogliono una scadenza o una regola del tipo «si conservano solo quelle dei
pezzi fuori tolleranza».
- **Che cosa fa la stazione se la rete cade** e non è configurata per elaborare
in locale: si blocca, o consente il calibro e segna il task come non
eseguibile? È una domanda per il cliente.
- **L'impacchettamento dell'agente.** Le macchine possono essere Linux o Windows
e i driver possono stare sulla macchina o nel container. La decisione è
volutamente tardiva; il design la rende tale tenendo l'agente sottile e neutro.
## Da verificare prima di cominciare
VisionSuite è stato lavorato fino al 15/08/2026 e il suo ultimo commit è
*«Rassegna delle firme pubbliche prima che si congelino»*: le API si stanno
stabilizzando proprio ora. Prima di fissare il commit del sottomodulo:
- **il suo README è indietro rispetto al codice.** Dichiara `vs-measure` «da
scrivere» quando invece esiste, con circa 4.865 righe e tredici file di test, e
dichiara `vs-dxf` non avviabile quando l'import di `common.calibration` non c'è
più. Va riletto lo stato reale, non quello documentato.
- **il test di calibrazione dato per rosso** — la focale recuperata a 373 contro
456 attesi, con tolleranza al 5% — va verificato sullo stato attuale. Il README
di VisionSuite avverte di guardarlo *prima* di costruirci sopra misure in
millimetri, che è esattamente ciò che questo lavoro fa. Le calibrazioni sono
state toccate il 13/08 e lo stato potrebbe essere cambiato.
- **il peso dell'ambiente.** `vs-dxf` dipende da PyTorch e l'installazione
completa arriva a circa 5 GB. È la ragione per cui il worker è un container a
parte, e va confermato che il solo sottoinsieme necessario pesi meno.