Files
TieMeasureFlow/docs/superpowers/specs/2026-08-16-visionsuite-integration-design.md
T
Adriano 6bfe3a5d1a docs(spec): registra le correzioni imposte dall'esecuzione
Il gate del fuori tolleranza vale per l'acquisizione e non per la quota:
la regola era pensata per la misura sequenziale e su un evento simultaneo
produceva un blocco permanente.

Piu' il bi-ambiente Python, la versione del motore timbrata al build, la
motivazione della divisione degli extra corretta, e due cose promesse e non
mantenute -- expected_json e image_path -- dichiarate come consegne del
piano 1b invece che lasciate come intenzioni.

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

23 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.

Non ancora consegnata. La colonna expected_json esiste, ma nessun percorso la scrive — nemmeno preview, che è l'unico posto dove un grafo gira davvero su un'immagine. La prova di non regressione è quindi promessa da questo documento e non mantenuta da nessuna riga di codice. Diventa una consegna nominata del piano 1b: «preview può promuovere le proprie uscite a expected_json». Finché non è fatta, questa decisione non va dichiarata completa.

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.

Correzioni imposte dall'esecuzione

Il piano derivato da questo documento è stato eseguito il 16/08/2026. Sei cose che qui erano scritte male o taciute, corrette dai fatti.

Il gate del fuori tolleranza vale per l'acquisizione, non per la quota

La decisione 7 e il testo del punto 5 parlano di «la quota successiva». Quella regola era stata pensata per la misura manuale e sequenziale, dove esiste davvero un momento successivo in cui l'operatore potrebbe tirare avanti con una lettura cattiva irrisolta.

Una camera produce tutte le N quote nello stesso istante. Dentro un'acquisizione non esiste un «tirare avanti», quindi lì non c'è nulla da presidiare — e applicare la regola sequenziale a un evento simultaneo produceva un blocco permanente: la lettura fuori tolleranza non veniva mai registrata, quindi nessuno poteva autorizzarla, e rifare la stessa foto ripeteva l'errore.

Il gate si valuta una volta per acquisizione: se la misura pendente è fra le quote che questa acquisizione riprodurrà non blocca — riacquisire è rimisurare quelle quote, che questo documento già dichiara la via d'uscita che non richiede il capoturno. Fra due acquisizioni, e alla chiusura del ciclo, blocca come prima.

Il repository è deliberatamente bi-ambiente

vs-task, vs-measure e vs-pm2d dichiarano requires-python >= 3.13; vs-core si ferma a 3.10. Backend e frontend restano su 3.11, l'albero della visione gira su 3.13, e l'extra vision porta i marker d'ambiente che tengono separate le due risoluzioni.

Questo rafforza la decisione 4 più di come era argomentata: con VisionSuite dentro il server FastAPI, questa scoperta avrebbe costretto l'intero backend a 3.13.

La versione del motore si timbra al build

vendor/visionsuite/.git è un file che rimanda a ../../.git/modules/..., che nel contesto di build non esiste: interrogare git dentro il container non funziona. engine_version() legge VISION_ENGINE_VERSION dall'ambiente, ripiega su git nei checkout di sviluppo, e alza un errore se non risolve né l'uno né l'altro. Il build fallisce se la variabile manca.

La divisione degli extra compra esplicitezza, non peso

Gli extra sono due — vision per il runner, vision-worker per il servizio web — ed è la forma giusta. Ma la motivazione che sembrava ovvia è falsa: vs-pm2d dichiara di suo fastapi, uvicorn, python-multipart e pillow, quindi un agente di stazione che fa pattern matching se li porterà dietro comunque. La correzione vera è a monte — una libreria non dovrebbe dipendere da un server web — e va aggiunta alle verifiche su VisionSuite qui sotto.

L'immagine di produzione non viene ancora conservata

vision_results.image_path e overlay_path migrano e restano NULL: nulla le scrive. Il flusso descritto in questo documento si chiude con «il client mostra esito e immagine», e finché quelle colonne sono vuote una misura contestata non ha prova materiale. Consegna nominata del piano 1b, insieme all'overlay.

Prima di applicare le migrazioni in produzione

La 011 rinomina un valore di enum su una tabella in esercizio. Va contato prima che nessuna riga usi xf_compare: in strict mode una riga sopravvissuta fa fallire l'ALTER a metà deploy, fuori strict mode diventa '' in silenzio. Attenzione anche alla finestra: il MODIFY su recipe_tasks è una rinomina di valore e userà ALGORITHM=COPY, mentre quello su measurements.input_method aggiunge in coda e dovrebbe restare INPLACE — da confermare sulle dimensioni reali, perché measurements è la tabella grossa.

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.