Appearance
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
| TCG | Source | Cartes | Sets | Path scan | FAISS | pgvector | Où vit l'index |
|---|---|---|---|---|---|---|---|
| Pokémon | TCGdex (multi-langues) | ~23 k | 204 | /scan-lab | ✅ baké (rollback) | ✅ | card_embeddings_lab (à nous) |
| One Piece | OPTCG API | ~3 k | 20 | /scan-lab | ✅ baké (rollback) | ✅ | card_embeddings_lab (à nous) |
| Magic | Scryfall bulk-data | ~107 k | 976 | /scan-lab | ❌ pas dans FAISS | ✅ | card_embeddings_lab (à nous) |
| Lorcana | Lorcast API | ~2.6 k | — | /scan-lab | ❌ pas dans FAISS | ✅ | card_embeddings_lab (à nous) |
| Flesh and Blood | the-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/scanhistorique du Node API. Multi-signal (pHash + OCR Tesseract + DINOv2 dans undecideMatchunifié), il complète les trois précédents qui sont mono-signal embedding. Voirscan-comppour la comparaison détaillée ettcg-collector-backend/docs/scanner/SCAN_PATHS.mdpour 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) | |
|---|---|---|---|---|
| Port | 8080 | 8000 | 8002 | 8080 |
| Pipeline | Mono-signal embedding | Mono-signal embedding | Mono-signal embedding | Multi-signal : pHash + OCR + embedding |
| Vector store | pgvector HNSW (Postgres) | FAISS IndexFlatIP (in-memory) | FAISS IndexFlatIP (in-memory) | pgvector + colonnes phash / printed_* |
| Source de l'index | Table card_embeddings_lab, peuplée par embedCatalogLab.ts | services/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 couverts | Poké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ées | JOIN SQL natif vers cards | Adapter qui rappelle notre api pour les non-Pokémon | Aucun — réponse brute du upstream | JOIN 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=truerégénère les vecteurs (ex. après le warp-fix#135), il met à jourcard_embeddings_lab— table partagée par Lab ET Combined depuis 2026-05-10. Les.binFAISS decompatettormkndrestent figés (le warp-fix a été porté dans le code Python via #138, sans rebuild des bins puisquebuild_index.pyn'appelle pascorrect_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çantembedCatalogLab.ts(~3 h pour le catalogue complet).
Décomposition disque :
| Chemin | Contenu | Taille |
|---|---|---|
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
| Phase | Statut |
|---|---|
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
- Architecture du scan de cartes — même diagramme, plus de détails par composant
- Plan — validation pgvector via /scan-lab — journal de phases complet avec historique d'incidents
- Ingest Magic the Gathering — le précédent : comment un nouveau TCG rejoint le path pgvector
- Rollback — intégration One Piece — comment dérouler un ajout de TCG si ça part en vrille
tcg-collector-backend/docs/INGEST.md— référence technique côté backend (pipeline, sources par TCG, multi-langues)