Skip to content

Vue d'ensemble de l'architecture ​

Une page pour expliquer toute la stack à un collègue qui débarque sans contexte. À lire en premier ; les docs détaillées (scan-architecture.md, les pages …-ingest par TCG, le journal de phases scan-lab-plan) creusent les décisions spécifiques.

Snapshot daté du 2026-05-08.

Pour le schema API/comptes/images et les frontieres de securite examines le 2026-10-03, voir Backend, API, images et autorisations. Les chiffres et parcours ci-dessous sont un historique de mai, pas un audit des services actuellement deployes.

TL;DR (le pitch en 30 secondes) ​

  • Les utilisateurs scannent une carte avec leur téléphone et reçoivent identité + prix + gestion de collection. Cinq TCG supportés aujourd'hui : Pokémon, One Piece, Magic the Gathering, Lorcana, Flesh and Blood.
  • Tout tourne sur notre propre VPS. Aucun appel API externe pendant un scan. Aucune dépendance runtime au repo d'un pote, même si l'un de nos containers vient à l'origine de chez lui.
  • La reconnaissance utilise des embeddings d'image DINOv2-small en entrée 518×518 avec correction de perspective OpenCV avant le modèle (même prétraitement que l'app de référence de Tormknd). Deux implémentations parallèles (FAISS sidecar vs pgvector dans notre Postgres) — voir Les deux paths de scan ci-dessous.
  • Catalogue total au 2026-05-08 : 143 452 vecteurs sur 5 TCG et ~1 200 sets, ~26 GB d'images sur disque, tout réembeddé en 518 + warp.

Vision d'ensemble ​

┌──── Téléphone (React Native) ─────────────────────────────┐
│  ScannerScreen → backendScan.ts                           │
│  Fan-out parallèle (Promise.allSettled) vers 4 endpoints :│
│   • POST :8080/scan-lab    (Lab,      mono-signal)        │
│   • POST :8000/scan-compat (Compat,   mono-signal)        │
│   • POST :8002/scan        (Tormknd,  mono-signal)        │
│   • POST :8080/scan        (Combined, MULTI-signal)       │
└────────────────────┬──────────────────────────────────────┘
                     │
                     ▼
┌──── Notre VPS — docker compose ───────────────────────────┐
│                                                           │
│  ┌─ tcg-vision   :8000 ─┐  Index FAISS, baké au build :   │
│  │  /scan-compat        │  ~30 k Pokémon + OP + Lorcana   │
│  │  DINOv2 + FAISS      │  (notre fork de tgc-app)        │
│  └────────┬─────────────┘                                 │
│           │ enrichissement via /cards/:id                 │
│           ▼                                               │
│  ┌─ api Fastify :8080 ──┐  /cards, /sets,                 │
│  │  /scan-lab           │  /scan-lab  (pgvector mono)     │
│  │  /scan               │  /scan      (multi-signal :     │
│  │                      │   pHash + OCR + DINOv2)         │
│  └────────┬─────────────┘                                 │
│           ▼                                               │
│  ┌─ postgres (pgvector) ┐  cards (~145 k), sets,          │
│  │  card_embeddings_lab │  card_embeddings_lab (HNSW)     │
│  │                      │  — partagée par Lab + Combined, │
│  └──────────────────────┘  cards.phash + cards.printed_*  │
│                                                           │
│  ┌─ embeddings sidecar ─┐  DINOv2-small @ 518×518,        │
│  │  interne :8000       │  + warp OpenCV (CPU),           │
│  │                      │  ~2 cards/s sur 3 cores         │
│  └──────────────────────┘                                 │
│                                                           │
│  ┌─ tormknd-api  :8002 ─┐  Code tgc-app non modifié,      │
│  │  /scan               │  étalon A/B figé,               │
│  │  (--profile tormknd) │  off par défaut                 │
│  └──────────────────────┘                                 │
└───────────────────────────────────────────────────────────┘

Plus un worker ingest one-shot (aussi dans compose) qui pull TCGdex / OPTCG API / Scryfall et écrit dans cards + data/images/.

Statut par TCG ​

TCGSourceCartesSetsPath scanFAISSpgvectorOù vit l'index
PokémonTCGdex (multi-langues)~23 k204/scan-lab✅ baké (rollback)✅card_embeddings_lab (à nous)
One PieceOPTCG API~3 k20/scan-lab✅ baké (rollback)✅card_embeddings_lab (à nous)
MagicScryfall bulk-data~107 k976/scan-lab❌ pas dans FAISS✅card_embeddings_lab (à nous)
LorcanaLorcast API~2.6 k—/scan-lab❌ pas dans FAISS✅card_embeddings_lab (à nous)
Flesh and Bloodthe-fab-cube GitHub bulk~9.3 k—/scan-lab❌ pas dans FAISS✅card_embeddings_lab (à nous)

Au 2026-05-08, les cinq TCG scannent via le path pgvector (/scan-lab), tous embeddés en 518×518 + correction de perspective façon Tormknd. Le sidecar FAISS de Tormknd tourne encore pour le rollback (couvre Pokémon + One Piece + un peu de Lorcana) mais n'est plus dans le chemin live. L'asymétrie d'origine a disparu — on ingest, embed et query tout chez nous. Voir catalog-updates.md pour le cycle d'ajout d'un nouveau set.

Les quatre paths de scan ​

Mise à jour du 2026-05-10 : un quatrième path a été ajouté à la fan-out mobile — Combined, l'endpoint /scan historique du Node API. Multi-signal (pHash + OCR Tesseract + DINOv2 dans un decideMatch unifié), il complète les trois précédents qui sont mono-signal embedding. Voir scan-comp pour la comparaison détaillée et tcg-collector-backend/docs/scanner/SCAN_PATHS.md pour la matrice technique complète.

Les quatre passent par DINOv2 pour produire un vecteur de 384 dim ; trois d'entre eux n'utilisent que ce signal (mono-signal embedding), le quatrième le combine avec pHash et OCR (multi-signal).

/scan-lab (Fastify)/scan-compat (tcg-vision)/scan (tormknd-api)/scan Combined (Fastify)
Port8080800080028080
PipelineMono-signal embeddingMono-signal embeddingMono-signal embeddingMulti-signal : pHash + OCR + embedding
Vector storepgvector HNSW (Postgres)FAISS IndexFlatIP (in-memory)FAISS IndexFlatIP (in-memory)pgvector + colonnes phash / printed_*
Source de l'indexTable card_embeddings_lab, peuplée par embedCatalogLab.tsservices/tcg-vision/data/faiss_index.bin baké au build (fork qui a divergé)tgc-app/data/faiss_index.bin du pote Tormknd, non modifiéTable card_embeddings_lab (depuis 2026-05-10, partagée avec Lab) + cards.phash + cards.printed_*
TCG couvertsPokémon + OP + Magic + Lorcana + FAB (143 452 vecteurs)Pokémon + One Piece + un peu de Lorcana (~30 370)Identique à /scan-compat (même dataset upstream)Pokémon + OP + Magic + Lorcana + FAB
Enrichissement métadonnéesJOIN SQL natif vers cardsAdapter qui rappelle notre api pour les non-PokémonAucun — réponse brute du upstreamJOIN SQL + signaux exposés (match.phash, ocr.*, embedding.*)
Utilisé par l'app RN✅ Bouton Scan Lab (défaut)Bouton Scan Compat (rollback / regression-bisect)Bouton Scan Tormknd (A/B contre l'upstream)Bouton Scan Combined (4e cellule, multi-signal)
Démarrage docker compose✅ par défaut✅ par défaut⚠ profile --profile tormknd (off par défaut)✅ par défaut (même container que Lab)

Phase F shippée le 2026-05-07 : l'app RN est passée sur /scan-lab pour le chemin par défaut. Depuis le 2026-05-09, le ScannerScreen.tsx fan-out parallèle vers tous les paths actifs en Promise.allSettled ; depuis le 2026-05-10, c'est 4 cellules côte-à-côte au lieu de 3 (ajout du Combined multi-signal). Le user choisit le winner — pas d'auto-rerank côté app, le but du fan-out est la comparaison.

Quand chaque path tranche différemment : Combined peut retourner no-match reason low-confidence quand son decideMatch n'arrive pas à faire converger pHash, OCR et embedding sur la même réponse — c'est par design (voir scan-comp pour le détail du cascade). Avant 2026-05-10, Combined retournait low-confidence quasi systématiquement parce qu'il consultait une table d'embeddings legacy card_embeddings qui s'est avérée vide (l'ingest qui la peuplait avait été abandonné depuis longtemps). Fix appliqué : Combined pointe maintenant sur la même table card_embeddings_lab que Lab.

Attention re-embed : quand embedCatalogLab.ts FORCE=true régénère les vecteurs (ex. après le warp-fix #135), il met à jour card_embeddings_lab — table partagée par Lab ET Combined depuis 2026-05-10. Les .bin FAISS de compat et tormknd restent figés (le warp-fix a été porté dans le code Python via #138, sans rebuild des bins puisque build_index.py n'appelle pas correct_perspective).

Pourquoi tout local ​

Toutes les briques de la stack sont sur notre VPS. Les seules sources de données entrantes sont au build/ingest (TCGdex / OPTCG / Scryfall) — jamais au runtime.

  • Un scan en 2030 avec TCGdex offline : marche encore. Index + images en local.
  • Un scan avec le repo Tormknd supprimé : marche encore. Le container qu'on a buildé est pinné chez nous.
  • DB perdue : rebuild depuis data/images/ (toujours sur disque) en relançant embedCatalogLab.ts (~3 h pour le catalogue complet).

Décomposition disque :

CheminContenuTaille
data/images/Tous les jpeg de cartes, rangés par set/numéro~26 GB
data/postgres/cards, sets, card_images, card_embeddings_lab~500 MB
data/cache/scryfall/JSON bulk Scryfall rafraîchi quotidiennement (gardé entre runs)~514 MB

Roadmap ​

PhaseStatut
A — Schéma card_embeddings_lab + endpoint /scan-lab✅ shippé le 2026-05-06 (#103)
B — Schéma cards variant-aware✅ shippé le 2026-05-06 (#105)
C — Ingest OP variant-aware✅ shippé le 2026-05-06 (#107)
D — Embed du catalogue complet✅ shippé le 2026-05-07 (3 TCG, 131 k+ vecteurs)
E — Cross-recall vs photos téléphone✅ shippé le 2026-05-07 (5/5 photos SV01, a surfacé + fixé la corruption disque variants OP)
F — Flip de l'app RN vers /scan-lab✅ shippé le 2026-05-07 (poc-tcg-collector#127)
Ingest Magic end-to-end✅ shippé le 2026-05-07 (#111)

Les trois TCG (Pokémon, One Piece, Magic) résolvent maintenant via /scan-lab. Items reportés : ingest Lorcana (API Lorcast + conversion AVIF→JPEG nécessaire, ~2-3h), Flesh and Blood (pas de source clean au 2026-05-07), retrait du container tcg-vision après quelques semaines de validation prod.

Voir aussi ​