Appearance
3. Mobile — 5ème cellule « Frame » dans le fan-out
Publié le 2026-05-12
Aboutissement de Path #5 « Frame » côté mobile. Première fois aujourd'hui qu'un utilisateur voit un changement concret dans l'app.
Avant / après
| Avant aujourd'hui | À partir d'aujourd'hui | |
|---|---|---|
| Capture | 1 photo | 1 photo |
| Backends interrogés | 4 (Lab / Compat / Tormknd / Combined) | 5 (+ Frame) |
| Cellules affichées | 4 | 5 |
| Pre-processing côté client | aucun | detectQuad + warpQuad pour la cellule Frame |
Comment marche la cellule Frame
L'écran Scanner fait toujours Promise.allSettled sur tous les modes. La nouveauté : pour le mode 'frame', le map callback fait un détour avant l'envoi réseau.
captured.path ──▶ detectQuad(uri)
│
┌──────┴──────┐
▼ ▼
null { corners, confidence, ts }
│ │
│ ▼
│ warpQuad(uri, corners)
│ │
│ ┌──────┴──────┐
│ ▼ ▼
│ null "file:///cache/scan-frame-warp-<ts>.jpg"
│ │ │
▼ ▼ ▼
{ ok: false, reason: 'no-quad-client' } scanViaBackend({ path: warped, … },
{ endpointMode: 'frame',
url: /scan-frame })Pourquoi pas de fallback sur la photo brute
Quand detectAndWarp rend null (carte non détectée ou warp KO), on n'envoie rien à /scan-frame. Une cellule vide étiquetée no-quad-client apparaît à la place.
Raison : si on retombait sur la photo brute, on perdrait la comparaison A/B nette. On veut savoir précisément quand le warp client améliore le matching et quand il rate. Une cellule qui se rabat silencieusement biaise les chiffres.
Les 4 autres cellules continuent de partir avec la photo brute en parallèle, donc l'utilisateur a toujours un résultat (sauf si tous les backends sont en panne — auquel cas le fallback on-device existant prend le relais).
Implémentation
Nouveau type de failure
ts
export type BackendScanFailure = {
ok: false;
reason:
| 'network'
| 'timeout'
| 'http-4xx'
| 'http-5xx'
| 'malformed-response'
| 'aborted'
| 'no-quad-client'; // ← nouveau
// …
};Distinct de 'malformed-response' (le serveur a renvoyé du JSON imbuvable) et d'un ok: true, match: null (le serveur a regardé l'embedding et n'a rien matché). Les trois cas ont des correctifs différents — il est précieux de les distinguer côté UI debug.
Nouvel orchestrateur
src/scan/quadDetector/detectAndWarp.ts :
ts
export async function detectAndWarp(photoUri: string): Promise<string | null> {
const detection = await detectQuad(photoUri);
if (detection === null) return null;
return warpQuad(photoUri, detection.corners);
}Pas de try/catch — les deux wrappers absorbent déjà leurs propres erreurs en null (pattern défensif documenté en PR 2a / 4a).
Ordre dans le fan-out
ts
const SCAN_MODES: readonly ScanEndpointMode[] = [
'lab', 'compat', 'tormknd', 'combined', 'frame',
];Les 4 cellules « photo brute » d'abord, la cellule « warp client » en dernier. Comme ça l'A/B est visuel : sur une même capture, on lit ligne par ligne ce que chaque backend a fait de la photo brute, puis ce que /scan-frame a fait du crop rectifié.
TDD
- 4 nouveaux tests pour
detectAndWarp(detectQuad null skip, warpQuad null bubble, happy path, payload forwarding). backendConfig.test.tsétendu pour asserter l'URL + le port du mode'frame'.scanDebugFormat.test.tsétendu pour asserter le label'Frame'.
335 / 335 jest, tsc --noEmit clean.
Ce que tu verras sur device
Capture une carte → 5 cellules apparaissent dans le summary, ordonnées Lab → Compat → Tormknd → Combined → Frame.
Cellule Frame réussie → similarité affichée, image de la carte matchée à côté. Devrait être ≥ celle de
/scan-labsi le crop client est correct.Cellule Frame en échec → label "no-quad-client". Causes typiques :
- Bord de carte mal détecté (contraste insuffisant)
- Carte trop près d'un bord du cadre photo
- Bougé / flou de mouvement
- Angle trop oblique (au-delà de ~25-30°)
Les 4 autres cellules continuent de fonctionner normalement dans tous ces cas.
Scan Debug affiche désormais 5 lignes par capture, groupées par
inputPhotoPath.
Métrique à surveiller
Le ratio « Frame matche le même card_id que Lab » sur les vraies photos de terrain. C'est l'indicateur direct de :
- la qualité de la détection OpenCV côté client (
detectQuad) - la qualité du warp perspective (
warpQuad) - la robustesse de DINOv2 aux variations de crop
Si le ratio est élevé, on peut envisager de retirer le détecteur de bords côté serveur (detectCardBounds dans _detect_quad_in_edges) sur les autres routes — économie de CPU non négligeable sur la backend.
Pour aller plus loin
- Backend /scan-frame — la route qui répond au mode
'frame' - Mobile warpQuad — perspective warp natif OpenCV — le bloc qui produit le crop
- Path #5 « Frame » — alternatives considérées — la décision d'origine
poc-tcg-collector#XXX— PR 4b (à remplacer après ouverture)