diff --git a/docs/superpowers/specs/2026-08-16-visionsuite-integration-design.md b/docs/superpowers/specs/2026-08-16-visionsuite-integration-design.md new file mode 100644 index 0000000..5e8aaac --- /dev/null +++ b/docs/superpowers/specs/2026-08-16-visionsuite-integration-design.md @@ -0,0 +1,399 @@ +# 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.