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

19 KiB

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, 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:

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 sottomodulo git i pacchetti vs-core, vs-camera, vs-pm2d, vs-measure, vs-dxf, su un commit fisso
Vision runner libreria condivisa immagine + grafo + calibrazione → quote e artefatti. Unica implementazione, due host
Vision worker container nel compose espone il runner dietro una API interna. Il container pesante
Agente di stazione 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.

[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 strutture di base e calibrazione, servono a entrambi
vs-pm2d solo se elabora in locale pattern matching
vs-measure solo se elabora in locale primitive e quote
vs-dxf solo se elabora in locale confronto col disegno. È quello che porta PyTorch
vs-camera no 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.